Demo site factory
Build a fictional showcase site for an industry vertical — spec, brand, Astro/Tailwind build on image placeholders, generated photography, then publish.
- updated
- 2026-08-16
- files
- 17
- size
- 142.4 kB
- bundle
- 064ff9ed8d2b
- tags
- astro · web · pipeline
[1] install
Paste this into any agent with network and file access. It installs to~/.agents/skills/demo-site-factory/ and verifies every file against the manifest.
Install the agent skill "demo-site-factory".
1. Fetch https://share.soft.pw/skills/demo-site-factory/manifest.json
2. Pick the install root: ~/.agents/skills if ~/.agents exists or can be created, otherwise ~/.claude/skills.
3. Build the bundle in a temp directory there (".tmp-demo-site-factory"): create it, then download every entry in "files" to its "path" using the given "url".
4. chmod +x any file whose "mode" is 0755.
5. Verify every downloaded file against its sha256. On the first mismatch, delete the temp directory and report the failing path — do not continue.
6. Move the temp directory to <install root>/demo-site-factory/, replacing any existing copy.
7. Read demo-site-factory/SKILL.md, then reply in one line: when you will use it and what you need from me. Do not run the skill yet.[2] when it triggers
- >"a demo site for restaurants / clinics / gyms / apartments"
- >Selling web-design services to businesses in one vertical and needing the proof
- >Adding another site to an existing family of demo sites
- >Continue, extend, or regenerate the images of a site built this way
[3] before you install
- node 22+ and npm/pnpm
- python3 (shape, typeset, and OG asset helpers)
- An image generation route — the pipeline builds on placeholders first
[4] what it does
What it does
A six-phase, spec-first pipeline that turns “a demo site for gyms” into a finished, publishable site. The order matters: the content model and brand are decided before any HTML exists, and the build runs entirely on image placeholders that carry their own generation briefs — so layout and copy are settled before a single image is generated.
How it runs
- Idea → documents — requirements and a content model, including the language-coverage decision.
- Brand — palette, type, and voice for the fictional business.
- Build on placeholders — Astro + Tailwind v4 site where every image slot is a brief, not a file.
- Generate images — the placeholder manifest becomes the image-generation hand-off.
- Swap and draw share cards — real assets go in, OG cards are rendered per page.
- Publish — GitHub push and deploy, only when
deployis passed.
What you get
A real static site with a documented content model, an image manifest you can regenerate from,
share cards, and a deploy path. Bundled assets include Placeholder.astro, the manifest and OG
scripts, and prune-unreferenced-assets.mjs. Scale is a parameter of the brief, not of the skill —
a five-page site stops after the pages it needs.
[5] bundle
| path | bytes | mode | sha256 |
|---|---|---|---|
| SKILL.md | 31931 | 0644 | e59acd6365 |
| assets/Placeholder.astro | 2596 | 0644 | 0b580a559d |
| assets/SocialIcons.astro | 3593 | 0644 | dd007b500a |
| assets/build-og-images.mjs | 2633 | 0644 | 6fb30ff232 |
| assets/build_og.py | 7548 | 0644 | ea48fd0bfd |
| assets/check-og-images.mjs | 3637 | 0644 | 7b1bac3f2e |
| assets/collect-image-manifest.mjs | 4480 | 0644 | 61b15a70d7 |
| assets/og-cards.mjs | 2674 | 0644 | 84188d63c7 |
| assets/prune-unreferenced-assets.mjs | 1598 | 0644 | 48fae0bb11 |
| assets/shape.py | 3822 | 0644 | d6cb41366b |
| assets/typeset.py | 5878 | 0644 | e4ef3dadcf |
| references/astro-stack.md | 19243 | 0644 | bd658e542f |
| references/footer-contract.md | 7365 | 0644 | 841787b222 |
| references/image-generation.md | 22282 | 0644 | d9bdb1a5f5 |
| references/placeholder-protocol.md | 5182 | 0644 | 4b7aef20b3 |
| references/positioning.md | 11358 | 0644 | 13c3935ccb |
| references/share-cards.md | 10040 | 0644 | 73154c37cf |
[6] read SKILL.md verbatim
---
name: demo-site-factory
description: >-
Build a fictional showcase website for a specific industry vertical — used to sell
web-design services to businesses in that vertical. A six-phase, spec-first pipeline:
expand a rough idea into requirements + a content model, design the brand, build the
Astro/Tailwind-v4 site entirely on image PLACEHOLDERS that carry generation briefs,
generate the images from the resulting manifest, swap them in, publish. Reach for this
whenever the user wants a demo/sample/portfolio site for a category of client ("a demo
site for restaurants / clinics / gyms / apartments"), or wants to add a new site to an
existing family of demo sites. Also use when they ask to continue, extend, or regenerate
images for a site built this way. Two optional arguments: `auto` runs all six phases
without stopping; `deploy` publishes the site live (off by default).
---
# Demo Site Factory
A repeatable pipeline for producing fictional demo websites, one per industry vertical,
that function as sales assets. Each site must be credible enough to feel real and
distinct enough that seeing two of them side by side proves range rather than a template.
## The through-line
> **The demo's job is not to be pretty. It is to prove that we understood this client's
> business better than they expected.**
That is why the content model comes before the layout, why prices and terms are published
rather than hidden, and why the visual register is derived from the vertical rather than
picked from taste. A beautiful site that says nothing specific about the trade is a
failed demo.
**The hardest requirement is contrast between siblings.** Every new site in the family
must be positioned *against* the previous ones, deliberately and in writing. See
[`references/positioning.md`](references/positioning.md) — the contrast table there is
mandatory, not optional colour.
## How to read this skill
**It records process and judgement, not content.** Every concrete number, entity name,
route, palette and phrase in here — and in its reference files — is an **illustration from
a site that already shipped**, never a template to fill in. Past sites appear only to make
a rule legible.
Two failure modes, and the second is the dangerous one:
- Ignoring the process and improvising. Rare, and obvious when it happens.
- **Inheriting the last site's content shape.** The previous demo's entity model, route
list, image subjects and vocabulary are sitting right there, already written and
plausible-looking, and they will quietly become the new site's defaults. A restaurant
built by analogy to an apartment gets unit-shaped thinking with dishes pasted in.
So: derive the entity, the routes, the numbers and the vocabulary from **this** vertical,
in phase 1, before opening any previous site. If a section here reads like it is telling
you *what* to build rather than *how to decide*, treat it as the example it is.
## Invocation arguments
Arguments arrive as free text after the skill name. Two tokens are recognised anywhere in
it, case-insensitively, bare or dashed (`auto`, `--auto`); everything else is the brief.
Echo back which ones you read before phase 1, so a typo surfaces in ten seconds rather
than at the end of a six-phase run.
| Token | Effect | Absent |
| --- | --- | --- |
| `auto` | Run all six phases end to end without stopping at the checkpoints. Every open question is yours to decide. | Stop at each checkpoint and wait for the user. |
| `deploy` | Run phase 6b — push the site live. | **Do not deploy.** Finish at 6a and hand over the verified build. |
They are independent. `auto` alone builds the whole site and stops before it goes live;
`deploy` alone still asks at every checkpoint and publishes at the end. **`auto` never
implies `deploy`** — an unattended run is exactly the one that should not put a hostname
into the world unasked.
### What `auto` changes
- The three phase checkpoints — positioning + routes, palette + logo, the placeholder
review — and the phase-1 language fork become **your** calls. Derive them from the
vertical and the contrast table, and write the reasoning into the document where the
decision lives, not only into the chat.
- The master-image approval in phase 4 is yours too. Still generate the masters first, in
one session, and still cross-check them against the invariants: you are standing in for
the user's eye, not removing the gate. Reject on the checklist, never on taste, and do
not regenerate for a sub-100% face match.
- Prefer the strongest option over the most defensible one. The reason to run unattended
is to get a site with a point of view; a run that picks the safe choice at every fork
produces the template this pipeline exists to avoid.
- **Report a decision log at the end** — each checkpoint, the choice, one line of reason.
Lead with the decisions a human is most likely to want reversed, because they cost
nothing now and a rebuild later.
### What `auto` does not change
Nothing under *Non-negotiables* is optional, and no verification gate is: `astro check`
clean, the 360px overflow sweep, the JS budget counted in built output, footer attribution
on every page, share cards present and not stale, the clean-clone build. `auto` removes
the waiting, not the checking.
Nor does it widen what the pipeline may do to the outside world. Repo creation still
needs an authenticated `gh`, deploying still needs the `deploy` token, and anything
irreversible or outward-facing that the phases do not already cover still gets asked
about — unattended is not the same as unsupervised.
## Non-negotiables
These hold in every phase, for every vertical:
1. **The content model is the single source of truth.** Every figure the customer could
act on lives in one document and one typed data file — whatever those figures are for
this trade. Pages read from it. Never invent a figure inside a page.
2. **The business is explicitly fictional**, and the site says so. No real address, phone,
review, award, certification, or press quote. No named testimonials.
3. **The footer carries the attribution on every page**, with the two names as real
hyperlinks opening in a new tab:
> A demo by **AlphaBlue** → `https://alphablueth.com`
> Creative direction by **Luke** → `https://soft.pw`
The names are the links — do not print the bare URLs alongside them. Both need
`target="_blank" rel="noopener noreferrer"`.
**The same attribution goes on the share card** — see rule 9. A demo is usually met
as a link pasted into a chat, by people who will never scroll to a footer.
4. **The footer carries social icons, and every one of them opens the pitch.** The
accounts do not exist, so an icon must never be a dead link or point at a real
profile. Clicking any icon opens a small dialog:
> This is a demo site made by AlphaBlue.
> Like it? Get yours. → `https://start.alphablueth.com` (new tab)
Offer the user the default set — **Facebook, Instagram, TikTok, YouTube** — and let
them choose per site; add or drop platforms to fit the vertical and the market
— a business this size, in this trade, in this market may run a local messaging app and
would never maintain a video channel. An implausible icon costs more credibility than a
missing one.
**Build the dialog in CSS, not JavaScript.** The footer is on every page, so a
scripted modal puts JS on every route and breaks rule 8. `:target` does it with zero
script — see [`references/footer-contract.md`](references/footer-contract.md).
**Footer changes must not introduce a single foreign value.** Surface, border, radius,
buttons and scrim all come from the site's existing tokens; verify by reading computed
styles against a reference element elsewhere on the site, not by eye.
5. **No demo transaction is presented as real.** Enquiry forms are labelled as demos,
collect no payment details, and never put personal data into a URL.
**Match the flow's fidelity to how central it is.** Where the form *is* the business —
an appointment-only practice with no walk-ins, a booking-led venue — a button that does
nothing guts the demo's whole argument. Build the flow out: selectable times, a summary,
a confirmation state. It still sends nothing, and it still says so.
6. **Placeholders before pixels.** The site is built, reviewed and shippable with zero
image assets. See [`references/placeholder-protocol.md`](references/placeholder-protocol.md).
7. **Mobile-first, verified at 360px** before any desktop polish.
8. **A stated JavaScript budget**, decided in phase 1 and enforced in phase 3. Most pages
ship zero JS.
9. **Every route has a share card**, 1200×630 PNG, carrying the brand mark and the
attribution, generated from that page's real title and description. The demo is shared
as a link far more often than it is browsed, so a link that unfurls as a grey box has
failed before it is opened. See [`references/share-cards.md`](references/share-cards.md).
## Scale the pipeline to the site
This pipeline was calibrated on a six-route, nineteen-image site. A three-route,
five-image site exists too, and running the full machinery on it produces a document set
longer than the website. That is not merely wasteful — an obviously oversized process gets
abandoned, and the parts that did matter get abandoned with it.
**Mandatory at every size**, however small:
- The content model as single source of truth (it can be one file of thirty lines)
- Placeholders with binding ratios, and the manifest
- One master per invariant subject before any image is generated
- The footer contract
- The fictional-business disclaimer, and no real identity
- 360px verification, and the stated JS budget
- Share cards, and the build-time check that they are neither missing nor stale — a
three-route site is *more* likely to be met as a pasted link, not less
**Only worth it above roughly ten images or five routes:**
- A separate `image-design-system.md` — below that, fold the invariants and the review
checklist into `requirements.md`
- Parallel generation batches — under ten images, one or two sequential runs are simpler
and hold consistency better
- A dedicated `AGENTS.md` — a small site can carry its rules in `requirements.md`
When in doubt, keep the *checks* and drop the *paperwork*.
## Phase 1 — Idea → documents
The user brings a rough idea. Expand it, in conversation, into a document set. Do not
start scaffolding until these exist and the user has agreed to them.
| Document | Holds |
| --- | --- |
| `docs/requirements.md` | Purpose, positioning, the contrast table, tech, routes, per-page responsibilities, content/visual/a11y/performance rules, the placeholder protocol, a staged definition of done |
| `docs/content-model.md` | **Every fact and figure.** Entities, prices, terms, facilities, hours, status vocabulary, form field specs, and an approved copy bank + banned-phrase list |
| `docs/brand-identity.md` | Written in phase 2 |
| `docs/image-design-system.md` | Property invariants, photographic treatment, master image set, prompt template, review checklist, crop constraints |
| `AGENTS.md` | **Only what is specific to this site** — positioning, the one rule, banned phrases, entity facts, route list. Point at this skill for everything shared; restating the common rules guarantees the two copies drift and no one knows which is authoritative. |
**Derive the route list and the content shape from the vertical, not from the last site.**
The previous demo's entity model is the most contaminating thing in the repo — see
[`references/positioning.md`](references/positioning.md) §7. Confirm the routes with the
user before scaffolding.
### Decide language coverage here — it is an option, not a default
Ask, in phase 1, and record the answer in `requirements.md`. Under `auto`, answer it
yourself from the market the fictional business sits in — and answer it *here*, because
this is the fork that punishes deferral most.
| Choice | What it costs | When it is right |
| --- | --- | --- |
| **English only** | Nothing now. Reserve the font architecture (a display face with no CJK/Thai coverage needs a named fallback family decided up front). | The demo's job is to show design capability, not to ship a bilingual product. |
| **Bilingual from the start** | Every content-model field doubles, routes gain a locale segment, all UI strings must be extracted, line-height needs a per-script token, the webfont subsets split by script, and the share cards double. Roughly 1.5× the phase-1 and phase-3 work. | The pitch is to a market where a monolingual site would be visibly wrong, or the client will actually run it. |
Retrofitting is materially worse than either — it reopens the content model, the routes
and every hardcoded string at once. So this is a phase-1 fork, never a phase-6 patch. If
the answer is English-only, still name the second-script font pairing in the brand doc so
the option stays cheap.
Two techniques that carry most of the value:
- **Write the banned-phrase list.** Naming the words the vertical must never use
(`luxury`, `elevate`, `curated`, `seamless`…) does more to fix the voice than any
amount of positive description.
- **Publish the unflattering facts.** Whatever this trade normally hides behind "contact
us" — the surcharges, the exclusions, the wait, the thing the business does *not* have.
Transparency is the most transferable thing a demo can demonstrate to a prospective
client, and it is the one move that works in every vertical.
**Checkpoint:** the user confirms positioning, the headline figures, and the route list.
Under `auto`, decide all three yourself and record them in `requirements.md`.
## Phase 2 — Brand
Invoke the **`brand-identity`** skill. Feed it the intent sentence and the contrast
table from phase 1 so it designs *against* the sibling sites too.
Guard rails specific to this pipeline:
- Do not let the anchor colour echo the agency's own brand — the demo must look like the
client's brand, not ours.
- Interrogate the category convention (blue for property, green for wellness, black for
luxury). Distinctiveness comes from contrast with the category, not conformity to it.
- **Verify glyph coverage before committing to a display font.** If the site shows a
currency, a script, or symbols the font lacks, they will silently fall back and look
broken. Read the font's `cmap`, do not assume.
- Output lands in `assets/brand/` with a build script, a raster script, and
`docs/brand-identity.md`. Keep the text-to-outline primitives in a `typeset.py` the logo
script imports — the share-card script in phase 5 needs exactly the same ones, and
redrawing them there is how the two drift apart.
**Checkpoint:** the user picks the palette applied-in-context, then the logo direction.
Under `auto`, still render both in context and still measure contrast before choosing —
the checkpoint moves, the evidence behind it does not.
## Phase 3 — Build on placeholders
Scaffold Astro + pnpm + Tailwind v4 and build every route. Follow
[`references/astro-stack.md`](references/astro-stack.md) — it carries the exact scaffold
plus a list of traps that have each cost real time.
Order that works:
1. Config, `@theme` tokens, self-hosted subset fonts.
2. The typed data file, transcribed from `content-model.md`.
3. Core components: `Placeholder`, the price display, the status pill, header, footer,
layout.
4. Routes, largest first. Verify the build after the first page, not after the last.
5. The manifest collector script.
The base layout's `<head>` carries the full share-card tag set from the start — the cards
themselves come in phase 5, but the tags and the slug rule are layout work, and an
`og:image` that must be absolute is far cheaper to get right now than to retrofit. See
[`references/share-cards.md`](references/share-cards.md).
Then verify, in this order: `astro check` clean → 360px with no horizontal overflow →
interactive logic exercised by script, not by eye → JS budget counted per built page →
footer attribution present.
**Checkpoint:** the user reviews the site on placeholders. Layout changes are cheap now
and expensive after images exist. Under `auto`, review it yourself in a browser at 360px
and at desktop before phase 4 — this is the last cheap moment, so it is the one checkpoint
worth spending real attention on rather than waving through.
## Phase 4 — Generate the images
`pnpm build && pnpm images:manifest` writes `docs/image-asset-manifest.md`: one entry per
placeholder with its binding ratio, final alt text, the routes it appears on, and the
brief.
**This runs here — no separate service.** `codex exec` has a built-in image generation
tool and honours exact aspect ratios, so the binding ratio drives generation directly with
no post-crop. Follow [`references/image-generation.md`](references/image-generation.md):
generate the masters in a single session, get them approved, then fan out in parallel
batches that attach the masters as references with `-i`.
Each generation inherits the invariants and the review checklist from
`image-design-system.md`. Reject on the checklist, not on taste. The most common failure
is that image models default to *aspirational* — every brief must actively pull toward
the register the vertical actually occupies.
**Pulling against gloss without also pinning the era is what produces the next failure**,
and that sentence above is where it starts. A brief that says only *unglamorous,
documentary, do not flatter it* returns a photograph of **twenty years ago** — sodium
light, fluorescent tubes, beige plastic, no digital equipment — because those are the
visual correlates of "not glossy" in the training data. Showing a working business as
shabby is not honesty; it is a different inaccuracy, and it undersells the client the demo
exists to win.
So the brief must also **state the era positively with a reject test**, cap equipment
density rather than stacking devices to prove modernity, **require people rather than
merely permitting them**, and check that the arrangement of the room asserts the same thing
the content model asserts. Four regenerations across three sites went into learning that;
the whole family — dated, over-corrected, emptied of people, or laid out in a way that
contradicts the copy beside it — is in
[`references/image-generation.md`](references/image-generation.md) under **The register
trap**. Read it before writing the first brief, not after the first rejection.
**Images are where a demo stops being a document and starts being a place.** They are the
last thing generated and the first thing seen, and a lifeless or dated set undoes copy that
took the whole pipeline to get right.
## Phase 5 — Swap, then draw the share cards
Replace placeholders one `id` at a time. The ratios were binding, so the layout must not
move. Optimize to WebP through Astro's image pipeline; keep source generations outside
the production asset graph. A half-generated site must never be broken.
Then `pnpm og:build` — cards last, because they are drawn from the built HTML and the
copy has to have stopped moving. Open the PNGs and look at them: the longest title, the
shortest, and one dynamic route. Full protocol in
[`references/share-cards.md`](references/share-cards.md).
## Phase 6 — Publish
Three parts: prove a clean environment builds, push it to GitHub (6a), deploy it (6b).
**6b runs only when the invocation carried the `deploy` token.** Without it the pipeline
finishes at 6a: a verified clean build, in a repo, not live. Say so in one line at the
end and give the exact command that would deploy it — do not deploy "since everything
passed", and do not ask whether to; the absent token already answered.
**6a and 6b are conditional on their tooling too.** Run them only when `gh` / `wrangler`
are installed *and* already authenticated; otherwise skip them without ceremony and hand
over the verified build. Never install or authenticate a CLI on the user's behalf, and
never treat a missing one as a failure — the deliverable is a site that builds clean, and
the host is the user's call.
**Prove a clean environment builds before handing it over.** The working copy has caches,
a venv, downloaded fonts and installed binaries the runner will not have. Clone the repo
somewhere else, install with `--frozen-lockfile`, and build:
```bash
git clone <remote> /tmp/verify && cd /tmp/verify
pnpm install --frozen-lockfile && pnpm build
```
Check in that clean output: derivatives actually generated, committed symlinks restored
and dereferenced, fonts present, no placeholder left in a production route, the footer
attribution on every page, and the share cards present and passing their check.
**Whatever a committed symlink points at must itself be committed** — which means
`.gitignore` may not exclude generated brand rasters. `public/icons/` links into
`assets/brand/png/`, so ignoring that folder as "build output" leaves a clone with dangling
favicon links. Generated-then-committed is the deliberate design here; the check that keeps
it honest is in phase 5, not a reason to stop tracking the files. Settle this in phase 3
rather than discovering it in the clean clone.
The cards are the sharpest test of this, because their toolchain is Python + fontTools +
librsvg and the runner has none of it. Strip the local extras off `PATH` for the clean
build (`PATH=/usr/bin:/bin:$(dirname $(which node)):…`) so a check that quietly shells out
to a locally-installed binary fails here rather than on the runner.
Pin `engines.node` and `packageManager` in `package.json` so the runner does not pick an
incompatible version — a first deploy failing on a Node mismatch is the most common and
most avoidable failure here.
**pnpm blocks dependency build scripts by default, and `pnpm install` exits non-zero when
it does** — so the build never starts. Declare them in `pnpm-workspace.yaml`, not in
`package.json`: pnpm 11 renamed the setting to `allowBuilds` and no longer reads the
`pnpm` field in `package.json` at all. Keep both spellings for older pnpm:
```yaml
allowBuilds: { esbuild: true, sharp: true } # pnpm 11
onlyBuiltDependencies: [esbuild, sharp] # pnpm 10
```
`sharp` is the one that matters: skip its build script and Astro's image pipeline can
degrade to copying originals untouched while still printing Complete.
### 6a — Push it to GitHub
**Check the tool is there and authorised first, and skip both 6a and 6b silently if it is
not.** These steps are a convenience, not part of the deliverable — a site that builds
clean is finished whether or not it has a remote. Never install or authenticate a CLI on
the user's behalf.
```bash
command -v gh >/dev/null && gh auth status >/dev/null 2>&1 || echo "no gh — skipping"
```
If it is available:
- **Read the family's existing naming before inventing one.** `gh repo list` and
`wrangler pages project list` show the convention; match it rather than deriving a name
from the directory. One family in use: repo `ab-demo-<site>`, Pages project the same,
and the sibling projects reveal it in one command.
- **Private by default.** A demo is a sales asset; publish the *site*, not the workshop.
`gh repo create <name> --private --source=. --remote=origin --description "…"`.
- **Scan before the first push.** Not because a demo has secrets, but because the check is
cheap and the mistake is permanent. Grep the tracked set for credential-shaped filenames
and strings, and eyeball the largest tracked files — the image originals are tens of
megabytes and are committed on purpose, but confirm that is what you are seeing.
- **Then prove the remote is deployable, not merely that the push succeeded.** Clone it
back from GitHub into a fresh directory, with `PATH` stripped, and build. This is where
a dangling committed symlink or a `.gitignore` that excluded a generated raster finally
shows up. `git push` reporting success is not evidence that anything works.
### 6b — Deploy
Same gate: `command -v wrangler && wrangler whoami` — skip if either fails. And the
`deploy` token, which is the gate that comes first.
**Set `site` in `astro.config.mjs` to the real production host BEFORE building** — it is
what makes `og:image` absolute and the canonical link correct, and both are wrong in a way
nobody sees locally. Read the family's convention off `wrangler pages project list`: the
sibling sites answer to `<project>.bizthai.net` with `<project>.pages.dev` behind it, so
the custom domain is the host to publish, not the `.pages.dev` one.
Decide it in phase 3 even when you are not deploying, so a run without `deploy` lands one
`wrangler pages deploy` from live rather than one rebuild from it. The cost of a late
change is now only a rebuild: **the share cards no longer carry the host** — their footer
strip is the fixed pitch URL, on every site in the family — so a domain change cannot
invalidate them. That was previously the sharpest trap in this phase, and removing the
coupling was the fix. See [`references/share-cards.md`](references/share-cards.md).
> `check-og-images.mjs` still only compares each card's baked title and description
> against its page. It does check that `og:image` points at the `site` host — but that tag
> is regenerated every build, so it is the pages, not the cards, that a host change moves.
```bash
wrangler pages project create <name> --production-branch main
wrangler pages deploy dist --project-name <name> --branch main
```
Then verify **against the deployed URL**, not the local `dist/`: a page, a share card, a
favicon, a font, and one real image derivative — status *and* content-type on each.
**A custom domain needs a permission wrangler's OAuth token does not have.** Adding the
domain to the Pages project succeeds (`pages:write`), but the DNS record does not get
created, and the token cannot read or write DNS — so the domain sits `pending` forever.
Two traps in that sequence:
- Querying DNS with that token returns an **authentication error**, and an empty result
list from a failed call is indistinguishable from a genuinely empty zone. Print
`success` and `errors` from the API response, or you will report "no record exists" as a
finding when you were simply not allowed to look.
- Adding the domain through the **dashboard** auto-creates the record for a same-account
zone; adding it through the **API** does not. So do not add a custom domain via API and
walk away — either hand the user the exact CNAME, or leave the domain off entirely.
**If the custom domain is dropped, `site` must go back to the `.pages.dev` URL** and the
site rebuilt — the cards themselves are unaffected now, but `og:image` and canonical are
not, and a stale one means every pasted link unfurls against a hostname that will never
resolve.
### Measuring it, once it is live
**The conversion does not happen on the demo.** It happens when a visitor leaves for the
pitch funnel. So the instrumentation that earns its keep is on the *link* and at the
*destination*, not on the site — and that instrumentation is the UTM scheme in
[`references/footer-contract.md`](references/footer-contract.md), which costs no
JavaScript at all.
**Do not add an analytics script to a site whose host already provides one.** Static hosts
increasingly ship a cookieless, consent-free page-view product with the platform; a second
script buys nothing and costs the JS budget. Ask what the host already gives you before
reaching for a tag.
**If a script is added anyway, amend the JS budget in `requirements.md` in the same
change.** The budget is a non-negotiable and states most pages ship zero JavaScript.
Slipping a tag past it silently means the next person counting `<script>` tags in the
built output concludes the site is broken. And check the claim it may falsify: a demo form
that says *nothing is sent, nothing is stored* stops being true the moment a tag reports
interactions with it, which is expensive on a site whose whole argument is candour.
**Verify an attribution chain end to end, never link by link.** Attribution runs
markup → transport → destination tooling, and each step can be individually defensible
while the chain delivers nothing. Seen here: `rel="noopener noreferrer"` on outbound links
(correct as a general default) plus a host analytics product that deliberately logs no
query strings (a reasonable privacy stance) — together they erased both the referrer and
the UTM parameters, so the funnel could not tell which demo produced a lead. Neither
choice was wrong. Follow one real click from demo to destination and confirm what actually
arrives.
## Reference files
- [`references/positioning.md`](references/positioning.md) — deriving a vertical's
positioning and forcing contrast against sibling demos
- [`references/footer-contract.md`](references/footer-contract.md) — the attribution,
the social icons, and the CSS-only pitch dialog every site must carry
- [`references/placeholder-protocol.md`](references/placeholder-protocol.md) — the
component, the brief format, the manifest contract
- [`references/astro-stack.md`](references/astro-stack.md) — scaffold, conventions, and
the trap list
- [`references/image-generation.md`](references/image-generation.md) — driving codex CLI,
holding aspect ratios, and the master-then-fan-out consistency strategy
- [`references/share-cards.md`](references/share-cards.md) — the Open Graph / Twitter
cards, the head tag set, and the staleness guard
### Copy these in rather than rewriting them
These sites are one-off sales assets, not a maintained product, so the mechanics are
**copied per site** rather than shared as a package. That is the right call for the
lifecycle — but it means every rewrite is a chance to introduce a silent variation, so
copy the files instead of regenerating them from memory.
| File | Notes |
| --- | --- |
| [`assets/Placeholder.astro`](assets/Placeholder.astro) | No edits — speaks the semantic aliases |
| [`assets/SocialIcons.astro`](assets/SocialIcons.astro) | Keep only the chosen platforms; colours need no edits |
| [`assets/collect-image-manifest.mjs`](assets/collect-image-manifest.mjs) | Set `SITE_NAME` |
| [`assets/prune-unreferenced-assets.mjs`](assets/prune-unreferenced-assets.mjs) | Wire into `build`; no edits needed |
| [`assets/typeset.py`](assets/typeset.py) | Text-to-outline primitives; imported by both brand scripts. No edits |
| [`assets/shape.py`](assets/shape.py) | **Only when a non-Latin script appears anywhere.** HarfBuzz shaping for the share cards — `typeset.py` lays out by advance width, which silently drops stacked marks. See [`references/share-cards.md`](references/share-cards.md) |
| [`assets/build_og.py`](assets/build_og.py) | Set the palette, the two font faces, and the display sizes |
| [`assets/og-cards.mjs`](assets/og-cards.mjs) | Set `SITE_NAME` — to the **data file's** spelling, not the wordmark's |
| [`assets/build-og-images.mjs`](assets/build-og-images.mjs) | Wire as `og:build`; no edits needed |
| [`assets/check-og-images.mjs`](assets/check-og-images.mjs) | Wire into `build`; no edits needed |
These components use **semantic aliases** (`bg-paper`, `border-line`, `text-ink-soft`,
`bg-action`, `bg-spark`), not palette names. Declare the alias block in `@theme` — see
[`references/astro-stack.md`](references/astro-stack.md) — and copy the components
untouched. Retokening them by hand was the one recurring source of per-site drift.
If this family ever stops being one-off — if a footer change has to reach every live site
— stop copying and extract these into a package instead.