garrepi.dev

August 1, 2026 · View on GitHub

Personal site. Static HTML generated by Bun and TypeScript — no framework.

Build

bun run build   # typecheck, then generate html-output/
bun run dev     # watch + local server at http://localhost:3000
bun run serve   # serve html-output/ without watching

Output lands in html-output/. That directory is what gets deployed.

How it works

src/main.ts is the entry point. It:

  1. Bundles client-side scripts via Bun.build() (currently only trips — see below).
  2. Fetches content from content/projects/ and content/trips/ — each subdirectory is one page.
  3. Builds a list of Renderable objects — one per page plus the global stylesheet.
  4. Writes everything to disk via src/utils/build.ts, which also copies the entire content/ directory to html-output/content/ so static assets (images, GPX files, etc.) are served alongside the HTML.

Content structure

Each project or trip lives in its own directory under content/:

content/
  trips/
    my-trip/
      info.json      # title, short description
      readme.md      # page body (markdown)
      res/
        gpx/         # GPX track files
        images/      # photos

info.json is validated with Zod against the ContentInfo schema in src/content/fetch-content.ts.

Trip pages and GPX maps

Trip markdown supports a custom <GPX> tag:

# Day 1

<GPX src="res/gpx/day1.gpx" />

Paths are relative to the trip's own content directory. Absolute paths (starting with /) pass through unchanged.

At build time, src/trips/trip-pages.ts uses a per-trip marked renderer to convert <GPX src="..."> into:

<div class="gpx-map" data-src="/content/trips/my-trip/res/gpx/day1.gpx"></div>

At runtime, src/trips/script.ts finds every .gpx-map div and initializes a Leaflet map in it, loading the track from data-src. This script is compiled by Bun.build() into html-output/trips/script.js (with Leaflet bundled from node_modules — no CDN) and injected into every trip page.

Adding a client-side script

The trips script pipeline is the established pattern:

  1. Write a TypeScript file (e.g. src/trips/script.ts) with /// <reference lib="dom" /> at the top.
  2. Add a Bun.build() call in src/main.ts that compiles it into html-output/.
  3. Pass the output URL(s) to the relevant page factory (TripPages, ProjectPages, etc.) via its options argument.
  4. The page factory threads them into htmlPage({ scripts: [...], styleLinks: [...] }).

Component model

Pages are plain TypeScript functions that return HTML strings. The shape is:

Renderable = { path: string; render: () => string }

src/components/html-page.ts — full HTML document shell, accepts optional scripts and styleLinks arrays.
src/components/head.ts<head> block, accepts optional extraLinks for additional stylesheets.
src/components/navbar.ts — top navigation bar.