# Create Gh Pages Site

> Scaffold a working GitHub Pages website from a vetted template and wire it to deploy automatically. Use when the user wants to create, scaffold, or publish a site on GitHub Pages: a reviewable VitePress specification, a static page, an Astro or Eleventy site, a React (Vite) SPA, or a Jekyll site. Picks the right template, injects the correct base path for the target repo (the #1 thing people get wrong), adds the official GitHub Actions Pages deploy workflow, and sets it up in the user's current repo by default or a new one if asked. Does not stop at demo content: it digests the target repo (README, manifests, entry points, docs) and authors a site about that repo (CLI reference, API/usage page, feature tour, or catalog) with labeled image placeholders the user swaps in. Do NOT use for non-Pages hosting (Vercel/Netlify/Azure), deploying an existing app without a Pages target, or plain web pages unrelated to GitHub Pages.

- Skill: `jongio/create-gh-pages-site` (Agent Skill, multi-file: 43 files)
- Install (CLI): `npx skillmds@latest add jongio/create-gh-pages-site`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jongio/create-gh-pages-site/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: jongio (https://skillmd.com/u/jongio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jongio/create-gh-pages-site

---


# Create GitHub Pages Site

Turn "put this on GitHub Pages" into a correct, building, auto-deploying repo. The
hard part of GitHub Pages isn't the HTML — it's the deploy plumbing and the
**base-path trap**. This skill owns both: it stamps a vetted template, sets the
base path for the exact repo, ships the current official Pages workflow, and walks
the user through enabling Pages.

It also never stops at the template's demo content. A stamped template is a
**skeleton, not the deliverable** — the skill reads the target repo and authors a
site about *that* project: real name and pitch, the right kind of reference (CLI,
library, app, or catalog), real install/usage pulled from the repo, and labeled
image placeholders the user can drop real art into. Shipping a site that still says
"Hello, Astro" for someone's CLI is a failure, even if it deploys.

## When to use which template

Pick by what the user is building. If unsure, ask one question (content vs. app)
and default to `static-html` for the simplest ask.

| Template | Reach for it when… | Tier | Build |
| --- | --- | --- | --- |
| `static-html` | A landing page, a few hand-made pages, "just put this HTML up". No toolchain. | static | none |
| `astro` | A content site, blog, docs, or marketing page that should be fast and mostly static. | SSG | `astro build` |
| `react-vite` | An interactive single-page app / dashboard with client-side routing. | SPA | `vite build` |
| `eleventy` | A data/Markdown-driven site (blog, docs) where content is files + structured data. | data SSG | `eleventy` |
| `jekyll` | The user wants the GitHub-native path, or is migrating an existing Jekyll site. | native | Jekyll |
| `skills-catalog` | A browsable catalog for a repository of Copilot skills, especially when composing `create-skills-repo`. | catalog | registry-defined |
| `spectator` | A technical specification, RFC, architecture proposal, design proposal, engineering decision, or policy proposal built for structured review. | SSG | `vitepress build docs` |

The built-in registry pin must include the selected template. A custom registry
requires an explicit `--registry-ref`, and a local checkout uses
`--templates-dir`.

These seven cover the static / SSG / SPA / data / native / catalog / specification
quadrants. For richer
*themes*, point the user at upstream galleries (astro.build/themes,
jamstackthemes.dev, github.com/topics/github-pages-template) and adapt — don't try
to hand-build a theme from scratch.

## The base-path trap (the thing to get right)

A GitHub **project** site is served from a subpath:
`https://<user>.github.io/<repo>/`. A site built assuming root (`/`) ships with
broken CSS, images, and links the moment it's a project site — assets 404 and the
page looks blank. A **user/org** site (`<user>.github.io` repo) *is* served from
`/`, so it must NOT carry a subpath prefix.

Each framework fixes this differently. The generator handles all of it from the
repo name; you rarely set it by hand:

| Template | Mechanism | Project value | User-site value |
| --- | --- | --- | --- |
| `static-html` | relative URLs (`./assets/...`) | — (immune) | — (immune) |
| `astro` | `base` in `astro.config.mjs` | `/repo/` | `/` |
| `react-vite` | `base` in `vite.config.js` + `basename` + `404.html` | `/repo/` | `/` |
| `eleventy` | `pathPrefix` via `PATH_PREFIX` env + `url` filter | `/repo/` | `/` |
| `jekyll` | `baseurl` in `_config.yml` + `relative_url` | `/repo` (no slash) | `""` (empty) |
| `spectator` | `base` in `spectator.config.ts` and VitePress config | `/repo/` | `/` |

**The generator detects the `<user>.github.io` user-site pattern and uses `/`
automatically.** If the user hasn't named the repo yet, scaffold with the repo
they intend; if they truly don't know, use `--base /` and tell them to re-run (or
edit the base) once the repo exists.

## Deployment model — one consistent flow

Every template deploys via the **GitHub Actions** Pages source, not from a branch.
Every external action reference must use a full commit SHA.

- `static-html`, `react-vite`, `eleventy` use that chain directly.
- `astro` uses the official `withastro/action@v6` (it builds + produces the Pages
  artifact) then `actions/deploy-pages@v5`.
- `jekyll` uses GitHub's official `actions/jekyll-build-pages@v1` then `deploy-pages`.

Every workflow limits top-level permissions to `contents: read`, grants Pages and
identity-token access only to the jobs that need them, uses one Pages concurrency
group, and deploys through the `github-pages` environment. There is **no
`gh-pages` branch** to manage.

> Action versions deprecate aggressively. Update the template registry to a reviewed
> action commit, then bump this skill's immutable default registry commit.

## Interview first — never scaffold on a guess

This skill produces a real repo with a live deploy pipeline, so getting the inputs
right matters more than speed. **Do not stamp anything until you know (a) which
template and (b) the target repo (or an explicit base path).** If the prompt is bare
("make me a GitHub Pages site") or names no framework/repo, interview the user — ask
only for what's missing, one focused question at a time, using the `ask_user` tool:

1. **What kind of site?** Map the answer to a template:
   - "a landing page / just some HTML / the simplest thing" → `static-html`
   - "a blog / docs / content / marketing site, fast" → `astro` (or `eleventy` if
     they say Markdown- or data-driven, or `jekyll` if they want the GitHub-native
     path or are migrating an existing Jekyll site)
   - "an app / dashboard / interactive / single-page app" → `react-vite`
   - "a catalog of Copilot skills / create a skills repo" → `skills-catalog`
   - "a specification / RFC / architecture or design proposal / engineering
     decision / policy proposal / site like the Eval Authoring Guide" →
     `spectator`

   If they're unsure, ask the single discriminating question — *content site or
   interactive app?* — and default to `static-html` for the simplest ask.
2. **Which repo? Assume the current repo by default.** The site is for the repo in
   context unless the user says otherwise — don't ask "current or new." Detect the
   current repo from git (`git remote get-url origin`, parsed to `owner/name`); the
   generator does the same automatically when you omit `--repo`. The repo drives the
   base path, so you must resolve it before stamping:
   - **Current repo detected** → use it. State the assumption in one line
     (*"Scaffolding into this repo, octocat/blog → base `/blog/`"*) and proceed; no
     question needed.
   - **No git context, detached, or no `origin`** → then ask for the `owner/name`,
     and whether it's an existing repo or a **new** one to create.
   - Only treat it as a **new/different** repo when the user explicitly asks for one.
     For a user site, confirm the repo is named `<user>.github.io` (base `/`);
     otherwise it's a project site (base `/repo/`).
3. **Title?** Optional — default to the repo name; never block on it.

You usually don't need to ask "what should the site say?" — the **content comes from
the repo** (you digest it after stamping; see "Digest the repo, then author it").
When the target repo is the current/an existing one, run `digest-repo.mjs` early to
confirm the type and let it inform the template choice (e.g. a `cli` repo is a great
fit for `astro` or `static-html` with a command reference). If the repo is empty or
brand-new, fall back to asking what the site should cover.

Skip any question the prompt already answered: *"an Astro blog for octocat/blog"*
needs no interview (template `astro`, repo `octocat/blog`). For a bare *"put this on
Pages"* inside a repo, assume the current repo and ask only for the template. Ask
only for the gaps. When you **inferred** rather than were told, confirm in one line
before scaffolding — e.g. *"Astro site → octocat/blog (current repo), base `/blog/` —
go?"*

## Output location invariant

**The user's current working directory is the destination workspace.** A repo slug
such as `octocat/blog` determines the Pages base path and URLs; it does not mean the
site belongs in `/tmp/blog`, a sibling directory, or the skill's own directory.

- If the workspace already contains repo files, scaffold in place with
  `--dir . --force`, even when it is not a git checkout and the prompt supplies the
  repo slug explicitly.
- Never `cd` into the skill directory to run the generator. Invoke
  `scripts/new-site.mjs` by its absolute path while keeping the workspace as the
  process working directory.
- Use another output directory only when the user explicitly asks for a subfolder
  or a new/different repo.
- When another generator is composing the result, use `--staging-dir` instead of
  `--dir`. The staging directory is isolated and the consumer owns the final merge.
- Before reporting completion, confirm the generated config, content, and
  `.github/workflows` files exist under the original working directory.

## The workflow you follow

1. **Interview / gather context.** Resolve the questions above via `ask_user`. You
   MUST end up with a chosen template and a target repo (or explicit `--base`) before
   stamping. **Default the target to the current repo** (detect it from
   `git remote get-url origin`); only ask when there's no git context or the user
   wants a different/new repo.
2. **Pick the template** from the table above.
3. **Stamp it** with the generator (next section). This injects the base path,
   site URL, and title, and lays down the deploy workflow.
4. **Place it in the repo:**
   - *Current repo (default)*: stamp into the repo root (or a subfolder if it's a
     subdirectory site, adjusting the workflow's upload path). From the original
     working directory, invoke the generator by absolute path and pass `--dir .`
     `--force` to write in place. Reconcile an existing `deploy.yml` rather than
     blindly overwriting it.
   - *New repo (only when asked)*: create it (e.g. `gh repo create <name> --public`),
     stamp into it, and push. Match the repo name you used for the base path.
5. **Digest the repo and author the site** (see "Digest the repo, then author it"
   below). This is the step that makes the site *real*: run the digest, replace
   every default page/section with repo-derived content of the right kind, add
   image placeholders + an `IMAGES.md`, and **prompt the user for the real images**
   (they can paste screenshots straight into the chat). Never skip to enabling Pages
   on the demo content.
6. **Enable Pages.** Tell the user (or do it with their approval):
   **Settings → Pages → Source → GitHub Actions**. By CLI:
   `gh api -X POST repos/<owner>/<repo>/pages -f build_type=workflow` (or PUT to
   update). After the first push to `main`, the workflow runs and the live URL
   appears in the Actions run summary and under Settings → Pages.
7. **Set the repo website link** to the Pages URL (the "Website" field in the repo
   header — same as ticking *Settings → "Use your GitHub Pages website"*). This is
   just the repo's `homepage`; point it at the site URL the generator prints:
   `gh repo edit <owner>/<repo> --homepage <site-url>` (or
   `gh api -X PATCH repos/<owner>/<repo> -f homepage=<site-url>`). Offer to set the
   exact URL, or let the user supply a custom domain instead. There's no separate
   "use Pages" boolean — setting `homepage` to the Pages URL *is* the checkbox.
8. **Verify it actually works** (see "Validate" below) — don't claim success on a
   green workflow alone, and don't claim success while template default copy or
   "Hello, world" demo content is still on the page.

## Stamp a site — the generator

```sh
node scripts/new-site.mjs <template> --repo <owner/name> [options]
```

| Option | Purpose |
| --- | --- |
| `--repo <owner/name>` | Target repo. Derives the base path + URLs (and detects user sites). **Defaults to the current repo's `origin` remote when omitted.** |
| `--base </path/>` | Override the base path (e.g. `/` for a user site or local preview). |
| `--dir <path>` | Output directory (default: `./<repo-name>`). |
| `--site-name "Title"` | Human title (default: derived from the repo name). |
| `--description "Text"` | Catalog description (default: derived from the title and repo). |
| `--author "Name"` | Catalog author (default: repo owner). |
| `--package-name <id>` | Package identifier (default: repo name). |
| `--marketplace-id <id>` | Marketplace identifier (default: owner and repo). |
| `--default-branch <id>` | Repository default branch used by catalog links and deployment triggers (default: `main`). |
| `--registry <owner/repo>` | Template registry repo to fetch from (default: `jongio/gh-pages-templates`; needs git + network). |
| `--registry-ref <sha>` | Full 40-character commit SHA. Required with a custom registry. Branches and tags are rejected. |
| `--templates-dir <path>` | Use a local `templates/` folder instead of fetching (offline). |
| `--staging-dir <path>` | Write a validated tree to a new path without applying it to a target. |
| `--force` | Write into a non-empty directory. |
| `--json` | Emit a machine-readable result for composition. |
| `--list` | List available templates. |

Templates are **not bundled in the skill**. The generator fetches the built-in
`jongio/gh-pages-templates` registry at a reviewed, immutable commit. A custom
registry requires `--registry-ref <full-sha>`. Pass `--templates-dir <path>` to
scaffold from a local copy offline.

Examples:

```sh
# Run from the target workspace; SKILL_DIR is this skill's absolute directory.
# Scaffold in place for the current repo (base inferred from its origin remote):
node "$SKILL_DIR/scripts/new-site.mjs" astro --dir . --force

# The workspace is an existing project without git metadata; the slug only supplies
# deployment identity and the base path. Output still goes into the workspace:
node "$SKILL_DIR/scripts/new-site.mjs" astro --repo octocat/blog --dir . --force

# Create a subfolder only when the user explicitly requested one:
node "$SKILL_DIR/scripts/new-site.mjs" react-vite --repo octocat/dashboard --dir ./site

# A user site (served from "/") or a quick local scaffold:
node "$SKILL_DIR/scripts/new-site.mjs" static-html --base / --dir . --force

# Safely compose a skills catalog without touching the consumer's target:
node "$SKILL_DIR/scripts/new-site.mjs" skills-catalog \
  --repo octocat/skills \
  --templates-dir ../gh-pages-templates/templates \
  --staging-dir ./.site-staging \
  --json
```

The generator replaces the shared base and repository sentinels plus the catalog
sentinels `__SITE_DESCRIPTION__`, `__REPO_OWNER__`, `__REPO_NAME__`,
`__AUTHOR_NAME__`, and `__MARKETPLACE_ID__`. Injection is deterministic and the
result has no placeholders left. After stamping you may hand-edit content freely.
Metadata values must be plain context-safe text. Markup, control characters, and
characters that can break JSON, YAML, JavaScript, or HTML attributes are rejected.

### Safe staging contract

Use `--staging-dir` whenever `create-skills-repo` or another tool will merge the
site into a larger output. The path must not exist and the option cannot be
combined with `--dir` or `--force`. The generator performs replacement and all
validation inside staging, returns only after success, and never touches the final
target. The consumer must check every destination conflict before copying staged
files.

Before either staging success or normal apply, the generator:

1. verifies an external registry checkout matches the requested full commit SHA;
2. proves the resolved template path remains inside the registry templates root;
3. rejects every symlink in the template tree;
4. rejects unresolved `__SENTINEL__` placeholders;
5. scans `.github/workflows/*.yml` and `.yaml` for excess permissions, unsafe
   triggers, non-SHA action references, persisted checkout credentials,
   lifecycle-capable installs without script suppression, and direct GitHub
   context interpolation in shell commands.

Normal `--dir` generation uses a private staging directory and applies only after
all checks pass. This preserves existing template and `--force` behavior while
ensuring an invalid template cannot partially modify the target.

The exact pinned built-in snapshot has a compatibility normalization for its five
legacy templates. It upgrades their known action tags to reviewed commit SHAs,
disables persisted checkout credentials and lifecycle scripts, and then validates
the result. Never apply this normalization to a custom registry.

## Digest the repo, then author it (never ship the template defaults)

Stamping gives you a working skeleton with **demo content**. The job is only half
done. Now make the site about the actual repo. This is not optional polish — it's
the deliverable.

### 1. Run the digest

```sh
node scripts/digest-repo.mjs --dir <path-to-repo> --json
```

It returns deterministic signals you build from: `name`, `description` (the pitch),
`repoSlug`, `license`, a `type` classification (`cli` | `library` | `app` |
`action` | `collection` | `docs` | `site`) with the reasons behind it, suggested
`install` commands, README `usageExamples` (code fences), `badges`, `docFiles`,
existing `images` (with role hints like `logo`/`hero`/`screenshot`), `languages`,
and `subProjects` (for monorepos/collections). Read the repo's README and key docs
yourself too — the digest points you at them; it doesn't replace judgment.

### 2. Build the right *kind* of site for the type

| Type | Tell-tale signals | Author the site around… |
| --- | --- | --- |
| `cli` | `bin`, console_scripts, `[[bin]]`, `--help` in README | Install + Quickstart, then a **command/flag reference** (a section or page per command), copy-paste examples, config/exit codes. Hero = terminal demo. |
| `library` | `main`/`exports`, `[lib]`, import examples | Install, an **import + usage** snippet, an **API reference** (exported functions/types from the README/docs), examples, badges. Hero = a concept diagram. |
| `app` | web-framework dep, `index.html`, `src/` | A **feature tour** with screenshots, a "Get started"/live-demo CTA. Hero = an app screenshot. |
| `action` | `action.yml` | A `uses:` snippet, an **inputs/outputs table**, an example workflow. |
| `collection` | `plugin.json`/`marketplace.json` + `skills/`, workspaces, `packages/` | A **catalog**: one card/detail per `subProject` (its pitch + install), plus a top-level install for the whole thing. Hero = a banner; per-item thumbnails. |
| `docs` | many Markdown docs, little code | A **docs nav + content**, pulling the existing Markdown in. |
| `site` / unknown | none of the above | A clean landing built from the repo's pitch and links. If the shape is unclear, ask the user what sections they want. |

### 3. Authoring rules

- **Replace every default.** No template demo copy survives — not the sample hero,
  not "Hello, Astro/world", not the example blog posts, not lorem. After building,
  grep the output for the template's stock phrases; none should remain.
- **Use real values from the digest:** the repo's name and `description` as the
  title/tagline, the `install` commands verbatim, the README `usageExamples` as
  real code blocks, `badges`, `license`, and links to the repo and each
  `subProject`. **Don't invent** features, commands, or APIs you can't see in the
  repo — if something's unclear, leave a visible `TODO` for the user rather than
  fabricate.
- **Fit the template's content model.** Use content collections (Astro/Eleventy:
  one entry per command/skill/post), pages (React/static), etc. Add or rename
  routes to match the content (e.g. a `commands/` or `catalog/` section) instead of
  forcing everything into the demo "blog". Keep the GitHub source link and the
  base-path-aware internal links the template already wires — never hand-write
  absolute `/...` links (use the template's base helper, or it breaks on a project
  site).
- **Keep Spectator free-form.** Do not force a standard specification outline.
  Derive pages, headings, and navigation from the supplied context. Preserve the
  Spectator reading and feedback system, but remove every Project Northstar sample
  page, label, requirement, source, and image reference.
- **Label uncertainty in Spectator.** Distinguish verified current behavior,
  user-approved decisions, proposals, assumptions, risks, and open questions.
  Do not invent owners, dates, requirements, interfaces, or security claims.

### 4. Add image placeholders the user can supply

Real sites need art the agent can't produce. Drop in **obvious placeholders** plus
a checklist so the user knows exactly what to provide:

```sh
node scripts/make-placeholder.mjs --out <site>/<images-dir> --preset <type> --repo owner/name
```

`--preset` is the repo type (`cli`/`library`/`app`/`collection`/`site`). It writes
labeled SVG placeholders (logo, social card, favicon, hero, and type-specific
shots) and an `IMAGES.md` manifest listing each file's purpose and recommended
dimensions. Then:

- **Reference them** from the pages you author — hero, top-bar logo, the OG/social
  meta tag, and per-item thumbnails for a catalog.
- **Reuse real images first.** If the digest found an existing logo or screenshot
  (e.g. a `docs/*.png`), use it instead of a placeholder.
- **Put images where the template serves static files:**
  - `astro`, `react-vite` → `public/images/` (served at `${BASE_URL}images/…`)
  - `spectator` → `docs/public/images/` (VitePress applies its configured base)
  - `static-html` → `assets/images/` (relative `./assets/images/…`)
  - `eleventy` → `src/assets/images/` (through the `url` filter)
  - `jekyll` → `assets/images/` (via `relative_url`)
- **Leave `IMAGES.md` in the images dir** as the hand-off, and tell the user it's
  there. A placeholder still deploys fine; it just visibly says "replace me".

### Ask for real images

After authoring, ask only for images the site references. Give each role, filename,
and recommended dimensions; accept pasted images, file paths, or URLs. Keep
unprovided placeholders, update `IMAGES.md`, and do not block deployment.
`digest-repo.mjs` discovers source material; `make-placeholder.mjs` creates the
placeholder set and handoff manifest.

## Per-template notes

- **static-html** — Zero build. All links are relative, so it's base-path-proof.
  Ships `index.html`, `about.html`, `404.html`, `assets/`, and a `.nojekyll`.
- **astro** — `site` = origin, `base` = `/repo/`. Internal links use
  `import.meta.env.BASE_URL`. `public/` is copied verbatim. Node 24 in CI by default.
- **react-vite** — `base` in `vite.config.js`; React Router `basename` derived from
  `BASE_URL`; `copy-404.mjs` (a `postbuild` hook) copies `index.html` → `404.html`
  so deep-link refreshes work on Pages. Reference public assets as `/asset` so Vite
  rewrites them with the base.
- **eleventy** — `pathPrefix` comes from the `PATH_PREFIX` env the workflow sets;
  every link/asset uses the `url` filter. Posts live in `src/posts/`, listed via
  `collections.posts`. Locally it serves at `/`.
- **jekyll** — `baseurl` (no trailing slash) in `_config.yml`; links use
  `relative_url`. Built in CI by `jekyll-build-pages` (honors the `Gemfile`). Local
  dev needs Ruby + Bundler; CI does not.
- **spectator**: VitePress reads `base` from `spectator.config.ts`. Content is
  free-form Markdown under `docs/`; navigation is explicit in the shared config.
  Preserve local search, page outlines, light/dark appearance, plain-language
  callouts, page feedback, selected-text feedback, and GitHub edit links. Run
  `npm test`, `npm run build`, and `npm run test:e2e`.

## Current repo vs. new repo
- **Current repo (the default)**: assume the site is for the repo in context. The
  generator infers the base path from its `origin` remote when you omit `--repo`.
  Put the site at the root for a whole-repo site, or in a subfolder and point the
  workflow's `upload-pages-artifact` `path:` at it. If a `deploy.yml` already exists,
  reconcile — don't blindly overwrite.
- **New repo (only when asked)**: create with `gh repo create`, stamp, push to
  `main`. For a **user site**, the repo MUST be named `<user>.github.io` and the base
  is `/` — the generator handles the base when you pass that repo name.

## Custom domains (documented, not automated)
For a custom domain: add a `CNAME` file (for static/Jekyll, at the served root;
for Astro, `public/CNAME`), set DNS at the registrar, and in Astro set `site` to
the domain and drop `base`. Don't automate DNS — explain the steps.

## Template registry & contributing
Templates live in **one** place: the
**[`jongio/gh-pages-templates`](https://github.com/jongio/gh-pages-templates)**
registry. The skill does **not** bundle its own copy — the generator fetches
templates from the registry at runtime (override with `--registry <owner/repo>`, or
scaffold offline from a local checkout with `--templates-dir <path>`):

```sh
node scripts/new-site.mjs astro --repo octocat/blog            # pinned default registry
node scripts/new-site.mjs skills-catalog --repo octocat/skills \
  --registry octocat/templates --registry-ref 0123456789abcdef0123456789abcdef01234567
node scripts/new-site.mjs astro --templates-dir ../gh-pages-templates/templates
```

Each template is a folder with a `template.json` manifest, a deploy workflow,
base-path handling via the sentinels, and a `README.md`. The registry also renders
the browsable gallery (live previews of every template) at
**https://jongio.github.io/gh-pages-templates/** — the single home for browsing and
previewing. Point users there to browse, and to the registry's `CONTRIBUTING.md` to
submit a new template. Template changes (add/fix a template, bump an action version)
land in the registry, not here.

## Validate — don't claim done on a green check

1. **Content check first.** Confirm the site is about the repo, not the template:
   the title/tagline, install commands, and examples are the repo's real values, and
   **no template demo copy survives** (grep the built output for the template's stock
   phrases — "Hello", "islands", "lorem", the sample post titles — and for leftover
   `__…__` sentinels). The page kind matches the repo type (CLI ref / API ref /
   feature tour / catalog). Image placeholders exist and `IMAGES.md` is present.
2. **Build it.** For `astro`/`react-vite`/`eleventy`/`spectator`, run
   `npm ci --ignore-scripts --no-audit --no-fund` then `npm run build` and confirm
   both exit 0 and emit the output dir
   (`dist` / `_site`). For `jekyll`, `bundle exec jekyll build` if Ruby is present.
3. **Check the base path.** Open the built output and confirm asset/link URLs carry
   the project prefix (`/repo/...`) — not bare `/...`. This is the failure mode that
   "looks deployed but renders blank."
4. **After deploy**, load the live `page_url` from the Actions run; click an
   internal link and (for the SPA) refresh a sub-route to confirm the `404.html`
   fallback works.
5. Only then report it as working.

Run the skill's own tests with `npm test` (the generator + the repo-digest and
placeholder checks, offline via a fixture). Template/workflow validation lives in
the `jongio/gh-pages-templates` registry.

## Footguns
- **Never** ship a project site built for `/` — assets 404. Set the base path (the generator does this; verify it).
- **Never** put a subpath base on a **user site** (`<user>.github.io`) — it must be `/` (Jekyll: `baseurl: ""`).
- **Never** use `actions/upload-artifact` for Pages — it's `upload-pages-artifact`.
- **Never** reach for `peaceiris/actions-gh-pages` or a `gh-pages` branch — use the first-party Actions flow these templates ship.
- **Never** forget to set **Source → GitHub Actions** in Settings → Pages; the workflow can't publish until Pages is enabled for Actions.
- **Don't** leave the repo "Website" link blank — set `homepage` to the Pages URL (`gh repo edit --homepage`) so visitors find the site; it's the same as the "Use your GitHub Pages website" checkbox.
- **Never** claim success because the workflow is green — load the URL and check an asset and an internal link actually resolve.
- **Never** ship the template's demo content. A stamped template that still says
  "Hello, Astro", lists sample blog posts, or contains Project Northstar is a
  failure. Digest the repo and author real content of the right kind.
- **Never** fabricate features, commands, or APIs to fill the page. Author only what
  the repo actually shows; leave a visible `TODO` when unsure.
- **Don't** leave bare image references with nothing behind them — add the placeholders + `IMAGES.md`, or reuse the repo's existing images.
- **Don't** ship placeholders silently — tell the user which real images the site needs and that they can paste a screenshot into the chat to fill each one.
- **Don't** hand-roll a base-path setup when the generator + sentinels already do it correctly per framework.
- **Never** pass a branch or tag as `--registry-ref`. Only a full commit SHA is
  immutable enough to validate.
- **Never** compose directly into another generator's target. Stage first, check conflicts, then merge.

