Dev Server
Introduction
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 uses conc (concurrently) to run three processes in parallel:
bun prepare:images- image optimization and asset preparationbun css:watch- the Tailwind CSS watcher, recompiling on every file changebun development(scripts/dev.ts) - handles HTTP requests with live reload
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 language 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 |
Language Resolution
The dev server resolves language from the URL path following the same rules as the SSG script:
/ → Slovenian (default) at root
/about/ → Slovenian /about/
/en/ → English homepage
/en/about/ → English /about/
The resolution algorithm:
- Check if the first URL segment matches a language code in
config/supported_languages.ts - If it does, set the active language to that code and strip the prefix
- If not, use the
default_language(configured insupported_languages.ts)
The resolved language + 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.lang | URL resolution | Active language code ("en", "sl") |
props.lang_url_prefix | Derived | "" for default language, "/sl" for others |
props.locale | language_locales | BCP-47 locale string ("en-US", "sl-SI") |
props.active_languages | Config | List of languages shown in the language switcher |
props.language_names | Config | Map of code → display name |
props.language_self_names | Derived | Map of code → native name from each language's own translation file |
props.default_language | Config | Default language 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 language prefix |
props.canonical_path | Resolved | Canonical path (without language prefix) |
props.language_urls | Derived | Map of code → URL prefix ("" for default, "/{lang}" 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
Language-Variant Templates
Templates can have language-specific variants. The resolution chain:
{name}.{requested_lang}.ree → {name}.{default_language}.ree → {name}.ree
For example, with lang=de and default_language=sl:
- Try
about/index.de.ree- not found - Try
about/index.sl.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