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]
FlagAliasDefault / SourceDescription
--port <n>-pPORT (env)Port the server listens on
--public <dir>--dir./src/publicSource 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

  1. The dev server runs a WebSocket endpoint at /__reload
  2. Every rendered page receives an injected <script> that connects to this WebSocket
  3. The file watcher monitors src/public/ for changes to .ree, .md, .json, and .ts files
  4. Changes are debounced at 100ms - rapid edits don't trigger multiple reloads
  5. When a .json file 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 ChangeBehavior
.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 bun dev's orchestrator)

File ChangeBehavior
.envFull restart of the dev server child process (environment variables are read at startup)
config/Full restart of the dev server child process (config is read via static imports at startup)

Only .env and config/ trigger a full restart, and only the dev server child process is killed and respawned - the Tailwind watcher keeps running throughout. Changes to scripts/dev.ts itself (or any other .ts module in its graph) are picked up in-place by bun --hot, which re-evaluates the module and reloads state without restarting the process.

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:

  1. 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
  2. If it does, set the active locale to the canonical BCP 47 code and strip the prefix
  3. If not, use the default_locale (configured in supported_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:

VariableSourceDescription
props.localeURL resolutionActive BCP 47 locale ("en-us", "sl-si")
props.langURL resolutionSame value as props.locale (kept for the shared template engine's variant lookup)
props.html_langDerivedShort language subtag ("sl") for the <html lang="..."> attribute
props.locale_url_prefixDerived"" for default locale, "/sl-si" for others
props.active_localesConfigList of locales shown in the locale picker
props.locale_namesConfigMap of code → display name
props.locale_self_namesDerivedMap of code → native name from each locale's own translation file
props.default_localeConfigDefault locale code
props.base_urlHard-codedAlways "/"
props.site_urlHard-codedAlways "" (set during production SSG)
props.hreflang_linksHard-codedAlways [] (set during production SSG)
props.site_nameHard-coded"Dev"
props.yearRuntimeCurrent year for copyright
props.is_devHard-codedtrue
props.rendered_atRuntimeISO string of when the render happened
props.request_urlResolvedFull URL of the current page, including the locale prefix
props.canonical_pathResolvedCanonical path (without locale prefix)
props.locale_urlsDerivedMap of locale → URL prefix ("" for default, "/{locale}" for others)
props.helperscreate_template_helpers()Object of template helper functions

Template Resolution

The dev server resolves templates in this order:

  1. Hash map lookup - checks the pre-built canonical → template map
  2. Reverse route map - if the path is a localized route, resolves back to canonical
  3. Direct file check - tries {path}.ree, {path}.md
  4. Index file check - tries {path}/index.ree, {path}/index.md
  5. 404 - if nothing matches, returns a 404 Not Found response

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:

  1. Try about/index.de-de.ree - not found
  2. Try about/index.sl-si.ree - found (used)
  3. 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:

  1. src/public/ - templates and their sibling assets (CSS, images)
  2. 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 & search index)

sitemap.xml, the RSS/JSON feeds (feed.xml, feed.json), and the search index (search-index.json) are SSG artifacts - they're emitted to dist/ by bun sitemap / bun rss / bun ssg:search, 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 pathServed from
/sitemap.xmldist/sitemap.xml
*/feed.xml, */feed.jsonthe matching file under dist/
*/search-index.jsonthe matching file under dist/

These copies are stale until you regenerate them - run bun ssg (or bun sitemap / bun rss / bun ssg:search) 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 Error page with the error message details
  • 404 errors - Unknown paths return a simple 404 Not Found page
  • Missing source directory - If --public points to a non-existent directory, the server exits with an error immediately