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:

  1. bun prepare:images - image optimization and asset preparation
  2. bun css:watch - the Tailwind CSS watcher, recompiling on every file change
  3. bun 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]
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 language 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 dev watcher)

File ChangeBehavior
scripts/dev.tsFull restart of both the Tailwind watcher and dev server
.envFull restart (environment variables are read at startup)
bunfig.tomlFull 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:

  1. Check if the first URL segment matches a language code in config/supported_languages.ts
  2. If it does, set the active language to that code and strip the prefix
  3. If not, use the default_language (configured in supported_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:

VariableSourceDescription
props.langURL resolutionActive language code ("en", "sl")
props.lang_url_prefixDerived"" for default language, "/sl" for others
props.localelanguage_localesBCP-47 locale string ("en-US", "sl-SI")
props.active_languagesConfigList of languages shown in the language switcher
props.language_namesConfigMap of code → display name
props.language_self_namesDerivedMap of code → native name from each language's own translation file
props.default_languageConfigDefault language 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 language prefix
props.canonical_pathResolvedCanonical path (without language prefix)
props.language_urlsDerivedMap of code → URL prefix ("" for default, "/{lang}" 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

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:

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

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 pathServed from
/sitemap.xmldist/sitemap.xml
*/feed.xml, */feed.jsonthe 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 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