Skip to main content

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.

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

  • _esc now also escapes single quotes; Markdown links reject javascript: / vbscript: / data:text/html URLs.
  • 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 into dist/assets/ at build time, so the exported site is self-contained.
  • Layouts: reusable Layout shares head / header / footer across pages.
  • Live reload: htmlforge render <file> -s -w auto-refreshes the browser.

Fixed

  • Video builds its <source> child correctly (no more fragile __new__).

License

MPL-2.0

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)

Source distribution for htmlforge 0.0.3
File Size Uploaded Provenance
htmlforge-0.0.3.tar.gz 38.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for htmlforge 0.0.3
File Interpreter ABI Platform
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page