mcp-ai-wpoos-astro-forge
Operational guide for the NV oOS Astro Forge (Proposal 096) — the WP→Astro frontend generator in examples/nvoos-astro-forge (wp-client, Astro content-layer loader, versioned sanitizer, scaffold CLI, b
Files1
View on GitHubSKILL.md.agents/skills/mcp-ai-wpoos-astro-forge/SKILL.md
GitHubNV oOS Astro Forge — ops guide (Proposal 096 Phase 0)
What this is
The generator that turns a WordPress/WooCommerce site into a native Astro 7
headless frontend. WordPress stays the CMS; the generated project pulls
content through an Astro content-layer loader at build time (freshness =
webhook → rebuild, never runtime fetches), renders sanitized HTML, and ships
SEO-complete static output. Pilot customer: a WooCommerce storefront — its
design language was already extracted via Proposal 098 (see the customer
notes below). Full context: docs/project/proposals/096-astro-forge-wp-generator-mcp-proposal.md
(+ -implementation-plan.md, -research.md).
Architecture decisions (do NOT re-litigate)
- Content-layer loaders, not materialized content files —
content_materializestays opt-in (096 §2.3). - REST-first:
wp/v2+wc/v3, never WPGraphQL._fieldspruning +_embedare the anti-N+1 discipline. - Sanitization is mandatory, versioned, allowlisted, never a passthrough — all WordPress HTML passes
lib/sanitize.tsbeforeset:html(096 §2.4). - No plaintext credentials — auth comes from
.env(WP_APPLICATION_USER/WP_APPLICATION_PASSWORD), never from the profile or the generated repo. SSRF-safe by construction: the CLI consumes operator-written site profiles only (the open-world fetch surface is the/mcpdesign_analyzeguard, Proposal 098). - Never silent truncation —
fetchAllreturnstruncated: true, the loader warns, entries carry the flag, pages show a notice. - Built assets never ship — the scaffold's gitignore gate guarantees
dist/+.astro/are ignored in the customer repo.
File map
| Path | Role |
|---|---|
lib/types.ts |
SiteProfile / WpCollection contracts |
lib/wp-client.ts |
WP REST client — X-WP-TotalPages pagination, typed WpClientError codes |
lib/wp-loader.ts |
Astro 7 content-layer loader (createWpLoader) |
lib/sanitize.ts |
SANITIZE_VERSION + allowlist + sanitizeHtml() |
lib/scaffold.ts |
Template copy, FORGE-marker rewrite, manifest, gates |
lib/vault.ts |
Phase 3.1 credential store — AES-256-GCM per-entry (~/.nvoos-forge/vault.json), NVOOS_FORGE_VAULT_KEY passphrase, plaintext opt-in, maskSecret |
lib/sites.ts |
Phase 3.4 multi-site registry (sites.json) — named profile refs + default flag |
lib/github.ts |
Phase 3.2 octokit deploy — private-by-default repo create, git-data commit/push, sanitized errors |
lib/deploy-config.ts |
Phase 3.3 generated pipeline — deploy.yml/rebuild.yml + fail-closed verify-dispatch.mjs |
lib/wizard.ts |
Phase 4 setup_wizard — analyze → register → scaffold → build → git init → pipeline |
lib/content-scan.ts |
§3.3 content audit scanner (htmlparser2 — embeds/alt/hosts/shortcode artifacts + unprocessed brackets) |
lib/redirects.ts |
§3.3 live 301-map generator (the template's exact rules, computed from REST) |
lib/ssrf.ts |
URL guard — private/literal-range refusal, registry allowlist + NVOOS_FORGE_OPEN_ANALYZE opt-in, CLI-only loopback exemption |
lib/project-files.ts |
Shared source collector for pushes (CLI deploy + github_commit tool) |
mcp/ |
Phase 4 MCP surface (095 dual-era pattern): single-source registry.ts (zod + annotations + write flags; router/full modes), bff.ts (rate limit → Host/Origin → fail-closed write auth), stdio.ts, 7 tool slices |
scripts/forge-mcp.mjs |
MCP Streamable HTTP entry (FORGE_MCP_PORT, default 8472) |
server.json |
061-pattern MCP registry metadata (io.github.nvdigitalsolutions/nvoos-astro-forge) — NOT in the npm files |
scripts/forge.mjs |
CLI (Node ≥ 22.6 — native TS type stripping, zero build step) |
scripts/wp-fixture-server.mjs |
Hermetic wp/v2 wire fixture — the Phase 0 CI stand-in for WordPress Playground |
scripts/builtin-template.mjs |
Test fixture generator (nested main/templates/blog/ tree with a REAL loader) |
templates/blog/ |
The production template (posts/pages, canonical + JSON-LD, sitemap/robots, --forge-* tokens) |
tests/ |
wp-client / sanitize / wp-loader / scaffold unit suites + forge-e2e.test.ts (exit gate) |
.github/workflows/astro-forge.yml |
CI: typecheck + unit + e2e on Node 24 (Astro 7 needs ≥ 22.23.4 — the runner's latest 22.x is one patch short) |
Dev loop (hermetic — no live site needed)
cd examples/nvoos-astro-forge
npm install
npm test # 38 unit tests (client, sanitizer, loader, scaffold)
npm run typecheck
npm run test:e2e # exit gate: fixture WP → scaffold → npm install → astro check + build → HTML asserts (~40s, registry access for the generated project's install)
node scripts/wp-fixture-server.mjs # :8473 — real wp/v2 wire shapes
node scripts/forge.mjs site.json --target ./out --check # live source check (root doc + per-collection size-vs-cap)
node scripts/forge.mjs site.json --target ./out # scaffold the project
Forging a customer site (the standard loop)
- Write
site.json(operator allowlist):{ url, name, collections: [{ slug, restBase, fields, embed, perPage, maxPages }] }. --checkfirst — a build must never silently truncate.- Scaffold →
cd <target> && npm install && npm run buildwithASTRO_SITEset. - Design language (098
design_themeoutput) applies over the template's--forge-*tokens insrc/styles/global.css— content is re-themed, never re-cloned.
The exit gate (CI-enforced)
tests/forge-e2e.test.ts boots the fixture server on an ephemeral port,
scaffolds the production template, npm installs the generated project,
runs astro check + astro build, then asserts on dist/: posts/pages
rendered, alert/onerror/javascript: count = 0 in the xss-trap post,
allowlisted embeds survive, canonical + BlogPosting JSON-LD + sitemap/robots
wired. The workflow runs on paths: examples/nvoos-astro-forge/**.
Gotchas (each cost a fix 2026-10-31 — do not re-hit)
- Astro 7 spread params are flat slash-joined strings, NOT arrays —
getStaticPathsforsrc/pages/posts/[...parts].astromust returnparams: { parts: '1/slug' }. The route generator uses the params value as the path segment directly and the validator requirestypeof value === 'string'(verified against astro 7.4.4 dist sources:routing/generator.js,routing/internal/validation.js,render/params-and-props.js). An array fails the build withGetStaticPathsInvalidRouteParam. - FORGE marker rewriting must be line-anchored — naive
indexOf('// FORGE:site')matches prose that mentions the marker inside a header comment and manglescontent.config.ts. Matchline.trim() === '// FORGE:site'only. - Template-copy filters must exclude by directory basename — a
/(node_modules|dist|\.astro)$/path regex also matches every*.astropage file (the\.astro$branch) and silently drops all pages. Filter onpath.basename(src)∈ {node_modules, dist, .astro}. - Emitting a regex literal through a JS template literal needs the
backslash doubled —
\\/+$/in the scaffold source (an identity escape otherwise emits//, turning the generated line into a comment and breaking the parse). - Truncation math:
truncated = page === maxPages && page < totalPages— a catalog that ends exactly at the cap is complete, not truncated. sanitize-htmlallowedIframeHostnamesstrips the disallowedsrcbut keeps the inert<iframe>tag — assert onsrcabsence, not tag absence.- Node ≥ 22.6 native type stripping runs the CLI with zero build step —
scripts/forge.mjsimports the.tslibs directly. Requirements: explicit.tsextension specifiers in relative imports, erasable-only TS (no enums/ namespaces),allowImportingTsExtensions: true+noEmitin the tsconfig. The generated template inheritsallowImportingTsExtensionsfromastro/tsconfigs/strict— vendored.ts-extension imports typecheck as-is. astro checkon a generated project needs@types/sanitize-htmland@types/nodein the template devDeps — the vendoredsanitize.tsandwp-client.tstypecheck under the language server.--overwritere-scaffold is destructive — itrms the target includingnode_modules(reinstall after). An empty target dir is safe to scaffold into; a non-empty non-forge dir is refused withoutoverwrite: true.- Woo sets
priceto the sale price while on sale — sale state issalePrice !== regularPrice, neversalePrice !== price(the first cut rendered no strike-throughs at all). wc/v3 needs consumer keys (401 without); wp/v2 product CPTs stay public but have no commerce fields — both shapes share one loader viakind: 'product'. _embeddoes not guarantee media — resolvefeatured_mediaIDs viawp/v2/media/{id}?_fields=source_urlwith bounded concurrency (5); an image miss is never a build failure.- Template pages binding collections must be scaffold-pruned when the
profile omits the collection, and every remaining consumer (the sitemap
endpoint) gates on
nvoos.json— an undefinedgetCollection()fails the build. - The WP static front page maps to profile
frontPage— rendered at/, skipped by the slug route + sitemap (one canonical URL). - Astro ignores underscore-prefixed files in
src/pages/(the partial convention) —_redirects.tsnever becomes a route. Emit/redirects.jsonand materializedist/_redirectspost-build. - Token rewrites must keep their FORGE markers and cover every token
the template block defines (layout tokens included) — see the
/* FORGE:tokens */span contract below. inferSizeis required on Astro Image for remote sources —widths/sizesalone fail the build withmissing-image-dimension. The domain allowlist mirrors the content layer's env-first rule, and SmartImage degrades to a plain<img>for non-allowlisted hosts (a CDN image must never fail a build).process.exit()races undici teardown on Windows (0xC0000409) — gracefulexitCode+ natural drain in the CLI.- The scaffold CLI is operator-run and has no rate limiting — rate limiting/retry belongs on the future MCP surface, not the CLI.
forge.mjsis plain JS, never TS syntax —.mjsgets NO type stripping:import typeand inlinetypespecifiers are syntax errors there (strip them; the runtime DEPLOY_MODES check carries the constraint).- Node strip-only mode bans parameter properties —
constructor(private readonly x)fails withERR_UNSUPPORTED_TYPESCRIPT_SYNTAXat CLI runtime even though vitest (esbuild) accepts it. Declare the field explicitly and assign in the constructor. Also banned: enums/namespaces; inlinetypespecifiers in named imports need a separateimport typestatement. - Secret grep gates must not match empty assignments —
PATTERN=[^ ]+matches the newline after an empty=(and the quote in the scaffold's own emission). Use value-shaped classes ([^[:space:]"']{4,}in GNU grep,[^'"\n]{4,}in JS) so.env.exampleplaceholders and scaffold.ts stay clean while real values still trip. - SDK v2 registerTool + zod 4.6 (cost a full session 2026-10-31): the
SDK's deprecated raw-shape overload (
ZodRawShape = Record<string, z.ZodType>) expects a zod classic surface that zod 4.6 no longer provides — raw shapes fail with$ZodType-missing-_deferrors. The non-deprecated overload accepts a zod INSTANCE typed as the SDK's exported structuralStandardSchemaWithJSON; type the registry field as that, never asz.ZodTypeAny/raw shapes. TheToolCallbackgeneric is a conditional type plain function expressions cannot satisfy even when the return type is provably assignable (probe it!) — register through a cast to an exact(name, config, cb) => unknownsignature. And the SDK imports types fromzod/v4— keep the mcp files importingzfrom'zod/v4'so type identities line up.
Template contract (FORGE markers)
templates/blog/src/content.config.ts carries line-anchored markers:
// FORGE:site … // FORGE:collections-end. scaffold() replaces that span
with env-driven siteUrl (import.meta.env.WP_SITE_URL ?? '<profile url>'),
the auth block, one WpCollection literal per profile collection, and the
export const collections block. Everything outside the span (imports,
defineCollection wiring) belongs to the template author. nvoos.json
(manifest v1: generator/url/generatedAt/collections) marks a forge project —
it is both the idempotency marker and the clobber guard.
Customer design-language notes (feed into the forge)
From the 098 pipeline for the pilot customer (a WooCommerce storefront):
- Extracted design language: accent
#720eec(Woo--wc-primary), surface#ffffff, body text#7a7a7a, type MuseoSansRounded/Oswald — theme namepilot. WCAG flag: body#7a7a7aon white = 4.29:1 (fails AA); the customer's unassigned pool has#555555(7.8:1) as the one-token fix — a ready upsell during the rebuild. - Forge today: posts/pages collections can be forged live against the
customer's site. Woo products need Phase 1's
wc/v3collection. - Keep the customer's name and URL out of skills and generated code — the design tokens alone are the deliverable (the 098 recreation loop is concept-adaptation, never cloning).
Phase roadmap (096 §5)
- Phase 0 — shipped (PR #7335, merged 2026-10-31): client, loader, sanitizer, scaffold CLI, blog template, hermetic fixture, CI exit gate.
- Phase 1a — shipped 2026-10-31 (PR #7337): Woo collections (
kind: 'product', wc/v3 + wp/v2 shapes, wc-consumer auth env), storefront template pages (/products/+ detail, Product JSON-LD), static front page (frontPage), per-collectionexclude,optionalsources, scaffold page pruning, brand name fromnvoos.json. Live pilot forge: 49 pages from production. - Phase 1b — shipped 2026-10-31: MIT license carve-out (owner decision
Q7, mirrors 092 Q8 — NOTICE ships into generated projects),
tokens_apply(098 design language over the--forge-*markers + WCAG contrast gate warnings), SEO suite (/feed.xml,/404.html,/redirects.json→dist/_redirectspost-build). - Phase 1c — shipped 2026-10-31:
site_analyze(forge.mjs analyze <url> [--write site.json]— types/counts/front-page/junk/Woo/SEO/lockdown probes, wc/v3-auth count fallback; live-verified against the pilot — all ten junk pages found automatically) + the media pipeline (SmartImage: Astro Image withinferSizefor same-host images, CDN passthrough). - Phase 1d — shipped 2026-10-31: Pagefind search (post-build index,
lazy-loaded
/search/shell) + thesource: 'payload'collection variant (nvoos-commerce/v1 envelope — body pagination,oldPricesale semantics, slugify for missing slugs; discovery offers the payload candidate when the namespace is advertised and no product CPT exists). - Phase 2 — shipped 2026-10-31: the standalone companion plugin
plugins/nvoos-forge-bridge(Q4 naming amendment —nvoos-astro-bridgewas taken by the velocity bridge addon): signed, debounced rebuild webhook (X-Forge-Signature,send_pending()/reset_pending()seams), inbound signature-verified/rebuildroute, single-use hashed preview tokens, redacted health, settings page with write-only secrets, uninstall.php, own phpcs + PHPUnit (24/24 on WP latest in Docker; the payload now also carries a self-verifyingtime/nonce/signatureproof —payload_signature()) +.github/workflows/phpunit-forge-bridge.ymlwith the PCP gate,.distignore, and.wordpress-org/listing assets. - Phase 3 — shipped 2026-10-31: vault + multi-site + GitHub deploy +
generated pipeline (see
-implementation-plan.md§2.7):forge vault/forge site/forge deploycommands; repos private by default; per-invocation auth (never persisted, masked); the emittedverify-dispatch.mjsis the CI-side signature verification — the bridge payload proof exists because theX-Forge-Signatureheader does not survive GitHub's dispatch delivery. Publish:@nvdigitalsolutions/nvoos- astro-forge, tagnvoos-astro-forge-v*→publish-astro-forge.yml(provenance). Secret grep gates in CI + the e2e credential-hygiene walk. - Phase 4 — core shipped 2026-10-31:
forge wizard(one command, URL → built + deploy-ready) and the/mcpsurface (see §2.8): 29-tool v1 catalog (incl. the §3.3 long-tail slices — content_audit/preview, redirects_generate, media_audit, collections_generate, site_update, preview_token, webhook_verify) over the single-source registry,NVOOS_FORGE_MODE=router|full(3 meta-tools vs full), dual-era Streamable HTTP (forge-mcp.mjs) + stdio (nvoos-forge-mcp-stdio), rate limit + Host/Origin + fail-closed write auth, SSRF-guarded fetches. Remaining: registry publication (owner) and the Playground demo blueprint.
Repo conventions
- PRs against
alpha-working, owner-merge; explicit-path staging (git add <file>, never-A) — seemcp-ai-wpoos-git-ops. - Adding a skill updates the inventories:
AGENTS.md(2 spots) +.github/copilot-instructions.md(2 spots), bumping the skill count. - Docs: the research file §0 carries the verified build learnings; the implementation plan §2.1 carries Phase 0 status vs plan.
References
examples/nvoos-astro-forge/README.md(layout, quick start, invariants)- Proposal 096 trio (
docs/project/proposals/096-*) · Proposal 098 (design language import — the theme input) · Proposal 092/095 (the storefront + ADS the template borrows from) - Related skills:
mcp-ai-wpoos-astro-storefront(the serving side — bridge, /mcp, tokens),mcp-ai-wpoos-playground-demos(the Phase 2 Playground fixture replacement),mcp-ai-wpoos-git-ops.