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
pip install htmlforgefrom 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")# 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| 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 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 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()
# Layout
page.row(Doc.make_card("A"), Doc.make_card("B"), Doc.make_card("C"))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!');"# 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!")')# 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")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")| 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() |
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..."),
),
)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| 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 |
# 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