htmlforge
Build web pages with Python, export to HTML with a single command — like manim generates videos.
manim scene.py MyScene → mp4
htmlforge page.py MyPage → html
htmlforge script app.py → js
Installation
pip install htmlforge
Quick Start
1. Write a Python page script mysite.py
from htmlforge import Doc
page = Doc("My Site")
# Generate styles with Python kwargs (no CSS strings!)
page.style("body", background="#f5f5f5", font_family="sans-serif")
page.style(".py-card", border_radius="12px", box_shadow="0 2px 8px rgba(0,0,0,.1)")
# Build the page
page.heading("Welcome to My Site")
page.text("Built with **htmlforge**, supports *Markdown* syntax")
page.card("Features",
"40+ HTML components",
"Generate styles with Python code",
"Export HTML with one command",
)
page.table(
["Feature", "Description"],
[
["Components", "40+ HTML components"],
["Built-in theme", "Works out of the box"],
["CLI tools", "render / serve / watch / script"],
],
)
page.button("Click me", on_click="alert('Hello!')", color="#4CAF50")
2. Run the command to export HTML
# Basic render
htmlforge render mysite.py
# Specify output directory
htmlforge render mysite.py -o build
# Render and start a local server
htmlforge render mysite.py -s
# Watch for file changes and auto-rebuild
htmlforge render mysite.py -w
# Render a specific page (when file has multiple pages)
htmlforge render mysite.py MyPage
CLI Commands
| Command | Description |
|---|---|
htmlforge render <file.py> |
Render all pages to dist/ |
htmlforge render <file.py> -o <dir> |
Specify output directory |
htmlforge render <file.py> <page> |
Render only the specified page |
htmlforge render <file.py> -s |
Start HTTP server after render |
htmlforge render <file.py> -s -p 3000 |
Specify server port |
htmlforge render <file.py> -w |
Watch for changes and auto-rebuild |
htmlforge render <file.py> -s -w |
Serve + watch, browser auto-reloads on change |
htmlforge script <file.py> |
Convert Python file to JavaScript |
htmlforge script <file.py> -o app.js |
Specify output JS filename |
htmlforge script <file.py> --helpers |
Include DOM helper functions |
Doc API — Ultra Simple
Doc is a high-level API designed for Python programmers who don't know HTML. It comes with a built-in theme — no CSS required to build great-looking pages.
from htmlforge import Doc
page = Doc("Title")
# Generate styles with Python kwargs (no CSS strings!)
page.style("body", background="linear-gradient(135deg, #667eea, #764ba2)", color="white")
page.style("h1", font_size="3rem", text_align="center")
page.style(".py-card", background="rgba(255,255,255,.15)", backdrop_filter="blur(10px)")
# Content (supports **Markdown** syntax)
page.heading("Hello World")
page.text("Supports **bold**, *italic*, `code`, [links](url)")
page.card("Card Title", "Card content, **supports Markdown**")
page.list(["Item A", "Item B", "Item C"])
page.table(["Col 1", "Col 2"], [["a", "b"], ["c", "d"]])
page.button("Button", on_click="alert('hi')", color="#e74c3c")
page.input("Type here...", name="name")
page.select(["Option A", "Option B", "Option C"])
page.code("print('hello')", lang="python")
page.details("Collapsible", "Hidden content...")
page.nav(("Home", "/"), ("About", "/about"))
page.divider()
# Row / grid
page.row(Doc.make_card("A"), Doc.make_card("B"), Doc.make_card("C"))
# Head / SEO
page.set_meta(description="My site", author="Me", keywords="python, web")
page.meta("My Site", property="og:title")
page.favicon("favicon.ico")
Block-level Markdown
page.markdown() renders full Markdown — not just inline — including headings,
lists, fenced code, tables, blockquotes and rules:
page.markdown('''
# Release Notes
- **Fast** rendering
- Zero dependencies
| Command | Purpose |
|---------|---------|
| render | Build HTML |
> Tip: pull in a whole README with `markdown_file`.
''')
# Or load straight from a file
page.markdown_file("README.md")
Static Assets
page.asset() registers a local file (image / css / js). It returns the URL to
reference, and htmlforge render copies the file into dist/assets/ so the
exported site is fully self-contained:
logo = page.asset("logo.png") # → "assets/logo.png"
page.image(logo, "Logo")
page.add_stylesheet(page.asset("theme.css"))
page.js_file(page.asset("app.js"))
After htmlforge render site.py, dist/ holds both index.html and
assets/logo.png.
Layouts — Share Header / Footer
A Layout is a reusable page skeleton: every page built from it shares the
same head tags, header and footer.
from htmlforge import Doc, Layout, Div, H1, P
site = Layout(
head=lambda doc: doc.set_meta(author="Me"),
header=lambda doc: Div(class_="top").add(H1("My Site")),
footer=lambda doc: P(f"© 2026 — {doc.title}"),
)
home = site.page("Home") # → a Doc with the shared chrome
home.heading("Welcome")
about = site.page("About")
about.text("About us...")
htmlforge render site.py
# → dist/home.html, dist/about.html (both wrapped in the shared header/footer)
Each slot accepts an Element, a list of elements, or a callable receiving the
page — so you can highlight the current nav item, inject the title, and more.
JavaScript & DOM Interaction
Bind events to elements
from htmlforge import Button
btn = Button("Click me")
# Bind raw JavaScript
btn.on("click", "alert('hello')")
btn.on("mouseover", "this.style.color='red'")
# Bind Python code (auto-converted to JS)
btn.on_py("click", "print('clicked!')")
# → onclick="console.log('clicked!');"
Add JavaScript to the page
# Raw JavaScript
page.js("document.title = 'Dynamic'")
page.js("function greet() { alert('hello'); }")
# External JS file
page.js_file("app.js")
# Python code → JavaScript (auto-converted)
page.py_script('''
def greet(name):
print(f"Hello {name}")
greet("World")
''')
# Run on page load (DOMContentLoaded)
page.on_ready("console.log('Page loaded!')")
page.on_ready_py('print("Ready!")')
DOM manipulation helpers
# Get element properties (returns JS expressions)
val = page.get_attr("#input", "value") # → document.querySelector('#input').getAttribute('value')
text = page.get_text("#title") # → document.querySelector('#title').textContent
html = page.get_html("#content") # → document.querySelector('#content').innerHTML
value = page.get_value("#myInput") # → document.querySelector('#myInput').value
# Set element properties (generates JS scripts)
page.set_attr("#el", "class", "active")
page.set_text("#title", "New Title")
page.set_html("#content", "<b>bold</b>")
page.set_value("#input", "hello")
# Visibility
page.show("#modal")
page.hide("#loading")
page.toggle("#sidebar")
# CSS class manipulation
page.add_class("#el", "active")
page.remove_class("#el", "hidden")
page.toggle_class("#menu", "open")
Security: every selector and value passed to these helpers is automatically escaped, so it is safe to feed them dynamic / user data.
Python → JavaScript Transpiler
Use htmlforge script to convert Python files to JavaScript:
htmlforge script app.py # → app.js
htmlforge script app.py -o out.js # → out.js
htmlforge script app.py --helpers # include DOM helpers ($, $$, getAttr, setAttr, ...)
Or use it programmatically:
from htmlforge import py_to_js, convert_file
# Convert Python source string to JS
js = py_to_js("""
def greet(name):
print(f"Hello {name}")
for i in range(10):
greet("World")
""")
# Convert a file
convert_file("app.py", "app.js")
Supported Python → JS conversions
| Python | JavaScript |
|---|---|
print(...) |
console.log(...) |
input(...) |
prompt(...) |
def f(x): |
function f(x) { |
class Foo(Bar): |
class Foo extends Bar { |
self.x |
this.x |
if/elif/else: |
if/else if/else { |
for i in range(n): |
for (let i = 0; i < n; i++) { |
for x in items: |
for (let x of items) { |
while cond: |
while (cond) { |
try/except/finally: |
try/catch/finally { |
True / False / None |
true / false / null |
and / or / not |
&& / || / ! |
a // b |
Math.floor(a / b) |
a ** b |
Math.pow(a, b) |
len(x) |
x.length |
f"Hello {name}" |
`Hello ${name}` |
lambda x: x + 1 |
(x) => x + 1 |
x in items |
items.includes(x) |
sorted(items) |
[...items].sort() |
Page API — Fine-grained Control
Use Page + the component system when you need more control:
from htmlforge import Page, H1, P, Div, Table, Button
page = Page("Title", lang="en")
page.add_css("body { font-family: sans-serif; }")
page.add(
H1("Welcome"),
P("Hello World"),
Div(class_="card").add(
H1("Features", level=2),
P("Content..."),
),
)
Element — Chainable API
div = Div(id="main", class_="wrapper")
div.add(H1("Title"), P("Content")) # Add child elements
div.css(color="red", font_size="16px") # Set inline styles (camelCase → kebab-case)
div.attr("data-id", "123") # Set attributes
div.on("click", "alert('clicked')") # Bind JS event
div.on_py("click", "print('clicked')") # Bind Python event (auto-converted)
html = div.render() # → HTML string
Components
| Category | Components |
|---|---|
| Structure | Div, Section, Header, Footer, Nav, Main, Article, Aside, Span, Container |
| Text | H1-H6, P, Strong, Em, Blockquote, Code, Pre, Small, Mark |
| Links/Media | Link, Img, Video, Audio, Iframe, Canvas |
| Lists | Ul, Ol, Li, List |
| Tables | Table, Thead, Tbody, Tr, Th, Td, Caption |
| Forms | Form, Input, Textarea, Select, Option, Checkbox, Radio, Label, Button |
| Other | Hr, Br, Text, Progress, Details, Summary |
Example: Multi-page Site
# site.py
from htmlforge import Doc
home = Doc("Home")
home.heading("Home")
home.text("Welcome to the home page")
about = Doc("About")
about.heading("About Us")
about.text("This is the about page")
blog = Doc("Blog")
blog.heading("Blog")
blog.text("Latest posts...")
htmlforge render site.py
# → dist/home.html, dist/about.html, dist/blog.html
Changelog
0.0.3
Security
_escnow also escapes single quotes; Markdown links rejectjavascript:/vbscript:/data:text/htmlURLs.- All DOM helpers (
set_text,set_attr,set_value,add_class, ...) escape their selectors and values, preventing JS injection and syntax breakage from dynamic data.
New
- SEO / meta:
page.meta(),page.set_meta(),page.favicon(). - Block-level Markdown:
page.markdown()/page.markdown_file()— headings, lists, fenced code, tables, blockquotes and rules. - Static assets:
page.asset()copies local files intodist/assets/at build time, so the exported site is self-contained. - Layouts: reusable
Layoutshares head / header / footer across pages. - Live reload:
htmlforge render <file> -s -wauto-refreshes the browser.
Fixed
Videobuilds its<source>child correctly (no more fragile__new__).
License
Metadata
Release files for htmlforge 0.0.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| htmlforge-0.0.3.tar.gz | 38.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| htmlforge-0.0.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.6 kB
Release files / htmlforge-0.0.3.tar.gz
| Download URL | htmlforge-0.0.3.tar.gz |
|---|---|
| Size | 38.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c8916d79727f95b568e9658ae73caf99bfd045d26e887e52f95bcb758ee73c81
|
|
BLAKE2b-256 checksum How to use checksums |
478a89487fcf0394fe6c45882db13df76293d00a5e75f2ba37f41acf3b2d9643
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.8
|
Release files / htmlforge-0.0.3-py3-none-any.whl
| Download URL | htmlforge-0.0.3-py3-none-any.whl |
|---|---|
| Size | 28.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8c9501a01d3b33e0419353ef25d18ea688083fbdd9cd06d450af7a2d5464acbd
|
|
BLAKE2b-256 checksum How to use checksums |
0c353f5331540eff044943f879997c267b0e854f976d1400fb76752629e96352
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.8
|
Security for htmlforge 0.0.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.