senternet-create-site
Orchestrate the full Senternet site build by running foundation, favicon, SEO, analytics, CSP, prerender, image, and performance skills in order.
SKILL.md.claude/skills/senternet-create-site/SKILL.md
GitHubCreate a Complete Optimized Marketing Site
Spin up or upfit a fully optimized marketing site by executing all site skills in sequence.
Mode Detection
Before asking anything, check whether the user provided a path to an existing directory:
- Existing directory — run in upfit mode: navigate to that directory and detect what's already implemented before each step. Skip steps that are complete, patch steps that are partial.
- No directory provided — run in create mode: ask the intake questions below and build from scratch. If the user does not have a design zip, directory, or HTML export, default to a barebones Hello World site using the requested project and directory names.
Framework Track
Every site runs on one framework track, held in $FRAMEWORK for the whole run: vite (Vite + React on Firebase Hosting) or nextjs (Next.js App Router on Firebase App Hosting). Read /senternet-site-framework for the decision guidance and the full convention map — it is the source of truth this orchestrator branches on.
- Upfit mode — detect the track before asking anything, using the detection rules in
/senternet-site-framework(.site-framework.jsonfirst, thennext.config.*/app/layout.tsx, thenvite.config.*). Never ask the user which framework an existing repo uses when the repo already answers it. If nothing is detectable, or both frameworks are present with no marker file, ask. - Create mode — ask intake question 0 below.
Once $FRAMEWORK is known, it applies to every step. Where a step below lists a Next.js variant, take that path when $FRAMEWORK is nextjs; otherwise take the Vite path. Where a step lists no variant, it is identical on both tracks.
If the user asks for a framework other than these two, follow the "Other frameworks" section of /senternet-site-framework: offer the closest supported track, and if they still want the other framework, translate each step through the convention map and tell them which steps were translated rather than executed verbatim.
Upfit Feature Inventory
In upfit mode, surface a visible feature inventory before the optional phases so the user can see what is already enabled and what is still available to add.
- For each optional capability, detect whether the repo already has it.
- Report each one as either
enabledoravailable. - If one or more optional capabilities are
available, present them to the user as a single enablement menu instead of separate yes/no prompts. - Only offer options for capabilities that are not already enabled.
Optional capabilities to inventory in upfit mode:
- Transactional email via Resend
- Reddit pixel for ad campaigns
- Spanish
/es/multilingual support - Ad landing pages for paid campaigns
- SEO blog
- Competitor comparison / alternative pages
- reCAPTCHA Enterprise protection for forms
Intake Questions
If creating new, ask:
0. Framework — Vite + React (default) or Next.js (App Router). Present it as a real choice with the tradeoff in one line each:
- Vite + React on Firebase Hosting (recommended for marketing sites) — static files on a CDN, no cold starts, cheapest to run, and every skill in this suite targets it first.
- Next.js on Firebase App Hosting — server rendering, route handlers instead of Cloud Functions, middleware and nonce-based CSP, on-demand regeneration. Costs cold starts and a heavier build.
If the user has no opinion, take Vite. If they name a different framework, follow the "Other frameworks" guidance in /senternet-site-framework. Record the answer as $FRAMEWORK.
- Site name (e.g.
myapp-site) — used as the directory name - App/product name (e.g.
MyApp) — used in copy, meta tags, schema.org - Domain (e.g.
myapp.com) — used in canonical URLs, sitemap, IndexNow, and Firebase Hosting domain setup; default canonical host iswww.myapp.comunless they explicitly want apex canonical - Design export — path to the zip file, directory, or HTML file exported from Claude Design (or another design tool). If they do not have one, skip this input and create a barebones Hello World site instead.
- App Store URL (if iOS app, e.g.
https://apps.apple.com/app/...) — used in CTA links - Twitter/X handle (e.g.
@MyAppHQ) — used in Twitter Card meta tags - Multilingual? (yes/no) — whether to add Spanish (
/es/) support - Local dev port — default
3000. Ask which port to use, and explain they may want a different one if they run multiple sites locally (e.g. 3001, 3002, 3003). Pass the answer to the scaffold skill for the chosen track as$PORT.
If upfitting an existing directory, ask only what's missing or cannot be detected:
- Detect
$FRAMEWORKfirst (see Framework Track above), then read the files that track actually uses. - Read
package.json,.firebaserc,.firebase-domain.json, and the env files to infer app name, domain, canonical host, and analytics IDs before asking. - On the Vite track also read
firebase.json,index.html, and.env.production. On the Next.js track readapphosting.yaml,next.config.*,app/layout.tsx, andconfig/routes.mjsinstead. - Only ask for values that can't be found in the project files.
Detection Rules (apply before every step)
Before executing each step, run the detection check listed for that step:
- Already implemented → skip the step; print a one-line note (e.g.
✓ firebase.json exists — skipping Firebase setup). - Partially implemented → patch only what's missing; note what was added.
- Not present → execute the full step.
Prerequisite: GitHub Setup (optional)
Detection:
.git/directory exists → skipgit init; check remote withgit remote -v.gitignoreexists → update it to include recommended entries (env files, credentials, build output, editor dirs, OS files) without removing existing entries- GitHub remote is already set → skip remote creation
Execute /senternet-site-github-setup for any missing pieces.
Ask: "Do you want to set up a git repo and GitHub remote now?" only if .git/ does not exist. If it does, skip the question entirely.
In upfit mode, include GitHub setup in the same feature inventory so the user can see whether the repo and remote are already enabled or still available to add.
Prerequisite: Google Cloud & Firebase Authentication
Detection:
- Run
gcloud auth list— if an active account is shown, skip gcloud auth - If
gcloud auth listfails or shows no credentialed account in this shell, do not stop there: rungcloud auth loginwith the standard scopes, then re-rungcloud auth listin the same shell before falling back to full auth - If the machine is headless or browser launch fails, add
--no-browserso gcloud prints device-flow instructions, then wait for the user to complete the browser/device step and re-rungcloud auth list - Run
firebase projects:list— if it succeeds, skip firebase login - If
firebase projects:listfails because the Firebase session expired or no active Firebase account is available, runfirebase login --reauthfirst, then re-runfirebase projects:listbefore falling back to full auth - Auth checks are account-scoped; all project-specific
gcloudandfirebasecommands later in the workflow must include--project "$PROJECT_ID"(or the current$PREFIX-dev/$PREFIX-prodproject).
Execute /senternet-site-gcloud-auth for any missing auth.
Phase 1: Project Foundation
Phase 1 is mandatory for every new site. Do not consider the site created until all three foundation pieces are in place:
- The framework scaffold for
$FRAMEWORK - Design system/components
- Firebase config and deploy wiring for the track's host
Step 1: Framework Scaffold
Run the scaffold skill for the chosen track:
$FRAMEWORK |
Skill | Host wired in Step 3 |
|---|---|---|
vite |
/senternet-site-vite-setup |
Firebase Hosting |
nextjs |
/senternet-site-nextjs-setup |
Firebase App Hosting |
Whichever runs must leave .site-framework.json in the site root before Phase 1 ends. Every later step reads it, so a missing marker file means every subsequent detection has to re-derive the track.
Vite track detection
package.jsonwithviteandreactin dependencies → skip scaffoldingvite.config.tsexists → check forhtmlPlugin,outDir: 'build',manualChunks; patch only missing piecessrc/main.tsxexists withhydrateRoot→ skip; without it → patchsrc/App.tsxrenders aScrollToTopcomponent (auseLocation+useEffect(() => window.scrollTo(0, 0), [pathname])helper) inside<BrowserRouter>→ skip; without it → patch it in. This is a consistent bug on existing sites: React Router preserves scroll position across navigation, so clicking a link while scrolled down lands the user partway down the next page. Always verify it is present and wired inside the router..env.developmentand.env.productionexist → skip env file creation
Execute /senternet-site-vite-setup for any missing pieces.
Next.js track detection
package.jsonwithnextin dependencies andapp/layout.tsxpresent → skip scaffoldingnext.config.tsexists → check forheaders(),poweredByHeader: false, and the absence ofoutput: 'export'; patch only missing piecesapphosting.yamlexists at the repo root → check thatNEXT_PUBLIC_BASE_URLis listed withBUILDavailability; patch if notconfig/routes.mjsexists → skip the route manifest; verify it covers every directory underapp/that has apage.tsxapp/sitemap.tsandapp/robots.tsexist → skip (Steps 6 and 7 below are already satisfied on this track).env.developmentand.env.productionexist → skip env file creationhydrateRoot, anapp-readydispatch, aScrollToTopcomponent, orreact-router-domfound in a Next.js repo → these are Vite-track artifacts that do not belong here; flag them rather than adding more
Execute /senternet-site-nextjs-setup for any missing pieces.
Both tracks
Pass the local dev port from the intake questions to the scaffold skill as $PORT (default 3000). On upfit runs, if package.json's dev script and the env files already pin a port, reuse that value instead of re-prompting.
Step 2: Site Design
Detection:
src/styles/design-system.css(Vite) orapp/globals.csscarrying the design tokens (Next.js) exists → skip design extractionsrc/pages/+src/components/(Vite) orapp/+components/(Next.js) contain React components → skip component conversion- Ask about design export only if components are absent and the user actually has a design export
On the Next.js track, converted components land in app/<segment>/page.tsx and components/, and only the leaves that need state or event handlers get 'use client'. Do not mark whole pages as client components.
No design export fallback:
- If the user does not have a design zip, directory, or HTML export, do not invoke
/senternet-site-design - Create a minimal Hello World home page using the requested site name and app/product name
- Keep the site intentionally barebones; the goal is a working scaffold rather than an extracted visual design
Execute /senternet-site-design if components are missing or the user wants to re-import a new design.
Step 3: Firebase Hosting
This step is required for every site, including brand-new creates. Never skip it just because the app already runs locally.
Detection (both tracks):
.firebasercexists → check for dev/prod project entries; add missing entries.firebase-domain.jsonexists withstatus: connectedand the expected apex/canonical pair → skip the custom domain handoff.firebase-domain.jsonexists withstatus: pending-dns→ keep moving on later phases and revisit the domain verification after other Firebase-safe steps finishdeploy:prodscript inpackage.json→ skip script addition
Vite track:
firebase.jsonexists → check for caching headers, security headers,cleanUrls; patch missing config keys
Next.js track:
apphosting.yamlexists → checkrunConfigand theenventries; patch missing keys. In a monorepo it belongs in the app directory, not the repo root.firebase apphosting:backends:list --project $PREFIX-prodreturns a backend for this repo → skip backend creation- A GitHub remote must exist before the backend can be created, because App Hosting deploys from a connected branch. If there isn't one, run the GitHub prerequisite first instead of deferring it.
- Root
package.jsonhasworkspacesbut the repo has noturbo.jsonornx.json→ the build will fail. App Hosting needs a supported monorepo tool (Turborepo or Nx) on top of workspaces; workspaces alone are not enough. Add Turborepo per step 6b of/senternet-site-nextjs-setupbefore creating the backend, and set the backend's root directory to the app's path. firebase.jsonon this track carries theapphostingblock (backendId,rootDir) for CLI deploys — but never hosting headers, which belong innext.config.ts
Execute /senternet-site-firebase for any missing pieces, including the custom-domain handoff when .firebase-domain.json is absent or still pending. Follow its Next.js track / App Hosting section when $FRAMEWORK is nextjs.
After this step, verify these files exist and are correct before moving on:
firebase.json(Vite) orapphosting.yaml(Next.js).firebaserc.firebase-domain.jsonif the canonical domain was connectedpackage.jsonwithdeploy:prodwired to the track's deploy command
Phase 2: SEO Foundation
Step 4: Favicon Assets
Detection:
public/favicon.svg,public/favicon.png,public/favicon.ico, andpublic/apple-touch-icon.pngall exist → skip icon generationindex.htmlorsrc/components/MetaTags.tsxalready links the favicon files → skip head tag patch- Next.js:
app/icon.png,app/apple-icon.png, andapp/favicon.icoexist → skip. These file conventions auto-emit the<link>tags, so no head patch is needed or wanted.
Execute /senternet-site-favicon for any missing pieces.
Step 5: Meta Tags
Detection:
index.htmlcontainsog:title→ OG tags present; check for Twitter Card, PWA, canonical, favicon — add only what's missingsrc/components/MetaTags.tsxexists → skip component creation- Schema.org
<script type="application/ld+json">inindex.html→ skip structured data
Next.js detection instead:
app/layout.tsxexportsmetadatawithmetadataBase,openGraph, andtwitter→ base tags present- Every
page.tsxexports its ownmetadata(orgenerateMetadata) with a unique title, description, andalternates.canonical→ skip per-page work; patch any page missing one - A JSON-LD
<script>in the layout or home page → skip structured data - Do not create
src/components/MetaTags.tsxon this track — a client component that mutatesdocument.headfights the Metadata API and loses on the first server render
Execute /senternet-site-metatags for any missing pieces.
Step 6: robots.txt
Detection:
public/robots.txtexists → skip; verify it points to the sitemap — update if not- Next.js:
app/robots.tsexists and returns the sitemap URL on the canonical host → skip. Do not also shippublic/robots.txt; two sources for the same path is a silent conflict.
Execute /senternet-site-robots if missing or malformed.
Step 7: Sitemap
Detection:
scripts/generate-sitemap.mjsexists → skip script creation; verify routes include/,/privacy,/termspublic/sitemap.xmlexists → regenerate only if routes have changedbuild:prodscript callsgenerate-sitemap→ skip wiring
Next.js detection instead:
app/sitemap.tsexists and readsconfig/routes.mjs→ skipconfig/routes.mjscovers everypage.tsxunderapp/, with legal pages markedindexable: false→ skip- There is no generate step to wire into
build:prodon this track, and nopublic/sitemap.xmlto commit
Execute /senternet-site-sitemap for any missing pieces.
Step 8: IndexNow
Detection:
- Any
public/*.txtfile matching a 32-char hex key → IndexNow key already exists; skip key generation scripts/indexnow.mjsexists → skip script creationdeploy:prodscript callsindexnow→ skip wiring- Next.js: the script reads the URL list from
config/routes.mjsrather than parsingpublic/sitemap.xml, and runs after the rollout completes rather than afterfirebase deploy
Execute /senternet-site-indexnow for any missing pieces.
Phase 3: Analytics, Email & Tracking
Step 9: Analytics Setup (Google Analytics or PostHog)
Provider selection:
- If the user has already picked a provider, go straight to it:
/senternet-site-google-analytics(GA4) or/senternet-site-posthog. - If the provider is undecided, run
/senternet-site-analyticsto choose, then continue with the selected skill. - Detecting an existing provider: a non-empty
VITE_GA_ID(or<!-- GA_START -->marker) means GA4 is already wired; a non-emptyVITE_POSTHOG_KEYorsrc/lib/analytics.tswith a PostHog init means PostHog is already wired. Skip selection when one is already present.
The detection and execution rules below apply to the GA4 path. For the PostHog path, follow /senternet-site-posthog and skip the Firebase web-app / Measurement ID steps.
Env var naming: analytics IDs are VITE_* on the Vite track and NEXT_PUBLIC_* on the Next.js track. On the Next.js track, every one of them must also appear in apphosting.yaml with BUILD availability, or it will be undefined in the deployed client bundle even though .env.production has it.
GA4 detection:
index.htmlcontains<!-- GA_START -->comment marker → GA block already present.env.productioncontains a non-emptyVITE_GA_ID=G-...value → skip env var setupvite.config.tshashtmlPluginhandling GA injection → skip plugin verification- Next.js: a
<Script src="https://www.googletagmanager.com/gtag/js...">(fromnext/script) inapp/layout.tsxand a non-emptyNEXT_PUBLIC_GA_IDin both.env.productionandapphosting.yaml→ skip. There is nohtmlPluginand no GA comment marker on this track, and no prerender strip step is needed. - The Firebase project already has a web app configured, the Hosting site association is complete, and
.env.productionalready has a non-empty GA ID → skip Firebase web-app setup - The Firebase project still needs a web app or Hosting association → run
/senternet-site-firebaseand follow its automated Firebase CLI flow
Execution rule (GA4 path):
- Resolve the production Firebase project ID first if you need it for the manual instructions, but do not use it to call
analyticsDetailsoraddGoogleAnalytics. - If the Firebase project still needs the web app or Hosting association, run
/senternet-site-firebasefirst. - Once the web app is created and the config is captured, either accept an existing Measurement ID or continue with the existing Measurement ID retrieval / env wiring step.
- Do not stop at the front-end GA markers alone. The step is not complete until the Firebase project has a web app, the Hosting association is in place, and
.env.productionhas the Measurement ID.
Step 9b: GDPR cookie consent for GA (recommended when GA4 is enabled)
Detection:
- A cookie consent module / banner exists (e.g.
cookie-consent,CookieConsentBanner,lib/cookie-consent.ts) that gates gtag until accept → skip - Geo endpoint exists (
/api/geoor equivalent) returningconsentRequired→ skip wiring; verify EU/GB set and GA inject path - GA still loads from a static
index.htmlblock or unconditionalnext/scriptwith no consent check → run the skill to gate it
When GA4 is present (or just wired in Step 9), execute /senternet-site-gdpr unless the user explicitly declines (e.g. Ahrefs-only / cookieless analytics, or EEA traffic is out of scope). Skip when the only analytics is cookieless.
Step 9c: CCPA opt-out for California (recommended when GA4 is enabled)
Detection:
- Consent module already understands a
ccparegime (or California region) and disables gtag on reject → skip - Geo endpoint returns state/region and a
regimeofgdpr|ccpa|none→ verify CCPA path; patch if only GDPR exists - GA loads with no way for a California visitor to stop it → run the skill
When GA4 is present, execute /senternet-site-ccpa unless the user explicitly declines (no CA traffic, or cookieless analytics only). Prefer one shared consent module with /senternet-site-gdpr rather than two banners. CCPA is opt-out (GA may run until Reject); do not reuse GDPR's "block until Allow all" rule for California.
Step 10: Transactional Email via Resend (optional — for forms, notifications, or onboarding)
Detection:
functions/src/index.tscontainsRESEND_API_KEYorsendResendEmail→ skipfirebase.jsonhas afunctionsblock and rootpackage.jsondeploy script already includeshosting,functions→ skip wiring- Next.js: the send path is a route handler at
app/api/<name>/route.ts, not a Cloud Function, andRESEND_API_KEYis asecret:ref inapphosting.yamlwithRUNTIMEavailability. Detect the route handler and the secret ref instead. Do not add afunctions/directory to a Next.js site just to send an email.
In upfit mode, include transactional email in the optional-feature menu if it is not already detected. If the user selects it, execute /senternet-site-email-resend.
Step 10b: Stripe Payments (optional — for selling a license, subscription, or product)
Detection:
functions/src/*referencesSTRIPE_SECRET_KEYorstripe.checkout→ skipfirebase.jsonrewrites include a Stripe function (e.g.createCheckoutSession) → skip wiring- Next.js: checkout and webhook live at
app/api/checkout/route.tsandapp/api/stripe-webhook/route.ts;STRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRETaresecret:refs inapphosting.yaml. The webhook handler must read the raw request body (await req.text()) before verifying the signature — parsing it as JSON first breaks verification.
In upfit mode, include "Payments via Stripe" in the optional-feature menu if it is not already detected. If the user selects it, execute /senternet-site-stripe. Receipt-email and signed-download-URL fulfillment depend on /senternet-site-email-resend, so run that first (or alongside) when the user chooses those.
Step 11: Reddit Pixel (optional — only if running Reddit ads)
Detection:
src/components/RedditPixel.tsxexists → skipindex.htmlcontains Reddit pixel bootstrap stub → skipVITE_REDDIT_PIXEL_IDin.env.production→ skip
In upfit mode, include Reddit pixel in the optional-feature menu if it is not already detected. If the user selects it, execute /senternet-site-ads-reddit-pixel.
Phase 4: Build Pipeline
Step 12: Prerendering
Vite track only. Skip this step entirely when $FRAMEWORK is nextjs — Next.js emits static HTML for every route it can, so a Puppeteer pass over the built output adds build time and a second, divergent copy of every page. Never install Puppeteer or create scripts/prerender.mjs on the Next.js track.
Detection:
scripts/prerender.mjsexists → skip script creation; verify ROUTES list covers current routesbuild:prodandbuild:devscripts callprerender→ skip wiring
Execute /senternet-site-prerender for any missing pieces.
Next.js equivalent check — do this instead, and treat a failure here the way you would treat a ✗ EMPTY prerender result:
npm run buildand read the route table it prints. Every marketing route should be marked static (○) or ISR, not dynamic (ƒ). A page that went dynamic by accident (usually acookies()/headers()/searchParamsread, or'use client'too high in the tree) is rendering per request and paying a cold start for content that never changes.npm start, thencurl -s localhost:$PORT/<route>for each route and confirm the response body contains the page's real headline and meta tags. This is exactly the crawler's view.
Phase 5: Images
Step 13: WebP Conversion
Detection:
scripts/convert-images.mjsexists → skipconvert-imagesnpm script inpackage.json→ skippublic/images/directory exists → skip directory creation- Next.js:
next/imagealready emits WebP/AVIF and responsivesrcsetat request time, so the conversion script and the hand-written<picture>pattern are redundant. Instead verify every content image usesnext/imagewith explicitwidth/height(orfillplussizes), and that the LCP image setspriority. Only keep a conversion script for images referenced outside the framework (share images, favicons, CSS backgrounds).
Execute /senternet-site-image-webp for any missing pieces.
Step 14: Share Images
Detection:
scripts/generate-share-images.mjsexists → skippublic/share/home.pngexists → skip generationindex.htmlog:imagepoints to/share/home.png→ skip update- Next.js: the generated PNGs still land in
public/share/; the reference moves toopenGraph.imagesin each page'smetadata(resolved againstmetadataBase) instead ofindex.html
Execute /senternet-site-share-images for any missing pieces.
Phase 6: Performance
This phase is mandatory for every site. Do not mark the site complete until Lighthouse optimization has been executed against a locally served production build, the reported regressions have been addressed, and the site is retested.
Step 15: Lighthouse Optimization
Detection:
vite.config.tshasmodulepreloadinjection andmanualChunks→ still run/senternet-site-lighthouseagainst the local production build to verify actual performance, image, caching, and LCP discovery behaviorindex.htmlhas<link rel="modulepreload">→ still run/senternet-site-lighthouselocally- CSS has
@media (prefers-reduced-motion)or mobile animation disabling → still run/senternet-site-lighthouselocally
Execute /senternet-site-lighthouse unconditionally in this phase against a locally served build/ output, then fix any reported Lighthouse issues before proceeding.
Next.js: run it against npm run build && npm start instead of a served build/ directory. Chunking, modulepreload, and font optimization are the framework's job here, so the fixes that matter are different: shrink the client-component boundary, add priority to the LCP image, use next/font rather than a <link> to a font CDN, and check the build's route table for pages that went dynamic. The score targets are the same.
Step 16: Mobile Optimization
Detection:
public/images/contains a-sm.webphero variant → skip mobile hero creationindex.htmlhas<link rel="preload" media="...">responsive preload → skip- Images have
loading="lazy"where appropriate → skip
Execute /senternet-site-mobile-optimize for any missing pieces.
Step 17: Mobile Navigation
Detection:
src/components/Layout.tsxhashidden md:flexon the desktop nav and anmd:hiddenhamburger button → skip- Layout imports
useStateand toggles amobile-menupanel → skip - Header nav is desktop-only with no responsive collapse → run
Execute /senternet-site-mobile-nav for any missing pieces.
Phase 7: Optional Features
For desktop-only app sites, execute /senternet-site-download-handoff to give mobile visitors email, share, copy, and download-anyway options. Reuse an existing suitable download/install destination and transactional email infrastructure.
Step 18: Multilingual (if requested)
Detection:
src/i18n.tsexists → already multilingual; skipsrc/App.tsxcontainsLanguageProvider→ skip- Prerender script contains
/es/routes → skip - Next.js: an
app/[locale]/segment (withgenerateStaticParamsreturning the locale list) plus middleware-based locale routing → skip.hreflangcomes fromalternates.languagesin each page'smetadatarather than from injected<link>tags.
If not present, include Spanish (/es/) support in the optional-feature menu in upfit mode. Execute /senternet-site-multilingual if the user selects it.
Step 19: Ad Landing Pages
Detection:
src/components/LandingPage.tsxexists → skip base component- A campaign-specific landing page route exists in
App.tsx→ skip - Next.js:
components/LandingPage.tsxplus anapp/lp/<campaign>/page.tsxper campaign. Campaign pages are usuallynoindex— setrobots: { index: false }in theirmetadataand mark themindexable: falseinconfig/routes.mjs.
In upfit mode, include ad landing pages in the optional-feature menu if they are not already detected. Execute /senternet-site-ads-landing if the user selects them.
Step 20: SEO Blog
Detection:
- A blog route and blog index component exist in
src/→ skip src/data/contains post data → skip- Next.js:
app/blog/page.tsx,app/blog/[slug]/page.tsxwithgenerateStaticParamsandgenerateMetadata, and post data undercontent/ordata/→ skip. Post URLs come from the samegenerateStaticParamslist that feedsconfig/routes.mjs, so the sitemap picks them up without a separate array.
In upfit mode, include the SEO blog in the optional-feature menu if it is not already detected. Execute /senternet-site-seo-blog if the user selects it.
Step 21: Compare Pages
Detection:
src/components/ComparePages.tsxexists → skip- Competitor/alternative routes exist in
App.tsx→ skip - Next.js:
app/vs/[competitor]/page.tsxandapp/alternatives/[competitor]/page.tsxwithgenerateStaticParams→ skip
In upfit mode, include competitor comparison / alternative pages in the optional-feature menu if they are not already detected. Execute /senternet-site-compare-pages if the user selects them.
Step 22: reCAPTCHA Enterprise for Forms
Detection:
gcloud recaptcha keys list --project "$PROJECT_ID"shows existing local/dev/prod form keys → skiprecaptchaenterprise.googleapis.comis already enabled → skip API enablement
In upfit mode, include reCAPTCHA Enterprise in the optional-feature menu if it is not already detected. Execute /senternet-recaptcha-enterprise if the user selects it.
Step 23: Content Security Policy
Run this after every third-party integration is wired (analytics, GDPR/CCPA consent, Resend, Stripe, Reddit pixel, reCAPTCHA) and before the verification build, so the policy is generated against the services the site actually loads. This step is not optional: every Senternet site ships a CSP.
Detection:
firebase.jsonalready sets aContent-Security-Policyheader → audit it against the services wired in this run, extend it for anything missing, and skip the rest- Next.js:
middleware.tsalready generates a per-request nonce and sets the CSP header → audit and extend only
Execute /senternet-site-csp. It audits which third-party origins are in use, moves inline scripts out of index.html (Vite) or wires the nonce through <Script> tags (Next.js), writes the header, and verifies it live.
Every service added after this point must have its origins added to the policy in the same change that adds the service. A new script that is not in the CSP silently fails in production.
Step 24: Verify everything works
Vite track:
npm run dev— dev server starts cleanlynpm run build:prod— builds, prerenders all routes (check for✗ EMPTYfailures)- (Optional — only if dev environment exists)
npm run deploy:dev— deploys to staging Firebase project - (Optional) Verify staging URL loads and meta tags are correct
- Fix any Lighthouse failures found on the local production build before first production deploy
- Load the local production build and confirm the console is free of CSP violations
npm run deploy:prod— deploys to production + runs IndexNow- (Optional, upfit only) If an existing deployed site and production URL are already detectable, run PageSpeed Insights against production after deploy to capture live CDN behavior
Next.js track:
npm run dev— dev server starts cleanlynpm run typecheck— cleannpm run build— no errors, and the route table shows the marketing routes as static (○), not dynamic (ƒ)npm start, thencurleach route and confirm real HTML and correct per-page meta come back- Confirm
/sitemap.xmllists only indexable routes and/robots.txtpoints at the canonical host - Fix any Lighthouse failures against the local production server before the first production rollout
- Deploy by pushing to the connected branch, or
npm run deploy:prodto force a rollout; watch it reachREADYbefore running IndexNow - Confirm the
Content-Security-Policyheader comes back on a deployed response and the console is free of CSP violations - Verify the deployed backend serves the same HTML the local production server did — a variable missing from
apphosting.yamlshows up here as an empty analytics ID or a wrong canonical host, and nowhere earlier
Step 25: Initialize Project Documentation
Detection:
AGENTS.mdexists → skip generation (do not overwrite existing project docs)CLAUDE.mdexists → skipREADME.mdexists → skip
If any are missing, execute /senternet-site-init for only the missing files. The generated docs must describe the track this site actually runs on — tech stack, build pipeline, deploy command, and the page-add rule all differ between tracks, and docs describing the wrong one are worse than no docs.
Page-add rule (enforce for every new page)
Vite track — the three-file rule. Every new page/route must update these files together; missing any one breaks crawlers or indexing:
src/App.tsx— add the<Route>scripts/prerender.mjs— add to ROUTES arraypublic/sitemap.xml(or regenerate viagenerate-sitemap.mjs) — indexable routes onlyscripts/generate-share-images.mjs— add a share image block and run it
Next.js track — the one-file rule. Routing, prerendering, and the sitemap all derive from two places:
app/<segment>/page.tsx— the page, with its ownexport const metadata(unique title, description,alternates.canonical,openGraph.images)config/routes.mjs— add the route,indexable: falsefor legal pagesscripts/generate-share-images.mjs— add a share image block and run it
app/sitemap.ts, app/robots.ts, and scripts/indexnow.mjs all read config/routes.mjs, so there is no third list to keep in sync and no prerender array to update.
Footer link organization (enforce as links accumulate)
The footer collects links from many skills (AI Disclosure, Other Projects, blog, compare pages, legal pages). Once it holds more than ~4–5 links, divide it into labeled category columns (e.g. Legal — Terms, Privacy, AI Disclosure; Product — Features, Pricing, Download; Company — About, Blog, Other Projects) rather than letting a single flat row grow unbounded. Each new footer link goes into the category it belongs to. See /senternet-site-design for the SiteFooter.tsx grouping convention. On upfit runs, if the footer already has many links in a flat list, migrate them into grouped columns.