Skip to content

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.html and _shared/footer.html at runtime. One 40-line loader, no build step.
  • Brand overrides live in a per-site brand-tokens.css that redeclares :root values.

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

  1. Page has <div data-forge-include="/_shared/header.html">…</div>.
  2. 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.
  3. 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.sh that packs landing/ + hub-api/ into CT 102 with a docker compose stack. Forge scripts/forge_sites_deploy.sh delegates.
  • 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

  1. SSG, decided no. Raw HTML + forge-include.js + tokens.css covers the need without the node_modules tax.
  2. Cache invalidation, not automated. Cloudflare cache is short (default) and static HTML changes propagate within seconds; if we ever need a purge, add a --purge flag that hits POST /zones/:id/purge_cache with a scoped API token.
  3. Screenshot diff, not wired yet. forge_sites_screenshot.sh just writes; a future forge_sites_screenshot.sh --compare baseline.png could diff with Pillow's ImageChops and fail the deploy on > threshold delta.