Dev Server
ReeWeb's development server serves templates and markdown files directly - no SSG step required. It renders .ree and .md files on demand, provides live reload via WebSocket when you edit sources, and serves static assets from both src/public/ and the project root static/ directory.
The dev server is started via the bun dev command, which runs scripts/dev/orchestrate.ts: it performs the one-shot setup steps (dynamic asset sync, image preparation, initial CSS build), then spawns the Tailwind CSS watcher (--watch=always) and the dev server child process (scripts/dev.ts) side by side.
Starting the Dev Server
bun dev
This single command runs the Tailwind watcher and the dev server side-by-side. Open http://localhost:3000 in a browser.
The port is read from PORT in .env (the example .env ships PORT=3000); pass --port to override it. The default source directory is ./src/public.
CLI Options
The dev server accepts these flags when invoked directly:
bun scripts/dev.ts [--port 3000] [--public ./src/public]
| Flag | Alias | Default / Source | Description |
|---|---|---|---|
--port <n> | -p | PORT (env) | Port the server listens on |
--public <dir> | --dir | ./src/public | Source directory with templates and assets |
--help | -h | - | Print usage and exit |
All flags are optional. Running bun dev passes no flags and uses the defaults.
Live Reload
The dev server injects a WebSocket client script into every rendered HTML page. When a source file changes, the server broadcasts a reload message to all connected WebSocket clients, and the browser refreshes automatically.
How It Works
- The dev server runs a WebSocket endpoint at
/__reload - Every rendered page receives an injected
<script>that connects to this WebSocket - The file watcher monitors
src/public/for changes to.ree,.md,.json, and.tsfiles - Changes are debounced at 100ms - rapid edits don't trigger multiple reloads
- When a
.jsonfile changes, translations are hot-reloaded without needing a full restart - the route map and locale names are rebuilt in-memory
What Triggers a Reload
| File Change | Behavior |
|---|---|
.json (translation) | Hot-reloads translations + route map, then triggers browser refresh |
.ree (template) | Triggers browser refresh (template re-read from disk on next request) |
.md (markdown) | Triggers browser refresh |
.ts (data loader) | Triggers browser refresh |
What Triggers a Full Restart (via dev watcher)
| File Change | Behavior |
|---|---|
scripts/dev.ts | Full restart of both the Tailwind watcher and dev server |
.env | Full restart (environment variables are read at startup) |
bunfig.toml | Full restart |
Locale Resolution
The dev server resolves the locale from the URL path following the same rules as the SSG script:
/ → Slovenian (default) at root
/about/ → Slovenian /about/
/en-us/ → English homepage
/en-us/about/ → English /about/
The resolution algorithm:
- Check if the first URL segment matches a configured locale in
config/supported_locales.ts(e.g.en-us) - the segment is compared case-insensitively, so/EN-US/also resolves - If it does, set the active locale to the canonical BCP 47 code and strip the prefix
- If not, use the
default_locale(configured insupported_locales.ts)
The resolved locale + canonical path is then looked up in the route map (which includes localized route_name substitutions) to find the correct template or markdown file.
Built-in Data Variables
Every template rendered by the dev server receives these variables automatically in its data context. You access them as props.xxx in templates:
| Variable | Source | Description |
|---|---|---|
props.locale | URL resolution | Active BCP 47 locale ("en-us", "sl-si") |
props.lang | URL resolution | Same value as props.locale (kept for the shared template engine's variant lookup) |
props.html_lang | Derived | Short language subtag ("sl") for the <html lang="..."> attribute |
props.locale_url_prefix | Derived | "" for default locale, "/sl-si" for others |
props.active_locales | Config | List of locales shown in the locale picker |
props.locale_names | Config | Map of code → display name |
props.locale_self_names | Derived | Map of code → native name from each locale's own translation file |
props.default_locale | Config | Default locale code |
props.base_url | Hard-coded | Always "/" |
props.site_url | Hard-coded | Always "" (set during production SSG) |
props.hreflang_links | Hard-coded | Always [] (set during production SSG) |
props.site_name | Hard-coded | "Dev" |
props.year | Runtime | Current year for copyright |
props.is_dev | Hard-coded | true |
props.rendered_at | Runtime | ISO string of when the render happened |
props.request_url | Resolved | Full URL of the current page, including the locale prefix |
props.canonical_path | Resolved | Canonical path (without locale prefix) |
props.locale_urls | Derived | Map of locale → URL prefix ("" for default, "/{locale}" for others) |
props.helpers | create_template_helpers() | Object of template helper functions |
Template Resolution
The dev server resolves templates in this order:
- Hash map lookup - checks the pre-built canonical → template map
- Reverse route map - if the path is a localized route, resolves back to canonical
- Direct file check - tries
{path}.ree,{path}.md - Index file check - tries
{path}/index.ree,{path}/index.md - 404 - if nothing matches, returns a
404 Not Foundresponse
Locale-Variant Templates
Templates can have locale-specific variants. The resolution chain:
{name}.{requested_locale}.ree → {name}.{default_locale}.ree → {name}.ree
For example, with locale=de-de and default_locale=sl-si:
- Try
about/index.de-de.ree- not found - Try
about/index.sl-si.ree- found (used) - Try
about/index.ree- fallback
This applies to every template load - pages, layouts, includes, and components.
Static File Serving
The dev server serves static files from two locations:
src/public/- templates and their sibling assets (CSS, images)static/- project-level static assets (compiled CSS, JS, favicon)
Static files are served with Cache-Control: no-cache headers to ensure changes appear immediately during development. The server checks src/public/ first - if a file exists there and isn't a .ree, .md, .json, or .ts file, it's served directly. If not found, it falls back to static/.
Generated SSG Artifacts (sitemap & feeds)
sitemap.xml and the RSS/JSON feeds (feed.xml, feed.json) are SSG artifacts - they're emitted to dist/ by bun sitemap / bun rss, not served from src/public/. To avoid 404s on links like /sitemap.xml during development, the dev server serves the last-built copy straight from dist/ as a convenience:
| Request path | Served from |
|---|---|
/sitemap.xml | dist/sitemap.xml |
*/feed.xml, */feed.json | the matching file under dist/ |
These copies are stale until you regenerate them - run bun ssg (or bun sitemap / bun rss) to refresh dist/. If the artifact hasn't been generated yet, the dev server returns a 404 with a hint telling you which command produces it.
robots.txt is intentionally not served from dist/: in dev it's served from the source src/public/robots.txt (which contains Disallow: /) so the dev server stays unindexable.
Error Handling
- 500 errors - Template compilation errors (syntax errors, missing includes, unclosed blocks) return a
500 Errorpage with the error message details - 404 errors - Unknown paths return a simple
404 Not Foundpage - Missing source directory - If
--publicpoints to a non-existent directory, the server exits with an error immediately