Personal site shell. Astro + React islands, deployed on Cloudflare Pages with media on Cloudflare R2.
The site is governed by Agent Design Contracts (ADCs) under contracts/. The
contracts are the source of truth; this README documents the operating
procedures that the contracts assume exist.
- Framework: Astro with React islands. JavaScript by default; TypeScript is permitted but not required.
- Hosting: Cloudflare Pages (static).
- Media: Cloudflare R2 via the helper at
src/lib/r2.js. Media is referenced by R2 key, never by hard-coded bucket URL. - Domain:
milodowling.com(target) andmilodowling.github.io(fallback).
npm install
npm run dev # local Astro dev server
npm run build # produce ./dist/
npm run preview # serve ./dist/ locallyThe dev server stages the preserved /dnd-tabletop/ tool from
_preserved-dnd-content/ into public/ automatically (see the
dndTabletopPassthrough integration in astro.config.mjs). Do not edit
files under _preserved-dnd-content/ or public/dnd-tabletop/ — the
preserved tool is read-only and managed by the build.
This is the documented two-file-touch procedure that the shell modularity
constraint (<shell-constraint-modularity-02> in
contracts/ADC_001_SHELL_OVERVIEW.md) is built around. Adding a section
must require only the two touches below. If you find yourself editing
shell layouts, the nav component, or routing internals, stop — the
modularity constraint has regressed.
-
Create the section's page directory and at least one page:
mkdir -p src/pages/<slug> cat > src/pages/<slug>/index.astro <<'EOF' --- import ShellLayout from "../../layouts/ShellLayout.astro"; --- <ShellLayout title="<Label> — milodowling.com"> <h1><Label></h1> </ShellLayout> EOF
-
Add one entry to
src/sections.config.js:{ slug: "<slug>", label: "<Label>", enabled: true },
That's the entire procedure. The nav reads the manifest and renders the new
section. Astro's filesystem routing serves /<slug>/ from the page file.
| Field | Type | Meaning |
|---|---|---|
slug |
string | Lowercase, URL-safe. Must match the directory name. |
label |
string | Free-form. Shown in navigation. |
enabled |
boolean | false hides the section from nav (route stays reachable). |
The shell's nav code reads only these three fields. Sections may carry additional metadata on their manifest entry, but that metadata is invisible to the shell.
The homepage exposes a single React-island mount point at
src/components/EngineSlot.jsx with the locked interface
{time, pointer, params} (see <home-impl-engine-interface-03> in
contracts/ADC_003_HOMEPAGE_AND_ENGINE_SLOT.md). The eventual WebGL+JS
video engine swaps this one file. Shell code does not change.
The v1 placeholder shader (lands in Phase 2) is deliberately ugly and non-suggestive — it proves the wiring works without prejudicing the engine's eventual aesthetic.
The R2 helper at src/lib/r2.js resolves R2 object keys to public URLs:
import { r2 } from "../lib/r2.js";
const url = r2("portfolio/2026/hero.jpg");
// → "https://media.milodowling.com/portfolio/2026/hero.jpg"The base URL comes from the R2_PUBLIC_BASE env var (see .env.example)
and is baked into the static build.
In portfolio markdown frontmatter, use the r2: prefix:
thumb: "r2:portfolio/2026/thumb.jpg"
media:
- "r2:portfolio/2026/image-1.jpg"The portfolio entry/index pages resolve r2:-prefixed values via the helper.
The virtual tabletop lives at public/dnd-tabletop/ and serves standalone
at /dnd-tabletop/ — the URL shared with friends before the rebuild, kept
serving deliberately (contracts/ADC_006_DND_TABLETOP.md). The 2026-07-10
D&D ADC pass absorbed the previously frozen preserved copy as first-party
source and added shared tables (live multiplayer), undo/redo, a grid
overlay, session export/import, and icon naming.
Multiplayer rooms are served by the dnd-sync Worker
(workers/dnd-sync/, one Durable Object per room):
npm run sync:dev— local room server on :8787 (the tool auto-targets it when served from localhost; e2e tests boot it automatically).npm run sync:deploy— deploy to the personal Cloudflare account. After the first deploy, stamp the Worker URL into theSYNC_SERVERconstant inpublic/dnd-tabletop/index.html.
contracts/ ADC contracts (source of truth)
adc_files/ ADC workflow artifacts (audits, refinements, impl)
docs/ Original design-session inputs
workers/dnd-sync/ Room server for the D&D tabletop (Worker + DO)
src/
layouts/ ShellLayout.astro
components/ Nav.astro, EngineSlot.jsx (Phase 2)
pages/ Filesystem-routed pages
lib/ r2.js, feed.js (Phase 3)
styles/ baseline.css
sections.config.js The section manifest
public/ Static assets (incl. the D&D tabletop tool)
Cloudflare Pages is configured to:
- Build command:
npm run build - Build output directory:
dist - Node version: 20+ (set via the Pages dashboard environment).
- Environment variables:
R2_PUBLIC_BASE(see.env.example).
Pages deploys on push to main. The feat/redesign branch deploys as a
preview environment.
The shell is governed by five contracts under contracts/:
ADC_001_SHELL_OVERVIEW.md— stack, cross-cutting constraints, federation.ADC_002_SECTION_MANIFEST_AND_ROUTING.md— manifest + filesystem routing.ADC_003_HOMEPAGE_AND_ENGINE_SLOT.md— hero + engine slot interface.ADC_004_PORTFOLIO_SECTION.md— portfolio content model + R2 helper.ADC_005_DEFERRED_SECTION_STUBS.md— synth/D&D/tools stubs.
Implementation phase reports live under adc_files/implementation/.