Forge Sites Playbook¶
How to stand up a new page end-to-end on Justin's infrastructure. No build step, no node_modules, no SSG.
Philosophy¶
- Pages are plain HTML. Tokens + shared header/footer come from
sites/_shared/. - Single-file pages (games, interactive landings) stay fully self-contained, they just pull
tokens.css+base.css. - Multi-page / content-heavy sites use
<div data-forge-include="…">to embed_shared/header.htmland_shared/footer.htmlat runtime. One 40-line loader, no build step. - Brand overrides live in a per-site
brand-tokens.cssthat redeclares:rootvalues.
Directory map¶
sites/
├── _shared/ Tokens, base, header/footer, include loader
│ ├── tokens.css CSS variables (colors, fonts, spacing, motion)
│ ├── base.css Reset + .container/.card/.btn/.eyebrow/.forge-hdr/.forge-ftr
│ ├── header.html Shared header (slots: brand, nav)
│ ├── footer.html Shared footer (slots: tagline, links)
│ ├── forge-include.js Fetch-and-inline loader with slot overrides
│ └── README.md
├── _templates/
│ ├── single-page.html PressYourLuck-style self-contained starter
│ └── content-page.html AustinGuide-style starter w/ shared header+footer
├── justinsforge.com/ Existing site
├── justinkrystal.com/ Existing site (has its own forge_sites_deploy.sh; untouched)
└── gizmo/ Existing site
Commands¶
All scripts live in scripts/sites/.
| Script | What it does |
|---|---|
forge_sites_new_page.sh <site> <slug> |
Copy a template into sites/<site>/[landing/]<slug>/index.html, substituting title/desc/eyebrow/OG macros |
forge_sites_preview.sh <site> |
Pick a free port (8090–8097), build a temp tree that unions the site with _shared+_templates, serve via python -m http.server. Surfaces Tailscale URL for Venus |
forge_sites_screenshot.sh <url> |
Desktop (1440×900) + mobile (393×852) Playwright screenshots → logs/site-screenshots/ |
forge_sites_deploy.sh <site> |
If the site has its own forge_sites_deploy.sh, delegate. Otherwise tar the site + _shared/ + _templates/, push to CT 102 (media-server) under /mnt/storage/appdata/<target>/landing via Finn |
Spinning up a new page¶
# 1. Scaffold
scripts/forge_sites_new_page.sh justinsforge.com my-new-page \
--template content \
--title "My New Page" \
--desc "What this page is about." \
--eyebrow "PROJECT"
# 2. Drop assets in sites/justinsforge.com/my-new-page/assets/
scripts/forge_assets_run.sh search "austin skyline sunset" --engine google --count 5 \
--out sites/justinsforge.com/my-new-page/assets/_raw
# 3. Optimize (produces webp + avif at responsive widths)
scripts/forge_assets_run.sh optimize sites/justinsforge.com/my-new-page/assets/_raw \
--out sites/justinsforge.com/my-new-page/assets \
--widths 640 1280 1920
# 4. Preview
scripts/forge_sites_preview.sh justinsforge.com
# → open http://100.97.43.104:<port>/my-new-page/ on Venus
# 5. Screenshot for handoff / diff
scripts/forge_sites_screenshot.sh http://127.0.0.1:<port>/my-new-page/ --label my-new-page
# 6. Deploy
scripts/forge_sites_deploy.sh justinsforge.com
When to pick which template¶
| You want | Use |
|---|---|
| A game / interactive experience with bespoke layout | single-page.html, everything inline, no shared includes |
| A content page / landing with standard header + footer | content-page.html, pulls in _shared/header.html + _shared/footer.html |
Adding a brand override¶
<link rel="stylesheet" href="/_shared/tokens.css">
<link rel="stylesheet" href="/brand-tokens.css"> <!-- your overrides -->
<link rel="stylesheet" href="/_shared/base.css">
/* sites/mybrand/brand-tokens.css */
:root {
--accent: #ff2d95;
--accent-bright: #ff4fa8;
--font-display: 'Bebas Neue', sans-serif;
}
How forge-include works¶
- Page has
<div data-forge-include="/_shared/header.html">…</div>. - On
DOMContentLoaded, the loader fetches the target, captures any<template data-forge-slot="name">blocks inside the include point, and uses them to fill<!-- forge:slot:name -->…<!-- /forge:slot:name -->blocks in the fetched HTML. - The loader replaces the include
<div>with the rendered result.
This gives per-page overrides without a build step. Works in any static host (nginx, CT 102, python http.server, Cloudflare Pages).
Deploy targets¶
- justinkrystal.com, has its own
forge_sites_deploy.shthat packslanding/+hub-api/into CT 102 with a docker compose stack. Forgescripts/forge_sites_deploy.shdelegates. - justinsforge.com, served by nginx on UDev itself (port 8100), proxied by Cloudflare tunnel. Deploy = edit in place; nginx picks it up. No remote sync needed.
- new sites, the generic deploy flow tars the site tree to CT 102 under
/mnt/storage/appdata/<target>/landing. Add a Cloudflare tunnel ingress entry for the new hostname on CT 102.
Open decisions¶
- SSG, decided no. Raw HTML +
forge-include.js+tokens.csscovers the need without the node_modules tax. - Cache invalidation, not automated. Cloudflare cache is short (default) and static HTML changes propagate within seconds; if we ever need a purge, add a
--purgeflag that hitsPOST /zones/:id/purge_cachewith a scoped API token. - Screenshot diff, not wired yet.
forge_sites_screenshot.shjust writes; a futureforge_sites_screenshot.sh --compare baseline.pngcould diff with Pillow'sImageChopsand fail the deploy on > threshold delta.