Hedron field guide for coding agents¶
Human readers
This page is designed to be given directly to a coding agent. It is intentionally dense, explicit, repetitive at important boundaries, and organized for retrieval rather than narrative reading. For a human learning path, start with Build your first app.
Agent objective¶
Build and modify Hedron applications using the stable public 1.0 API. Preserve ordinary web semantics, FastAPI behavior, accessibility, security, and production constraints. Do not invent Hedron symbols or infer behavior from a similarly named React, template, or Python UI API.
Target the published hedron>=1.0.0 train unless the project lockfile says otherwise.
Inspect the installed version before changing an existing application:
This guide's canonical host is Hedron, the FastAPI-native application. If the project uses
HedronFlask, HedronDjango, or Edron, stop and use that host's documentation; do not paste
FastAPI decorators into an adapter application.
Operating rules¶
Apply these rules in order:
- Read
pyproject.toml, the application constructor, and existing route/component patterns. - Preserve the existing host, security profile, session configuration, theme, and dependency injection model.
- Prefer exports documented under
from hedron import .... Usehedron_coreonly when a referenced public contract explicitly places a type there, such asNodeLike. - Use
@app.pagefor navigable documents,@app.viewfor safe replaceable reads, and@app.actionfor mutations. - Compose components; do not call child
.render(), concatenate HTML, or convert a child to a string. - Use a view handle's
path,dom_id, and built-in controls. Do not guess an HTMX target. - Keep durable state in application-owned storage. Rendering must not perform hidden writes.
- Use built-in presentation props and components before adding CSS. Never create a React/Vite project merely to style a Hedron app.
- Keep FastAPI dependencies, middleware, lifespan, response models, and JSON routes when they already solve the problem.
- Run focused tests,
hedron check, and the production build before declaring completion.
Translate from HTML, CSS, and React¶
Hedron produces normal HTML and CSS, but its component tree is evaluated on the Python server. HTMX performs bounded HTTP requests and DOM swaps. Alpine is used for disposable browser-local presentation state. There is no React runtime, virtual DOM, hydration pass, or general-purpose client state store.
| Familiar concept | Hedron equivalent | Important difference |
|---|---|---|
| HTML document route | @app.page("/path") |
The handler returns a component tree; Hedron renders the document. |
| HTML element | html.div(...), html.button(...) |
Attributes are Python arguments and values are validated/escaped. |
| JSX component | Component[Props] or a function returning components |
Rendering occurs on the server and must be pure. |
children |
Positional component arguments or children= |
Pass nodes, not rendered HTML strings. |
| React Router page | FastAPI/Hedron route | HTTP is the source of truth; normal middleware and dependencies apply. |
| Server component/read | @app.view |
Returns a targeted HTML fragment handle for HTMX. |
| Mutation/action | @app.action |
Unsafe methods are subject to authentication, authorization, validation, and CSRF. |
fetch() then setState() |
HTMX request and swap | Prefer a generated view/action handle over handwritten request wiring. |
useState for open/selected state |
Built-in local component behavior or Alpine | Use only for disposable presentation state, never authoritative business state. |
| Database/query state | Application service called by a route | Keep it server-owned; do not store it in Alpine or the DOM. |
| CSS flex column | Stack |
Use validated gap, alignment, density, and responsive props. |
| CSS flex row | Inline |
Preserve source and focus order when wrapping. |
| CSS grid/media query | Grid(columns={"base": 1, "md": 3}) |
Responsive maps are mobile-first and use finite breakpoints. |
| CSS module | Component-local styles.css and style symbols |
Hedron scopes/fingerprints CSS during its build. |
className |
class_ |
Built-in classes remain; an application class is an extension hook. |
aria-* / data-* |
aria={...} / data={...} |
Prefer semantic components that already own the relationship. |
dangerouslySetInnerHTML |
TrustedHtml at a reviewed trust boundary |
Plain strings are escaped. Never wrap untrusted input to silence validation. |
| React effect | FastAPI lifespan, middleware, route, job, or explicit browser behavior | Never start I/O or mutate durable state from render(). |
Equivalent tree construction¶
HTML/JSX:
<main>
<h1>Deployments</h1>
<section aria-label="Current status">
<p>All systems operational</p>
<button type="button">Refresh</button>
</section>
</main>
Hedron:
from hedron import Card, Heading, Main, Stack, Text
content = Main(
Stack(
Heading("Deployments", level=1),
Card(
Text("All systems operational"),
title="Current status",
),
)
)
Add the refresh button only after declaring the server view it calls; unlike an inert JSX
onClick placeholder, the interaction must have an HTTP owner.
Core runtime model¶
Hedron is still FastAPI¶
Hedron subclasses FastAPI. Use normal FastAPI features beside Hedron pages:
import os
from fastapi import Depends
from hedron import Hedron, Text
app = Hedron(
title="Operations",
security="standard",
explorer="off",
session_secret=os.environ.get(
"HEDRON_SESSION_SECRET",
"replace-in-production",
),
)
def current_user() -> str:
return "ada" # Replace with an application-owned authentication dependency.
@app.get("/api/health")
def api_health() -> dict[str, str]:
return {"status": "ok"}
@app.page("/account")
def account(user: str = Depends(current_user)):
return Text(f"Signed in as {user}")
Do not create a second FastAPI app to add JSON routes. Do not bypass existing dependencies when adding a page or action.
Components describe output; the renderer emits output¶
A component is closer to a pure React function component than to a mutable widget object:
from hedron import Card, Component, Props, Text
from hedron_core import NodeLike
class UserCardProps(Props):
name: str
role: str
class UserCard(Component[UserCardProps]):
props_type = UserCardProps
def __init__(self, *, name: str, role: str) -> None:
super().__init__(UserCardProps(name=name, role=role))
def render(self) -> NodeLike:
return Card(Text(self.props.role), title=self.props.name)
Requirements:
- Declare
props_typeon concrete components. - Treat props as immutable.
- Return
NodeLike; do not return aResponsefromComponent.render(). - Do not query a database, call a remote service, write state, or enqueue work in
render(). - Pass
UserCard(...)into a parent. Never passUserCard(...).render(). - Let the top-level request renderer handle escaping, assets, diagnostics, and identity.
Choose the interaction owner¶
Use this decision sequence:
Is it a navigable document?
yes -> @app.page
no -> Does it change server or durable state?
yes -> @app.action
no -> Does it fetch/recompute replaceable server content?
yes -> @app.view
no -> Is it disposable local presentation state?
yes -> built-in local behavior or Alpine
no -> ordinary component composition
Examples of server state: database records, permissions, jobs, carts, workflow state, user preferences, data shared across tabs/users, and anything that must survive navigation.
Examples of local presentation state: whether a disclosure is open, the selected tab before it
affects server data, temporary menu visibility, and transient focus behavior. Prefer built-ins such
as Tabs, Expander, Dialog, Popover, and ColorModeToggle; they already carry accessibility
and progressive-enhancement behavior.
Canonical complete application¶
This is a copy-safe baseline containing a full page, replaceable GET view, classic POST action, CSRF field, built-in styling, ordinary FastAPI form parsing, and a fragment test:
import os
from datetime import datetime, timezone
from fastapi import Form as FastAPIForm
from hedron import (
Card,
CsrfField,
Form,
Heading,
Hedron,
Page,
Stack,
SubmitButton,
Text,
TextInput,
html,
redirect_local,
)
app = Hedron(
title="Operations",
security="standard",
explorer="off",
session_secret=os.environ.get(
"HEDRON_SESSION_SECRET",
"replace-in-production",
),
)
_NOTES: list[str] = [] # Demo only. Use durable storage in production.
@app.view("/status")
def status():
stamp = datetime.now(timezone.utc).strftime("%H:%M:%S UTC")
return html.div(
Text(f"All systems operational · refreshed {stamp}"),
role="status",
aria={"live": "polite"},
)
@app.action("/notes", method="POST")
def add_note(note: str = FastAPIForm(...)):
value = note.strip()
if value:
_NOTES.append(value)
return redirect_local("/")
@app.page("/")
def home() -> Page:
return Page(
Stack(
Heading("Operations", level=1),
Card(
status(),
status.refresh_button("Refresh status"),
title="Service status",
),
Card(
Form(
CsrfField(),
TextInput("note", required=True),
SubmitButton("Save note"),
action="/notes",
method="post",
),
title=f"Notes ({len(_NOTES)})",
),
),
title="Operations",
)
from fastapi.testclient import TestClient
from app import app, status
def test_home_and_status_fragment() -> None:
with TestClient(app) as client:
page = client.get("/")
fragment = client.get(
status.path,
headers={
"HX-Request": "true",
"HX-Target": status.dom_id,
},
)
assert page.status_code == 200
assert "<html" in page.text.lower()
assert fragment.status_code == 200
assert "<html" not in fragment.text.lower()
assert "All systems operational" in fragment.text
Expanded production reference: Full code on GitHub
Run and inspect it:
python -m pip install "hedron>=1.0.0" "uvicorn[standard]"
uvicorn app:app --reload
python -m hedron --app app:app routes
python -m hedron --app app:app check
The list is intentionally marked as demo-only. Multiple workers do not share Python process memory, and a restart erases it.
Page, view, and action contracts¶
Page: equivalent to a server-rendered route component¶
@app.page("/reports")
def reports():
return Stack(
Heading("Reports", level=1),
Text("Choose a report."),
)
A page should return the document's presentation tree. It may receive FastAPI dependencies,
path/query parameters, and a Request. Keep writes out of GET page handlers.
View: equivalent to a server read plus targeted render¶
@app.view("/queue-size")
def queue_size():
return Text("12 jobs waiting")
@app.page("/")
def dashboard():
return Stack(
queue_size(),
queue_size.refresh_button("Refresh queue"),
)
The decorated function becomes a handle:
queue_size()materializes its stable host in the page.queue_size.pathis the registered request path.queue_size.dom_idis the authorized target ID.queue_size.refresh_button(...)emits matching HTMX request/target attributes.
Do not replace this with a made-up hx-target="#queue" unless you deliberately use the
advanced FragmentRegion API and preserve the same region through route, control, and response.
Action: equivalent to an API mutation, not an event callback¶
from hedron import refresh
@app.action("/queue/retry", method="POST", fallback="/")
def retry_failed_jobs():
retry_jobs_in_service_layer()
return refresh(queue_size).toast("Retry requested")
Treat the action like any production API endpoint: authenticate, authorize the specific object, validate input, make the write idempotent when appropriate, and return an explicit outcome. A UI control is not an authorization boundary.
HTML authoring rules¶
Prefer a semantic built-in when one exists. Use the html factory for small native details:
from hedron import Heading, Link, Main, Text, html
tree = Main(
Heading("Audit log", level=1),
Text("Recent security events"),
html.ul(
html.li("Session created"),
html.li("Role changed"),
aria={"label": "Events"},
data={"source": "audit"},
class_="audit-events",
),
)
Rules:
- Plain strings and
Text(...)are escaped. - Use
class_, notclassName. - Use
aria={"label": ...}anddata={"name": ...}for grouped attributes. - Python-friendly HTMX spellings such as
hx_get=are supported, but handles and controls are safer because they keep the URL and target synchronized. - Use native links for navigation and buttons/forms for actions. Do not make a clickable
div. - Preserve heading order, labels, landmarks, keyboard behavior, focus, and live-region semantics.
- Use
SafeUrlfor deliberate URL trust boundaries andTrustedHtmlonly after reviewed sanitization. Never mark user input trusted to make an error disappear.
CSS and visual design rules¶
Think of Hedron built-ins as a typed design system over ordinary CSS. Start with semantic composition rather than recreating a Tailwind or CSS-in-JS layer in Python.
from hedron import Card, Grid, Heading, Metric, Stack, Text
overview = Stack(
Heading("Overview", level=1),
Grid(
Card(Metric("Requests", "18.4k", delta="+7%", delta_tone="up")),
Card(Text("84 ms"), title="p95 latency"),
Card(Text("No incidents"), title="Incidents"),
columns={"base": 1, "md": 3},
),
gap="lg",
)
Priority order:
- Choose the correct semantic/component primitive.
- Use shared props such as
appearance,emphasis,size,density,gap,padding,elevation, and responsive maps when the component documents them. - Use
Theme,DesignSystem,StyleScope, or a documented theme token for product-wide presentation. - For a project-owned component with unique visual detail, colocate
styles.cssin its component folder and use Hedron's scoped style-symbol/build path. - Use
class_as a stable extension hook when a stylesheet owns that class.
Do not:
- invent unsupported props because they resemble CSS properties;
- pass arbitrary inline
styledictionaries as the primary styling system; - inject runtime
<style>elements; - copy CSS used by documentation simulations into an application;
- target undocumented private descendant selectors;
- add React, npm, Vite, or a client CSS framework unless the project explicitly owns that separate frontend architecture.
Hedron's production build compiles and fingerprints component CSS and assets. Node.js is not required for the normal Hedron build.
State ownership¶
| State | Owner | Wrong owner |
|---|---|---|
| Open menu/tab/disclosure | Built-in local behavior or Alpine | Database/session round trip for every toggle |
| Form draft before submit | Native browser form controls | Global Python variable |
| Authenticated identity | Host session/identity provider | DOM, Alpine, hidden field |
| Record or workflow state | Database/service layer | Component instance or module list |
| Job progress | Durable job backend, polled by a view | Unbounded in-memory task registry in production |
| Cache | Configured bounded backend and explicit policy | Unbounded module dictionary |
| Request-specific values | FastAPI request/dependencies | Mutable process-global singleton |
Assume multiple processes and concurrent requests. A component object is not a React component instance with a persistent lifecycle. Reconstruct the UI from authoritative state on each request.
Forms and security¶
For a cookie-authenticated FastAPI/Flask form:
- use
@app.actionfor the unsafe route; - render
CsrfField()inside the form; - perform a safe GET before testing POST so the client receives the CSRF cookie;
- validate with FastAPI/Pydantic or
FormBody; - authenticate and authorize in dependencies or the service boundary;
- never trust hidden fields for identity, tenant, price, role, or permission;
- redirect after a classic successful POST, or return a validated fragment outcome for HTMX.
Hedron's CSRF protection does not replace authorization. Escaping does not make a URL, redirect, or raw HTML trusted. A signed cookie prevents tampering; it does not provide server-side revocation or make secrets safe to store in the cookie.
Testing pattern¶
Test documents and fragments separately. The test_app.py tab in the canonical application
uses the view handle as the target authority and verifies that a fragment response does not
silently become a full document.
Also test:
- unknown HTMX targets fail with
403; - unauthenticated and unauthorized callers fail closed;
- invalid form data produces the documented validation response;
- loading, empty, success, and recoverable-error branches preserve the target boundary;
- keyboard/focus behavior and announcements in a real browser for interactive components;
- production construction/build with the actual deployment settings.
Troubleshooting algorithm¶
Diagnose in this order:
- Confirm interpreter and version:
python -c "import hedron; print(hedron.__version__)". - Run
python -m hedron --app module:app routesand verify path/method ownership. - Run
python -m hedron --app module:app check; follow theHED-*remediation. - Inspect browser Network data: request URL, method, status,
HX-Request,HX-Target, response body, and responseHX-*headers. - Inspect rendered HTML for the expected host ID and static asset URLs.
- Reproduce with
TestClientusing the same cookies, headers, and form fields. - Only then inspect lower-level renderer or framework internals.
| Symptom | Likely cause | Correct response |
|---|---|---|
ModuleNotFoundError: hedron |
Wrong environment/interpreter | Activate the project environment; install/sync with that interpreter. |
hedron: command not found |
Console script not on PATH |
Use python -m hedron. |
| Blank/unstyled page | Static assets not mounted or proxy path stripped | Use Hedron() or mount_hedron_static; preserve /hedron-static/ and /hedron-assets/. |
| Button does nothing | No executable route/HTMX attributes, JS asset failure, or disabled control | Use a handle control; inspect HTML and Network. |
Fragment returns 403 |
HX-Target does not match the declared host/region |
Use handle.dom_id or the handle's built-in control. Do not weaken the allowlist. |
POST returns 403 |
Missing/mismatched CSRF cookie and token, or authorization denial | GET first, send CsrfField() value, then distinguish CSRF from authz. |
Request returns 422 |
Input names/types do not match FastAPI/Pydantic boundary | Align form names and model fields; render validation errors. |
| Full document appears inside a panel | A view/action returned a page or the wrong route was called | Return only the fragment tree for the target. |
| Replacement loses future refreshes | outerHTML response did not preserve the host boundary |
Return the complete target host or use the generated view handle contract. |
| Works with one worker only | Process-local state/cache/jobs/session assumption | Move authoritative state to shared durable infrastructure. |
| CSS change missing in production | Build manifest/assets are stale | Run hedron build and deploy the generated build directory. |
| Production startup refuses to run | Missing build, weak secret, in-memory backend, unsafe plugin/explorer setting | Fix the named gate; do not casually suppress it. |
Never “fix” a 403, CSRF error, safe URL error, or production gate by disabling the policy before
identifying the violated contract.
Production completion criteria¶
Before claiming a Hedron app is production-ready:
- pin Hedron and optional packages to compatible bounded ranges;
- use
security="standard"or"strict"and an explicit strongsession_secret; - turn Explorer off or secure it with real authorization;
- run
hedron buildand deploy its manifest/assets; - configure durable cache/job backends for production and multi-worker operation;
- use shared session infrastructure or a deliberate affinity model when required;
- keep authentication, object authorization, tenant isolation, CSRF, and validation explicit;
- bound uploads, caches, job payloads, tables, streams, and in-memory collections;
- preserve static paths and root-path behavior through the reverse proxy;
- run tests plus
hedron check; smoke the real proxy path and an unsafe form action.
Use Ship a Hedron app as the release checklist.
Forbidden substitutions¶
Do not substitute these patterns unless the user explicitly requests a different architecture:
- React component state for server-authoritative state;
- client-side fetching for an existing
@app.viewhandle; - a generic
<div>tree for available semantic Hedron components; - raw string HTML for components or
html.*nodes; - manual
HX-*response headers forInteractionResult/response helpers; - guessed CSS properties as component kwargs;
- process-local dictionaries for production persistence;
- a GET route for a mutation;
- hidden form values for authorization;
- private FastAPI, Starlette, HTMX, Alpine, or Hedron internals for a documented public API.
Retrieval map¶
When this page is insufficient, read only the smallest relevant authority:
| Need | Authority |
|---|---|
| First runnable application | Build your first app |
| Route/component mental model | Core concepts |
| API selection | Choose an interaction API |
| Exact symbol by task | API by task |
| Components and constructor contracts | Component catalog |
| HTMX requests and targets | HTMX interactions |
| Local browser state | What is Alpine? |
| Forms and CSRF | Minimal form · Forms and actions |
| Styling and component CSS | Comprehensive styling |
| Tests | Test your UI |
| Failure lookup | Troubleshooting · Error codes |
| Deployment | Ship · Deployment |
| Exact signatures | Autodoc |
| Stability/support | Stability · Current release |
When source code and public documentation appear to disagree, do not silently depend on an internal behavior. Verify the installed version, prefer the stable documented contract, and report the discrepancy.