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:
- Bundles client-side scripts via
Bun.build()(currently only trips — see below). - Fetches content from
content/projects/andcontent/trips/— each subdirectory is one page. - Builds a list of
Renderableobjects — one per page plus the global stylesheet. - Writes everything to disk via
src/utils/build.ts, which also copies the entirecontent/directory tohtml-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:
- Write a TypeScript file (e.g.
src/trips/script.ts) with/// <reference lib="dom" />at the top. - Add a
Bun.build()call insrc/main.tsthat compiles it intohtml-output/. - Pass the output URL(s) to the relevant page factory (
TripPages,ProjectPages, etc.) via itsoptionsargument. - 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.