Installation

ReeWeb has one hard prerequisite: Bun. Bun is the runtime, package manager, bundler, and test runner. bun create fetches the public starter repository and the install task fetches the other tooling it needs (Git is only required if you prefer to clone the starter yourself).

Installing Bun

macOS / Linux / WSL
curl -fsSL https://bun.sh/install | bash
Windows
powershell -c "irm bun.sh/install.ps1|iex"

Verify the install:

bun --version

The output should be a version number. If command fails, the installer's directory isn't on your PATH.

Starting a Project

The recommended way to start is bun create, which fetches the starter as a tarball (so the template's Git history never arrives) and initializes a fresh repository with a single initial commit. Cloning the repository works the same way - the installer re-initializes Git for your site either way.

bun create reepolee/reeweb __PROJECT_DIR__
cd __PROJECT_DIR__
bun dev

Prefer git clone if you want the repository cloned rather than fetched as a tarball:

git clone https://github.com/reepolee/reeweb.git __PROJECT_DIR__
cd __PROJECT_DIR__
bun install
bun dev

bun reeweb:install runs automatically: it is a postinstall step, so bun create fires it during the fetch and your first bun install on a cloned checkout fires it too - there is no separate command to run. It fetches ReeWeb's tooling (the reettier and reesql formatters, libvips, and the vendor/ browser assets), creates .env when needed, installs Bun dependencies, then replaces the starter Git repository with one for your project and makes its initial commit. It does not build CSS or prepare images; bun dev and bun ssg do that when they run.

The install task pulls Tailwind CSS v4, tailwind-merge, TypeScript, Bun's type definitions (@types/bun), and @cloudflare/workers-types as dev dependencies. None of them ship to the browser: ReeWeb has zero runtime dependencies. When the public starter has changes you want, use the Upgrade Guide to compare it with your site and choose the files to adopt.

Next, open .env and set SITE_URL to your real domain. The installer copies .env.example, which ships SITE_URL=https://example.com, and that value is baked into sitemap.xml, robots.txt, the feeds, and every canonical and hreflang link the first time you build.

Generating the Static Site

bun ssg

bun ssg is the whole production build, not just the HTML render. It runs, in order:

  1. dynamic:sync, which pulls dynamic assets when REEPOLEE_API_URL is set
  2. prepare:images, which generates responsive images from assets/images/
  3. css:build, which compiles and minifies Tailwind CSS
  4. scripts/ssg.ts, the static site generator itself
  5. og:images, the Open Graph card for every page (see libvips)
  6. sitemap, then rss, then ssg:search

The generator step (scripts/ssg.ts) is the one that:

  • Renders all .ree templates and .md files to static HTML
  • Copies static assets (CSS, JS, images, fonts) to the output directory
  • Generates localized routes for every configured locale
  • Emits redirects for paths declared in config/redirects.ts

Because bun ssg already covers the sitemap, feeds, and search index, you do not need to run those separately after it. The individual scripts in Other Commands exist for re-running a single stage.

The output goes to dist/. Serve it with any static file server:

bun preview

This starts a lightweight HTTP server on the PORT from your .env (3000 by default) serving the generated files. Stop bun dev first: both servers bind the same port, and unlike bun dev, bun preview does not reclaim it. It exits with EADDRINUSE instead.

Development Mode

The bun dev command above runs scripts/dev/orchestrate.ts, which performs the one-shot setup steps (asset sync, image preparation, initial CSS build), then runs the Tailwind CSS watcher (--watch=always) and the development server (scripts/dev.ts) side by side. Changes to templates, markdown, translations, or CSS are detected automatically and the browser reloads. No manual refresh needed.

The CSS watcher recompiles src/css/style.css to src/public/css/style.min.css on every change.

Other Commands

PurposeCommand
SSG with verbose loggingbun scripts/ssg.ts --public ./src/public --dist ./dist --verbose
SSG with hreflang linksbun scripts/ssg.ts --public ./src/public --dist ./dist --site-url https://example.com
CSS once (minified)bun css:build
CSS watchbun css:watch
Generate sitemapbun sitemap
Generate RSS feedbun rss
Generate search indexbun ssg:search
Generate Open Graph cardsbun og:images
Format sourcebun format (uses reettier)
Re-fetch tooling and vendor assetsbun get:pre

Verifying the Setup

Stop bun dev if it is still running, then:

cd __PROJECT_DIR__
bun ssg
bun preview

If you see "✅ Static Site Generation complete" in the terminal and the preview loads at http://localhost:3000, everything is wired up correctly.

A note about libvips for Open Graph images

bun ssg generates a PNG Open Graph card for every page as part of its pipeline (the og:images step, which runs after the generator finishes and before the sitemap and feeds), and that step shells out to the native libvips library.

bun reeweb:install already installs it for you, by way of bun get:pre, so a project set up as described above needs nothing extra. To install or repair it on its own:

bun get:vips

This supports Windows (prebuilt from GitHub releases), macOS (Homebrew), and Linux (apt/dnf/pacman), and adds vips to your PATH automatically. Prefer bun get:vips over calling bun scripts/cli.ts vips directly: the script pins the version ReeWeb is tested against, and the Windows installer needs a real version number to pick a release asset. Without libvips, bun ssg fails on the og:images step; bun dev does not need it, since it doesn't generate Open Graph images.

Every rendered page needs a non-empty <title>. The og:images step reads it to build that page's card and fails the entire bun ssg run - not just that one card - if a page has no <title> tag, or an empty one (<title></title>). This is intentional: a page with no title is incomplete regardless of Open Graph, so the build treats it as a hard error rather than silently shipping a titleless page. layout.ree already sets one from props.title / props.site_name; a template that renders without going through a layout is the case to watch.

Editor Setup

For VSCode, .vscode/settings.json in this project maps .ree files to a custom language. For other editors, treating .ree as HTML gets you most of the way - the tag syntax ({= }, {#if}, {#each}) is distinct enough that the HTML highlighter ignores it cleanly.