How this application is put together: why the files, tasks, and configuration exist,
and how the pieces work together during development and deployment. Based on
DESIGN.md in the repository root.
The application is a standard FastAPI app, assembled with holm. Pages render on the server. The browser receives that HTML plus two built static assets: the stylesheet and the JS bundle.
A Next.js-like metaframework: standard FastAPI, htmy, and
HTMX composed through file-system conventions. Files in
app/ become pages and actions.
The standard web framework underneath. Pages and actions are plain FastAPI routes, so the ecosystem works unchanged.
Server-side rendering in typed, async Python with JSX-like components. No template language required.
Dynamic updates without a client framework. Endpoints return HTML fragments, rendered via FastHX.
Utility-first styling for the application and its components.
Accessible components as editable Python htmy code,
vendored into your repo from
htmui.
You own the files.
New to holm? The holm in a hurry
guide covers the essentials in under five minutes.
.
├── .agents/skills/ # holm-web agent skill
├── app/ # Application package; holm walks this
│ ├── main.py # FastAPI app, holm wiring, static files, layout slots
│ ├── settings.py # pydantic-settings; which CSS/JS files to serve
│ ├── head.py # <head> component, with page metadata support
│ ├── nav.py # Root navigation; highlights the current page
│ ├── error.py # Error handlers; 404 redirects to /not-found
│ ├── layout.py # Root layout; renders layout.jinja (optional)
│ ├── layout.jinja # Root layout markup
│ ├── not_found/ # Not-found page package (served at /not-found)
│ │ └── page.py
│ └── page.py # Home page
├── assets/ # Stylesheet and JS bundle sources
│ ├── app.css # Tailwind input; your CSS goes here
│ └── app.js # JS entry point; your scripts go here
├── components/ # BasecoatUI as Python; yours to edit
├── static/ # Build outputs, served at /static
├── Procfile # honcho process definitions (poe start)
├── package.json # Tailwind, HTMX, and the JS bundler
└── pyproject.toml # Python dependencies, tool config, poe tasks
This application also includes a greeting action (app/actions.py),
this design page (app/design/), a component showcase (app/showcase/),
and demo styles (assets/demo.css). Delete any you don't need.
Components live outside app/
holm walks app/: page.py becomes a route,
layout.py or layout.jinja wraps pages,
actions.py defines endpoints that return HTML fragments.
UI components live in components/ at the project root, where
discovery never touches them. Import them as
from components.button import button.
assets/ vs static/
assets/ holds the source files for the stylesheet and the JS
bundle; you edit those. static/ holds the compiled files the app
serves. Don't edit anything in static/ by hand.
The project ships the holm-web agent skill in
.agents/skills/. Agents should load it before working on the
application: it documents holm's routing, layout, page, action, and form
conventions, and can answer questions about the library itself. See the
holm skill command for managing it in other projects.
app/main.py creates the FastAPI app, mounts static/ at
/static, adds gzip compression, and hands everything to
holm.App(). Custom FastAPI middleware, exception handlers, or
additional mounts belong there.
Slots are configured in the same call. The root layout renders three named slots
besides children: head (the <head>
component from app/head.py), nav (the current-page-aware
navigation bar from app/nav.py), and theme_switcher.
Nested layouts can define further slots.
Within app/, holm's conventions apply:
page.py serves GET requests for its package's path,
layout.py (or layout.jinja) wraps the pages of its
package and everything below it, and actions.py defines endpoints
that return HTML fragments, a natural fit for HTMX. error.py maps error
codes to handlers; the 404 handler redirects to /not-found. app/layout.py is
optional. holm renders layout.jinja on its own when no
Python counterpart exists; the file exists to make the mechanism explicit and to
serve as a starting point for programmatic layouts.
app/head.py renders the <head> and reads page
metadata (such as this page's title) from the htmy context.
app/settings.py holds runtime settings via pydantic-settings, reading
.env when present, including which stylesheet and JS bundle to serve:
the CSS_FILE and JS_FILE environment variables behind
poe dev and poe preview.
Python is not bundled; only the stylesheet and the JS bundle are built. Sources
live in assets/ (app.css for your CSS, app.js
for your scripts) and are compiled into static/ in two flavors:
| Source | Dev build | Production build |
|---|---|---|
assets/app.css |
static/app-dev.css: unminified, rebuilt on every save |
static/app.css: minified, in the repo |
assets/app.js |
static/app-dev.js: unminified, rebuilt on every save |
static/app.js: minified, in the repo |
The minified files belong in the repo so a fresh checkout can run and deploy
without a production build. The dev files are build artifacts that can be deleted
and regenerated at any time. Which pair is served is decided by
CSS_FILE and JS_FILE from app/settings.py,
rendered into <head> by app/head.py.
All build commands are poe tasks (build-dev-css,
build-prod-css, build-dev-js, build-prod-js;
poe build runs the production builds) delegating to the Tailwind CLI
and the JS bundler. The package.json exists for these tools, and
there is no application JavaScript beyond what assets/app.js imports.
The components/ directory is a copy of the
htmui
BasecoatUI catalog: pure Python htmy components in your project, not
a locked dependency. Open a file and change it. Import them as
from components.button import button.
uv run poe start # http://localhost:5000
That runs three processes together via honcho, as defined in the
Procfile:
fastapi dev, with auto-reloadstatic/app-dev.cssstatic/app-dev.js
The app is the first process in the Procfile, and honcho
assigns ports to processes in that order, so the app gets port 5000. Edit
assets/, app/, or components/, save, refresh.
Run the application standalone (for example uv run poe dev or
fastapi dev) and it comes up on the FastAPI default port instead,
which is 8000. poe dev serves the dev stylesheet and JS bundle;
poe preview serves the minified files, which is what will be deployed,
so use it to verify production assets.
uv.lockpoe: recurring commands as tasks; poe check runs all checks in one go
A holm application is a plain FastAPI application, so it deploys
anywhere FastAPI is supported as a first-class citizen. Deployments serve
the minified builds, so build them first:
uv run poe build
Providers that support the [tool.fastapi] configuration in
pyproject.toml work out of the box with no extra
configuration, for example:
uv run fastapi deploy