Personal site, plus a dashboard that edits it.
| Host | Serves |
|---|---|
odiwr.com |
the landing page, work write-ups at /[slug], /dashboard |
projects.odiwr.com |
the projects listing |
creative.odiwr.com |
the creative mosaic |
wishlist.odiwr.com |
the wishlist โ unlisted: not linked, not in a sitemap, noindex |
cinema.odiwr.com |
clips from films and shows; each post at /<slug> on that host |
Every subdomain is this same app: src/proxy.ts rewrites their root onto
/projects, /creative, /wishlist and /cinema (the cinema's posts too, not
just its root), and gives each host its own
robots.txt and sitemap.xml. DNS has to point them here.
Everything editable lives in one JSON document in R2 (site/content.json),
read through src/lib/content.ts. There is no database. The dashboard writes it;
the site reads it through a short cache that a save clears.
Until a section has real entries it shows filler from src/lib/filler.ts, which
disappears on its own once the first entry is published.
/dashboard, behind Google sign-in restricted to ADMIN_EMAILS. Analytics reads
GA4 through a service account; Work, Wishlist, Cinema, Blog and Media edit the
document and the bucket. Cinema takes any number of clips at once: each becomes a
post, with a still cut from it in the browser for its tile.
Copy into .env.local โ it is gitignored, and none of it belongs in the repo.
NEXT_PUBLIC_SITE_URL # optional; defaults to https://odiwr.com
SESSION_SECRET # signs the dashboard cookie (PAYLOAD_SECRET also accepted)
ADMIN_EMAILS # comma separated allow-list
GOOGLE_CLIENT_ID # OAuth client for the dashboard sign-in
GOOGLE_CLIENT_SECRET
GOOGLE_SITE_VERIFICATION # optional; Search Console meta tag
NEXT_PUBLIC_GA_MEASUREMENT_ID # GA4 web stream "G-โฆ" id; without it no visits are recorded
GA_PROPERTY_ID # GA4 numeric property id
GOOGLE_SERVICE_ACCOUNT_EMAIL # read-only access to that property
GOOGLE_SERVICE_ACCOUNT_KEY
R2_ENDPOINT # the content document and all uploads
R2_BUCKET
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
R2_PUBLIC_URL # public origin, e.g. https://media.odiwr.com
The OAuth client needs <origin>/dashboard/auth/callback registered as a
redirect URI, for production and for localhost.
Each writes a module that is committed; re-run after changing its inputs.
| Command | Writes |
|---|---|
npm run icons |
every favicon size from public/brand/catguy.svg |
npm run icons:data |
inline SVG for the icons found in src/ |
npm run stacks |
the searchable tech-stack index |
npm run fonts:subset |
the CJK and @ font subsets, and their unicode-ranges |
npm run ffmpeg:worker |
public/ffmpeg/, ffmpeg.wasm's worker served unbundled for the clip cutter |
npm run r2:plan prints a reorganisation plan; npm run r2:apply performs it.
Moves are copy-then-delete and cannot be undone, and assets are shared with the
old site โ read the header of scripts/reorganize-r2.mjs first.