How it all fits together.
index97 is a small set of cooperating systems. Each one does one thing.
File-based routing
At startup, index97 scans your pages directory recursively and builds a route table. Every file with a recognized extension becomes a route.
Route types
| Extension | Type | Behavior |
|---|---|---|
| .phtml | page | Server-rendered template. Optionally paired with a .js sibling for data. Wrapped in the nearest _layout.html. |
| .html | document | Served as a full HTML document. No layout applied. Good for self-contained pages like error pages. |
| .js | handler | Exports named HTTP method functions. Paired with a .phtml or .html sibling for rendering. Wins over .html at the same pattern. |
| .md | markdown | Rendered via Bun's built-in markdown renderer. YAML front matter maps keys to layout slots. |
Naming conventions
| Pattern | Meaning |
|---|---|
| _filename | Private — not a route. Used for layouts, auth helpers, DB modules, partials. |
| [param] | Dynamic segment — matches any URL segment, available as req.params.param. |
| index | Maps to the directory root (/blog/index.phtml → /blog). |
Request lifecycle
For a handler route (.js with a .phtml template):
- Request arrives at
Bun.serve() resolveMethod()checks for_methodform field and rewrites the method- The exported handler function (
GET,POST, etc.) is called with the request - If it returns a plain object, the object is merged with the
.phtmltemplate viarender(), and the data is retained resolveIncludes()processes any<include>tags in the rendered HTML, with the handler's data available for@keyattribute referencesfindLayout()walks up directories to find the nearest_layout.htmlapplyLayout()extracts named slots and injects content into the layout- If a
_layout.jsexists, itsdata(req)is called and the result renders layout-level variables rewriteFormMethods()converts non-standard form methods to POST + hidden_methodfield- In dev mode, the HMR script is injected
- The final HTML is returned as a
Response
Layout system
_layout.html acts as a wrapper for all pages in its directory and subdirectories. index97 walks up from the current page's directory until it finds one (or reaches the pages root).
Pages populate layout slots using <template data-slot="name"> elements. The layout references them with default value.
A sibling _layout.js can export data(req) to provide request-aware data to the layout itself — used for things like session-aware navigation.
Partials system
The <include src="partial.phtml"> tag includes server-side partials. Data is passed explicitly via attributes. The parent's data context — whether from a .phtml page handler or a .js route handler — is always available for @key resolution:
| Syntax | Meaning |
|---|---|
attr="@key" | Pass data.key from the parent context (the @ sigil resolves from parent data) |
attr="literal" | Pass the string "literal" as a value |
| (no attrs) | The partial inherits the full parent data context |
Error pages
index97 looks for error pages by walking up from the failing route's directory:
- 404 errors →
_404.html - All other errors →
_error.html
This means a /blog/_404.html handles 404s within the blog section, while the root /_404.html catches everything else. Error pages receive {{status}}, {{title}}, and {{message}} variables.
Hot reload
In dev mode, index97 watches the pages directory with Bun's file watcher. Changes are broadcast to connected browsers via SSE at /__index97_hmr. The injected client script handles two cases:
- CSS changes — re-stamps the
<link>href with a?t=timestampquery string, forcing the browser to re-fetch the stylesheet with no page reload and no scroll reset. - All other changes — fetches the updated page HTML, parses it with
DOMParser, then morphs the live DOM in place. Attributes, text nodes, and child elements are patched recursively. No full page reload, no lost state.
Form method override
HTML forms only natively support GET and POST. index97 provides two-way method override:
- Server rendering:
<form method="DELETE">is rewritten tomethod="POST"with a hidden<input name="_method" value="DELETE"> - Request handling:
resolveMethod()reads the_methodfield and constructs a newRequestwith the correct HTTP method before dispatching to the handler