A complete playbook for building a fast, SEO/AEO-optimized, multi-page marketing website using:
- Claude Design for visual design + React component generation
- GitHub for version control
- Cloudflare Pages for hosting (free, fast CDN)
- Custom build pipeline for pre-compilation + AEO optimization
- No-code tracking stack for analytics
Total annual cost: ~$15-30 (domain registration only). Everything else is free.
This SOP is the result of building versorix.com from scratch and then reusing the same stack on the Accu-Track and The InjectR sites. It includes the build bugs all three hit so you can skip them.
The stack is the template. The design is yours. Everything in Phases 2-9 (build pipeline, SEO/AEO, tracking, deploy) is meant to be copied exactly. It is shared plumbing and it is identical on every site. Phase 1, the visual design, is the opposite: it must come out different every single time. If your finished site shares the example's palette, layout, fonts, or motion, you built a clone, and a clone is worth nothing to the buyer who is paying for custom. The plumbing is invisible to the visitor. The design is the whole first impression. §4 is built around forcing that divergence.
§0. Hand this to Claude (or Claude Code)
For the AI reading this in a new chat: This document is an executable playbook. Read all 18 sections before doing anything. Then run the intake below, walk the user through Phase 0 (strategy + copy) before opening Claude Design, then execute Phases 1–9 in order. Pause at every phase gate. The trial-and-error is already done — don't improvise on the technical stack.
0.1 Kickoff prompt — paste into a new chat
If you're spinning up a new Claude or Claude Code session to build a site, upload this document and paste this:
You're going to help me build a production marketing website following this SOP
I've uploaded.
Read the document in full first, all 18 sections, before doing anything. Then
ask me the intake questions in §0.2. Don't start building anything until I've
answered all of them. After I've answered, walk me through Phase 0 (strategy +
copy) before we touch any tooling. Then execute Phases 1-9 in order, pausing at
each phase gate in §0.3 to confirm with me.
Don't deviate from the technical stack. The build bugs in §13 are already solved,
follow the documented solutions, don't re-derive them.
Phase 1 (design) is the one part that must come out DIFFERENT every time. Make me
do the Design DNA step in §4.1, hold me to the anti-clone gate in §4.8, and refuse
to ship a look-alike of this document's example site (yellow-on-black, centered
hero, wipe transitions). My site should look like it could only belong to my
business.
That's the entire kickoff. The document does the rest.
0.2 Intake — Claude Code asks the user
Don't proceed past this step until every row has an answer:
| Variable | What to ask | Example |
|---|---|---|
| Business name | Legal name + casual name | "Versorix Inc." / "Versorix" |
| Founder name + role | For Schema.org + About page | "Jane Doe, Founder" |
| Audience (specific) | Who buys. Push back on vague answers. | "GTA home services contractors, avg job $1K+" |
| Positioning | One sentence: "Helps X get Y without Z" | "Helps contractors get booked jobs without becoming marketers" |
| Pages | Default 6: home / about / [process page] / contact / privacy / terms. Name the process page after what THIS business does (/services, /work, /how-it-works). /method is the example's term, not a default. |
Ask if they need more (almost always: no) |
| Primary brand color | Hex + name | "#FFE100 (volt yellow)" |
| Secondary brand color | Hex + name | "#111110 (brand black)" |
| Domain | Already registered? | versorix.com |
| Email on domain? | Critical for DNS path decision in §7 | "yes, Microsoft 365" |
| Primary CTA | The ONE action every page leads to | "Book a 15-min strategy call" |
| Booking system | Calendar / form / external | "Calendly embed" or "custom form" |
| Tracking IDs | GT, GA4, Pixel, Clarity (can be set up post-launch) | Optional at start |
| Service area | For LocalBusiness schema | "List of cities you serve" |
| Phone | For schema + footer | "+1-XXX-XXX-XXXX" |
| Public email | Separate from founder email | "[email protected]" |
0.3 Phase gates — confirm before proceeding
After each phase, stop and confirm with the user. Don't blow through.
- Phase 0 — Strategy + copy locked. User signed off on positioning + per-page copy.
- Phase 1: Design DNA locked (§4.1) and anti-clone gate passed (§4.8). Claude Design pages exported and visually approved on desktop AND mobile. The finished design does not resemble the example site or any prior build.
- Phase 2 —
npm run buildruns clean. Compiled JS + updated HTML produced. - Phase 3 — Site live on the
*.pages.devURL. User has visited it. - Phase 4 — Custom domain working.
https://www.<domain>resolves with valid SSL. - Phase 5 — robots, sitemap, llms.txt, body-seo files, schema all in place. AEO score 90+/100.
- Phase 6 — Tracking stack firing on every page. Verified with Pixel Helper + Tag Assistant.
- Phase 7 — Images compressed, favicons generated, OG image done, security headers set.
- Phase 8 — GSC, GBP, Bing, GA4 conversions, Meta events all configured.
- Phase 9 — All audits pass. User verified on multiple devices.
0.4 If context resets mid-build
- Ask the user which phase gate was last completed
- Scan the conversation history for what was already done
- Resume — don't restart
0.5 Anti-patterns — refuse these even if the user asks
- Don't add Content-Security-Policy initially. Bug 2 in §13 explains why. Add it later, after auditing every script/style/connect endpoint.
- Don't pre-render via Playwright. Bug 1 in §13 explains why. The bundler's blob URLs die outside their origin.
- Don't migrate full DNS to Cloudflare if the user has email on the domain. Use the CNAME-only path in §7. Migration risk is real.
- Don't write the marketing copy yourself. That's Phase 0 and it's the user's job. Push back if they try to skip it.
- Don't suggest single-page architecture. Multi-page is the default for marketing sites. Real URLs matter.
- Don't skip §8.5 (hidden body SEO). It's the moat. Without it, the site scores ~67/100 on AEO. With it, 100/100.
- Don't run a build step on Cloudflare. Build locally, commit compiled output, push. No CI surprises.
- Don't default the design to a dark background with one neon accent. That is the generic AI look and the reason past builds came out identical. Force the Design DNA step in §4.1 instead.
- Don't reflexively add a loading screen plus a wipe page transition. They are one site's signature, not a requirement. Add them only if the user deliberately chose that motion personality.
- Don't reuse this document's example colors, fonts, or page names.
#FFE100, a black canvas, and a page literally named/methodbelong to the example business. Every build picks its own. - Don't ship a site that looks like the last one built from this SOP. Run the anti-clone gate in §4.8 before export. Same engine, different car, every time.
Table of contents
- Hand this to Claude Code
- What you're building
- Prerequisites
- Phase 0: Strategy + copy first
- Phase 1: Design in Claude Design
- Phase 2: Set up the repo + build pipeline
- Phase 3: Deploy to Cloudflare Pages
- Phase 4: DNS + custom domain
- Phase 5: SEO + AEO infrastructure
- Phase 6: Tracking stack
- Phase 7: Performance optimization
- Phase 8: Account-side setup (post-launch)
- Phase 9: Verification + monitoring
- Build bugs to avoid
- Ongoing workflow
- Cost breakdown
- Appendix A: File structure reference
- Appendix B: Decision tree for architectural choices
- Strategic lessons (not technical bugs)
1. What you're building
A production-grade marketing website with these characteristics:
| Property | What it gives you |
|---|---|
| Multi-page React | Real URLs per section (/, /about, /method, etc.), browser back/forward works, sitelinks possible |
| Pre-rendered HTML | Each page has substantive content in the markup before any JavaScript runs |
| Pre-compiled JSX → JS | No in-browser Babel runtime (~500KB saved per page, ~300ms boot time recovered) |
| Production React builds | ~30% smaller than dev builds |
| Hidden semantic SEO/AEO content | Per-page H1, H2 hierarchy, internal links, external citations for AI crawlers |
| Full structured data | Schema.org LocalBusiness + ProfessionalService + WebSite + FAQPage on every page |
| Complete tracking stack | Google Tag, GA4, Meta Pixel, Microsoft Clarity firing on every page |
| AI-crawler ready | llms.txt + llms-full.txt + Schema.org for ChatGPT browse / Perplexity / Claude |
| PWA-capable | Web manifest, theme color, full favicon set including Apple touch + Android Chrome |
| Auto-deploy | Push to GitHub → Cloudflare deploys in 30-60 seconds |
| Edge cached globally | <200ms page load anywhere in the world |
| DDoS protected | Cloudflare's free tier |
What this is NOT good for:
- Apps with user accounts, login, dashboards (use Next.js or similar)
- E-commerce with payment processing (use Shopify or Next.js + Stripe)
- Real-time features (chat, notifications) (use a proper backend)
- Sites with thousands of dynamic pages (use a real CMS like Sanity + Astro)
For solo operators, small agencies, and service businesses, this stack is genuinely optimal. Cheap, fast, owned.
2. Prerequisites
Accounts (all free)
- GitHub — https://github.com/signup
- Cloudflare — https://dash.cloudflare.com/sign-up
- Claude Design — https://claude.ai/design (Claude Pro required for higher limits)
- Google Account — for Search Console, Analytics, Tag Manager, Business Profile
- Meta Business — for Meta Pixel + Events Manager
- Microsoft Account — for Clarity + Bing Webmaster
Domain
You need a domain. Register one at any registrar:
- GoDaddy — easiest UI, slightly more expensive (~$15-30/year)
- Cloudflare Registrar — cheapest at cost (~$10/year for .com), bonus: integrates natively if you use Cloudflare DNS
- Porkbun / Namecheap — solid alternatives
Local tools
Install on your computer:
- Node.js 18+ — https://nodejs.org (LTS version)
- Git — https://git-scm.com
- VS Code (or any code editor) — https://code.visualstudio.com
- Python 3.10+ — only if you'll be running image optimization scripts
That's it. No paid software required.
3. Phase 0: Strategy + copy first
Do this before you touch any tech. Most websites fail because the copy is written after the design, not before.
3.1 Define the audience
Write down on paper (or in a Notion doc):
- Who is this site for? (Specific. "Small business owners" is not specific. "GTA home services contractors with average jobs over $1,000" is specific.)
- What problem are they searching to solve?
- What words would they actually type into Google?
- What's the ONE action you want them to take? (Book a call? Buy? Sign up?)
If you can't answer all four in one sentence each, do not proceed. The site won't convert.
3.2 Lock the positioning
Write your positioning statement:
"[Business name] helps [specific audience] [achieve outcome] without [common pain point]."
Example (Versorix): "Versorix helps GTA home services contractors get more booked jobs without becoming marketers themselves."
If your positioning shifts later, EVERY page on the site has to change. Lock this first.
3.3 Write the copy
For each page, write the copy in a plain text document before any design exists:
Home page:
- Headline (the ONE sentence that explains everything — should be punchy, 6-10 words)
- Sub-headline (1-2 sentences supporting the headline)
- 3-5 sections of body content
- One primary call-to-action ("Book a call", "Start free trial", etc.)
Other pages (typically 4-6 total):
- About — founder story, why this business exists
- Method/How it works — what you actually do
- Pricing or Services — what they're buying
- Contact — booking calendar or contact info
- Privacy + Terms — required legal pages
3.4 Plan the information architecture
Decide what pages exist BEFORE building. Standard set:
/— Home/about— About / founder story/method(or/how-it-worksor/services) — Your process/contact— Booking / contact/privacy— Privacy policy/terms— Terms of service
You can add more later (case studies, blog, etc.) but ship with these 6 first.
Name the process page after what the business actually does (/services, /how-it-works, /work, /pricing). /method is the example business's term for it because it has a named methodology. Most businesses don't. Don't paste the word method onto a 3-truck contractor.
3.5 Regulated verticals: a compliance pass before copy
If the business is in a regulated field (medical, aesthetics, financial, legal, health), run a compliance pass on the positioning and copy BEFORE design. The regulator governs what words are allowed on the page, and retrofitting compliance after the copy is locked means rewriting the whole site. This came out of The InjectR build (nurse-led medical aesthetics under Health Canada plus College of Nurses rules).
What to pin down:
- Banned claims and terms. Many regulators prohibit superlatives ("best," "guaranteed," "permanent"), and some prohibit naming prescription products in a promotional context at all (Health Canada bans direct-to-consumer prescription-drug advertising). List the banned words before writing a line.
- Where regulated terms ARE allowed. Often educational/FAQ context and the hidden body-SEO block (§8.5) can carry terms a promotional headline cannot. That is where search relevance lives without tripping the ad rules.
- Proof and testimonials. Before/after imagery usually needs a visible "individual results vary" line and documented consent. Health claims soften to wellness language with no disease-treatment promise.
- Ad creative is the strictest surface. Whatever the platform plus the regulator will not allow in a paid ad is the tightest constraint. Write to that bar.
Output: a short "compliance guardrails" block at the top of the Phase 0 doc that every downstream page and ad must obey. For non-regulated businesses, skip this subsection.
3.6 Output of Phase 0
A single document containing:
- Audience definition
- Positioning statement
- Per-page copy
- Page map / IA
That document is the SOURCE OF TRUTH for the entire build. Everything else is execution.
4. Phase 1: Design in Claude Design (build YOUR site, not a clone)
Claude Design generates React components from natural-language prompts. You'll use it to design every page. Read 4.0 before you prompt anything. It is the reason most sites built from earlier versions of this SOP came out looking the same.
The deeper design and copy thinking now lives in the Design & Copy Rulebook - Anti-Slop + Divergence Engine. Run its divergence engine to roll a Design DNA before you prompt, and use its anti-slop blocklists, generative menus, tooling playbook, and quality bar. This section is the build-side summary; §4.3 (motion), §4.4 (unique layouts), and §4.5 (component sources) defer to the rulebook for the full rules. Pair the rulebook with its companion website-build-ledger so the anti-clone gate can check that this build does not repeat the last one.
4.0 The contract: the stack is the template, the design is not
Everything in Phases 2 through 9 is meant to be copied verbatim. The build pipeline, the SEO/AEO infrastructure, the tracking stack, the deploy flow. That is shared plumbing. It is identical on every site and that is correct.
Phase 1 is the opposite. The visual design must come out different every single time. Same colors, same layout skeleton, same motion, same fonts as the last site means you built a clone, and a clone is worth nothing. The plumbing is invisible to the visitor. The design is the entire first impression.
So the rule for this phase: do not accept Claude Design's first instinct, and do not copy the example values in this document. The look an AI reaches for by default (dark background, one bright accent, centered hero, big sans-serif headline, fade-up-on-scroll) is the exact look everyone else gets. Your job in Phase 1 is to walk away from that default on purpose.
4.1 Build your Design DNA before you prompt anything
Lock six decisions before you open Claude Design. Write them in your Phase 0 doc. These six are what make your site unmistakably yours. Spend real time here. Twenty minutes of divergence now saves you from a generic site you will quietly hate in a month.
1. Mood / archetype. Pick ONE and commit. Do not blend three. Directions to react to (steal a direction, not the specifics):
- Editorial / print: big serif headlines, generous margins, magazine columns, restrained color. Reads expensive and calm.
- Brutalist: raw, high-contrast, visible structure, system fonts used on purpose, hard edges.
- Warm / organic: off-white canvas, earthy palette, rounded forms, soft grain, hand-feel.
- Technical / grid: visible baseline grid, monospace accents, data-dense, precise.
- Luxury minimal: huge whitespace, small type, one hero image doing all the work, slow motion.
- Retro / analog: muted print palettes, halftone, vintage type, texture.
- Maximalist: layered, loud, color-saturated, confident, lots happening.
- Soft / glass: light, blurred translucency, pastel depth, gentle.
Name your pick in one sentence: "Warm editorial, like a high-end renovation magazine, not a tech startup." That sentence goes straight into the prompt.
2. Color system. This is where clones are born. Do not default to black-plus-one-neon. Decide:
- Base canvas: dark, light, off-white, cream, deep navy, warm grey? Most distinctive sites are NOT pure black.
- One or two real brand colors (your actual brand, not the example in this doc).
- A neutral ramp (5 to 7 greys, ideally tinted toward your brand rather than pure grey). Pull palettes from Coolors (coolors.co), Adobe Color, or by eyedropping a photo, product, or physical thing the business already owns. Specify colors to Claude Design in hex or oklch. oklch is worth learning: "make the accent oklch(0.7 0.18 145)" gives you even, predictable control over lightness and saturation that hex fights you on.
3. Type system. Type carries most of the personality. A distinct display face on the headlines changes everything. Pair a display/headline face with a clean body face. Free, high-quality sources: Google Fonts and Fontshare. Name them in the prompt: "Headlines in Fraunces, body in Inter." Do not ship with the default system sans on everything. That is the AI house style and it reads generic instantly.
4. Layout skeleton. The default is a centered single column: hero, three feature cards, testimonial row, CTA. Everyone gets it. Choose something with more spine:
- Split screen (text one side, visual the other)
- Asymmetric (off-center hero, content hung off a grid line)
- Sidebar / fixed rail navigation
- Editorial columns with pull quotes
- Horizontal scroll section
- Oversized type as the hero itself (no image) Tell Claude Design the skeleton explicitly: "Asymmetric hero, headline hung left off a 12-column grid, hero visual bleeding off the right edge."
5. Motion personality. Decide the feel: calm and slow, kinetic and snappy, or physical and springy. This drives every animation choice in 4.3. Note: you do NOT have to add a full-screen loading screen and a wipe page transition. Those are one specific signature, not a requirement. Many of the best sites simply fade content in and move on.
6. One signature element. The single thing people remember. Pick ONE per site and make it different from the last: a distinctive cursor, a scroll-driven hero animation, a texture/grain overlay, an interactive piece (a slider, a calculator, a hover-reveal gallery), or an unusual type treatment. One. Not five.
Output of 4.1: a short "Design DNA" block in your Phase 0 doc covering all six. That block is what you paste into the design prompt below.
4.2 The prompt that produces a unique site
Don't ask Claude Design to "design a website." It will guess, and its guess is the generic look. Feed it your Design DNA and your copy:
Build a marketing website for [BUSINESS NAME], a [BUSINESS TYPE] serving [AUDIENCE].
POSITIONING: [your positioning statement]
PAGES: [list your actual pages, named after what this business does]
DESIGN DNA (follow this exactly, do not default to a generic look):
- Mood: [your one-sentence archetype, e.g. "warm editorial, like a high-end reno magazine"]
- Base canvas: [e.g. "warm off-white #FAF7F2, not pure white, never pure black"]
- Brand colors: [your hex/oklch values + names]
- Type: [display face] on headlines, [body face] on body
- Layout skeleton: [your chosen skeleton, described spatially]
- Motion personality: [calm / kinetic / physical] + one sentence
- Signature element: [the one memorable thing]
CONTENT:
[paste your Phase 0 copy here, organized by page]
DO NOT:
- Default to a dark background with a single neon accent. That is the generic AI look.
- Use a centered single-column hero with three feature cards unless I asked for it.
- Put a default system sans on everything. Use the faces named above.
- Add a full-screen loading screen or wipe page transition unless I asked for it.
TECHNICAL REQUIREMENTS (these never change, every site uses them):
- Multi-page architecture (separate HTML files, not single-page hash routing)
- Pre-rendered: content visible in HTML before JavaScript runs
- Mobile-first responsive
- Per-page meta descriptions and canonical URLs
- All animations no-op under prefers-reduced-motion
The DESIGN DNA and DO NOT blocks are what break the clone. The TECHNICAL REQUIREMENTS block is the part that stays identical for everyone.
Insist on these technical specifics too (push back hard if Claude Design resists, they are non-negotiable for production performance):
- Architecture: multi-page React, each page as its own HTML file, not single-page with hash routing.
- Pre-rendering: each HTML file should aim to contain the rendered DOM in the markup, not just a bare
<div id="root">. - Per-page metadata: each page gets its own
<title>, meta description, canonical URL, and OpenGraph tags in the<head>. - No in-browser Babel: compile JSX to JS at build time, not via
<script type="text/babel">runtime.
(Page transitions are a design choice, not a requirement. If your motion personality calls for them, ask for "a JS click interceptor for page transitions so they work across real document navigation." If it doesn't, skip them. Don't add them by reflex.)
4.3 How to get Claude Design to add animations (and keep them tasteful)
Claude Design can do far more motion than its default fade-up. You have to ask, and you have to ask specifically. "Add some animations" gets you generic. Name the technique AND the element.
Motion vocabulary that works:
- Scroll-triggered reveals: "as each section enters the viewport, animate it in." Specify the feel: "slow, 600ms, ease-out, slight upward drift" vs "snappy, 200ms."
- Staggered children: "stagger the feature cards in one after another, 80ms apart." This single trick reads as expensive.
- Parallax: "the hero image scrolls slower than the text in front of it." Use sparingly.
- Magnetic / springy buttons: "the primary CTA gently pulls toward the cursor on hover and springs back."
- Marquee: "an infinite horizontal marquee of [service words / place names] under the hero."
- Text effects: "the headline characters scramble into place on load," or "reveal each word of the headline on a stagger," or "wipe the headline in line by line behind a mask."
- SVG draw-on: "the logo / underline draws itself in like a pen stroke on load."
- Physical / inertia: "add momentum to the horizontal scroll," or "the cards tilt slightly to follow the cursor."
- Cursor: "a custom cursor that grows and inverts over interactive elements."
How to keep it tasteful (the part people skip):
- Pick a motion budget. Two or three signature moments per page, not motion on every element. Constant motion reads cheap and tires the eye.
- Match the motion personality from 4.1. Luxury-minimal = slow, heavy, few. Kinetic = fast, many, snappy. Mixing the two looks broken.
- Always require the reduced-motion no-op (it is in the prompt template). Motion-sensitive users and most accessibility audits expect it.
Iterating on motion is just plain language against the preview: "the hero entrance is too fast, slow it to ~700ms and make it heavier." "Drop the parallax, it's distracting." "Add a subtle stagger to the nav links on load." Claude Design adjusts in place.
4.4 How to get unique layouts and bespoke functionality
Two techniques.
Break the template explicitly. Claude Design defaults to safe, centered, symmetric. Push it: "make the layout asymmetric, hang the headline off the left grid line, let the hero visual bleed off the right edge." "Use a fixed left sidebar for navigation instead of a top bar." "Make the services section a horizontal scroll, not a vertical stack." "Use oversized type as the hero, no image at all." Name the unusual choice or you get the safe one.
Design from a reference, not from scratch. If you've seen a layout you like, screenshot it and hand it over: "I like the spatial structure of this layout, the way it splits the screen and uses the big number. Adapt that structure to my Design DNA and my content. Do NOT copy its colors, fonts, or brand, only the spatial idea." This is the single fastest route to a non-generic layout. Adapt the structure, never the brand (see 4.5 on sourcing).
Bespoke functionality. A marketing site is not limited to static sections. Ask for the interactive thing that fits the business:
- A before/after image slider (renovation, detailing, design, landscaping)
- A simple cost / ROI / savings calculator
- A filterable project gallery
- An interactive service selector that updates the CTA
- An animated step-by-step process, a comparison table, a FAQ accordion Describe the behavior and the inputs: "a before/after slider in the hero, drag the handle to wipe between the two images." Keep it to one or two interactive pieces. Functionality is a signature element (4.1), not a feature dump.
4.5 Pulling components from 21st.dev (and feeding them to Claude Design)
21st.dev (21st.dev) is a large library of polished, modern React + Tailwind UI components: heroes, navbars, pricing tables, marquees, animated backgrounds, bento grids, footers. It's one of the best places to grab a specific, well-built piece instead of describing it from scratch. Other useful sources: shadcn/ui (clean primitives), Aceternity UI and Magic UI (flashy animated components), Hover.dev.
How to use it WITHOUT creating another clone:
- Browse 21st.dev and find a component whose STRUCTURE you like (a particular animated hero, a bento grid, a marquee).
- Grab it. Most components have a "copy code" and a "copy prompt for AI" option. Take either.
- Hand it to Claude Design with an adaptation instruction, not a paste instruction:
Here is a component I want to use as a starting point: [paste the code or the prompt].
Adapt it to my site: recolor it to my palette ([your colors]), swap the fonts to
[your faces], rewrite the copy to [your content], and match the spacing and feel of
the rest of my site. I want the underlying mechanic, not its original look.
- The rule that prevents cloning: recolor AND restructure so the result isn't recognizable as the source. If two people grab the same 21st.dev hero and neither changes the palette, type, and copy, they ship the same hero. Change all three.
Licensing: most 21st.dev components are MIT or community-licensed and free for commercial use. Check the individual component's license before shipping a client site.
On the pipeline: components you bring in are just more JSX/CSS. They flow through the same Phase 2 build (compile JSX, production React, body SEO injection) exactly like Claude Design's own output. Nothing special to configure.
4.6 Changing colors, fonts, spacing, and other attributes
After the first generation you'll want to tune. Claude Design responds to plain-language attribute changes. Two scopes:
Global changes (whole site at once). Point at the design tokens. Claude Design keeps colors and type in a tokens file (often styles/colors_and_type.css). Say: "change the accent everywhere to oklch(0.65 0.2 25), a warm red." "Swap the body font to Inter across the whole site." "Increase the global spacing scale by ~20%, the site feels cramped." "Round all corners to 4px, the current radius is too soft." Global = edit the token, everything updates.
Targeted changes (one element or section). "Make only the hero headline larger and tighter, leading-none." "Give the pricing section a dark background so it pops against the light page." "The footer links are too low-contrast, darken them."
Attribute vocabulary Claude Design understands: color (hex / oklch / named), font family / weight / size / leading / tracking, spacing / padding / margin / gap, corner radius, border width and color, shadow depth, background (solid / gradient / image / texture), contrast, dark vs light section. If you can name it, you can change it. Iterate in small passes and look at desktop AND mobile after each.
If a change keeps reverting or only half-applies, the value is usually hardcoded in a component instead of pulled from the token file. Tell Claude Design: "pull this color from the design token instead of hardcoding it, then change the token."
4.7 Iterate visually before you export
Claude Design previews pages before export. Go through every page on desktop AND mobile:
- Look at every page at both sizes (toggle the preview).
- Click every internal link. Verify it goes to the right page.
- Check every CTA. Does it lead to your booking flow / form?
- Read every word. Is it your voice, not generic agency-speak? (See §18.5 on AI slop.)
- Test the motion. Does it match your motion personality, or did defaults creep back in?
Push back on the usual AI tells:
- "Industry-leading" / "transform your business": replace with specifics.
- Stock photos of suited businesspeople: real photos or honest placeholders.
- "Trusted by 500+ clients": only if true.
- Em-dashes everywhere: replace with periods (signals AI writing).
- Multiple CTAs competing on one page: pick ONE primary action.
4.8 The anti-clone gate (do not skip)
Before you export, run this check. If you answer "yes" to three or more, you've drifted back to the default look. Go back to 4.1.
- Is the background pure black or near-black with a single bright accent?
- Is the hero a centered single column with the headline in the middle?
- Is the body (and headline) a default sans like Inter / system-ui with no distinct display face?
- Below the hero, is it three equal feature cards in a row?
- Is the only motion a fade-up-on-scroll?
- Did a loading screen + wipe transition appear by reflex rather than by choice?
- Would this site be indistinguishable from the last one built with this SOP?
A unique site answers "no" to most of these. The goal isn't weird for its own sake. It's a design that could only belong to this business.
4.9 Export the output
When the design is locked, export from Claude Design. You'll get a folder roughly like this (your component files will be named for YOUR design, so the exact filenames differ, this is just the shape):
your-site/
├── index.html (home page entry)
├── about.html (one HTML entry per page)
├── [your-pages].html (named after what the business does)
├── privacy.html
├── terms.html
├── src/ (JSX source files)
│ ├── app.jsx (router)
│ ├── shared.jsx (Nav, Footer, common components)
│ ├── home.jsx (one component per page)
│ ├── [page].jsx
│ ├── legal.jsx (privacy + terms in one file)
│ └── [your-components].jsx (named for YOUR design: a hero animation,
│ a slider, a marquee. Optional motion helpers
│ like motion.jsx / page-transitions.js /
│ loading-screen.js appear ONLY if you chose
│ that motion personality in §4.1)
├── styles/
│ ├── site.css (all your styles)
│ ├── colors_and_type.css (design tokens: your colors + fonts live here)
│ └── fonts/ (font files)
└── assets/
├── logos/
├── visuals/ (custom illustrations, screenshots)
└── brand/ (logo variants)
Save this folder. This is your starting point for Phase 2.
4.10 What to expect from Claude Design's output
Honest expectations:
- Each HTML file: ~12-15KB, contains the page-specific meta + script tags. Bodies will usually be EMPTY (just a
<div id="root">+ React mount scripts). Despite the "pre-render" promise, what you usually get is multi-page React entries that hydrate client-side. Phase 5 fixes this. - JSX files: ~2-15KB each, use the
window.X = Xglobals pattern (not ES modules). - CSS: one big site.css plus a tokens file.
- Assets: just the images, no compression yet.
- Sometimes you get static HTML/CSS/JS, not React/JSX. Claude Design does not always export a React bundle. The The InjectR site came out as plain static HTML, CSS, and JS with no JSX and no bundler. When that happens, the entire JSX compile in Phase 2 (§5.2,
compile-jsx.mjs) is moot: nothing to transpile, no in-browser Babel to strip. You skip straight to body-SEO injection, meta/schema, image optimization, tracking, and deploy. Check which kind of export you have before running Phase 2. See Appendix B.
This is normal. Phase 2 onward is where YOU add the production layer.
4.11 The premium interaction layer (wired post-export, not in Claude Design)
For most sites, the motion Claude Design produces from §4.3 is enough. For a top-tier site (the kind a premium client is paying custom money for), the signature interactions are wired AFTER export, in the build phase, not generated in Claude Design. This came out of The InjectR build and a teardown of relayfi.com: that whole "how is this not impossible" feel is DOM plus GSAP plus smooth-scroll, not exotic WebGL. Every technique is reproducible.
The borrowable moves, in rough order of leverage:
- Momentum smooth scroll (Lenis). One library, sitewide. Reads as "expensive" more than any single animation. The highest-leverage borrow.
- Scroll-pinned or scroll-scrubbed hero. A section that pins and assembles, or a video that scrubs to scroll position via
currentTime, as you scroll. GSAP ScrollTrigger with a scrubbed timeline. This is the signature "wow." - Masked line-by-line headline reveal. GSAP SplitText (or a Framer Motion stagger) revealing the headline by word or line behind a mask.
- Stat count-up, draggable carousels, magnetic CTAs, SVG stroke-draw. Pick a few, not all.
How to do it without losing the work on the next re-export:
- Claude Design builds the static design, layout, type, copy, and the look. It does NOT build the bespoke GSAP/Lenis interactions.
- The interactions get added to the exported code as a separate, re-appliable build step (see §5.5), so re-exporting from Claude Design does not nuke them.
- Target 4 to 6 signature interactions, not 30. Hero scrub, smooth scroll, one headline reveal, a card-assemble, a carousel, a count-up gets you the relay level without drowning the page in motion. The §4.3 motion-budget and reduced-motion rules still apply.
The differentiator here is taste and the willingness to custom-build each section, not a magic tool. Budget days of polish, not one prompt.
5. Phase 2: Set up the repo + build pipeline
5.1 Initialize the repo
On your computer:
# Create a folder for the site
mkdir my-site
cd my-site
# Copy everything from Claude Design's export into this folder
# Then:
git init
echo "node_modules/" > .gitignore
echo "*.log" >> .gitignore
echo ".DS_Store" >> .gitignore
# Set up package.json for the build scripts
mkdir scripts
cd scripts
npm init -y
npm install --save-dev @babel/core @babel/preset-react playwright
cd ..
5.2 Create the build script
Save as scripts/compile-jsx.mjs:
import { readdir, readFile, writeFile, mkdir, rm } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, resolve, basename, extname } from 'node:path';
import { transformAsync } from '@babel/core';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SITE_DIR = resolve(__dirname, '..');
const SRC_DIR = resolve(SITE_DIR, 'src');
const OUT_DIR = resolve(SRC_DIR, 'compiled');
const BODY_SEO_DIR = resolve(__dirname, 'body-seo');
// Wipe + recreate compiled/
try { await rm(OUT_DIR, { recursive: true, force: true }); } catch {}
await mkdir(OUT_DIR, { recursive: true });
// Babel config — classic runtime, production mode
const babelOptions = {
presets: **'@babel/preset-react', { runtime: 'classic', development: false }**,
babelrc: false,
configFile: false,
sourceType: 'script',
compact: false
};
// Step 1: Transpile all JSX to JS
const entries = await readdir(SRC_DIR);
const jsxFiles = entries.filter(f => f.toLowerCase().endsWith('.jsx'));
for (const file of jsxFiles) {
const inPath = resolve(SRC_DIR, file);
const outPath = resolve(OUT_DIR, basename(file, extname(file)) + '.js');
const input = await readFile(inPath, 'utf-8');
const result = await transformAsync(input, { ...babelOptions, filename: file });
await writeFile(outPath, result.code, 'utf-8');
console.log('Compiled', file);
}
// Step 2: Update each HTML file
const rootEntries = await readdir(SITE_DIR);
const htmlFiles = rootEntries.filter(f => f.toLowerCase().endsWith('.html'));
for (const htmlFile of htmlFiles) {
const path = resolve(SITE_DIR, htmlFile);
let html = await readFile(path, 'utf-8');
// Drop @babel/standalone
html = html.replace(/[ \t]*<script\b[^>]*src="https:\/\/unpkg\.com\/@babel\/standalone[^"]*"[^>]*><\/script>\s*\r?\n?/gi, '');
// Switch React dev → production
html = html.replace(
/<script\b([^>]*)src="https:\/\/unpkg\.com\/react@18\.3\.1\/umd\/react\.development\.js"([^>]*)><\/script>/gi,
'<script src="https://unpkg.com/[email protected]/umd/react.production.min.js" crossorigin="anonymous"></script>'
);
html = html.replace(
/<script\b([^>]*)src="https:\/\/unpkg\.com\/react-dom@18\.3\.1\/umd\/react-dom\.development\.js"([^>]*)><\/script>/gi,
'<script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js" crossorigin="anonymous"></script>'
);
// Rewrite <script type="text/babel" src="src/X.jsx"> → <script src="src/compiled/X.js">
html = html.replace(
/<script\b\s+type="text\/babel"\s+src="src\/([^"]+)\.jsx"\s*><\/script>/gi,
'<script src="src/compiled/$1.js"></script>'
);
// Step 3: Inject per-page body SEO content
const pageName = basename(htmlFile, '.html');
try {
const seoContent = (await readFile(resolve(BODY_SEO_DIR, pageName + '.html'), 'utf-8')).trim();
if (!html.includes('vx-seo-content')) {
const idx = html.toLowerCase().lastIndexOf('</body>');
if (idx !== -1) {
html = html.slice(0, idx) + '\n' + seoContent + '\n' + html.slice(idx);
}
}
} catch { /* no body SEO file for this page */ }
await writeFile(path, html, 'utf-8');
console.log('Updated', htmlFile);
}
console.log('Done.');
5.3 Add npm scripts
Update scripts/package.json:
{
"scripts": {
"build": "node compile-jsx.mjs"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-react": "^7.29.0"
}
}
5.4 Run the build
cd scripts
npm run build
This will:
- Compile every JSX file in
src/to plain JS insrc/compiled/ - Update every HTML file at root to use compiled JS instead of
text/babel - Switch React from dev build to production build
- Remove the in-browser Babel runtime
Verify by opening one of the HTML files in a browser — should still work, but faster.
5.5 Keep every post-export edit in a re-runnable script
The single most important workflow rule for this stack: never hand-edit the exported files for anything you will need again. Every post-export modification (tracking snippets, Schema.org JSON-LD, the body-SEO injection, conversion tags, even bulk find-and-replace) lives in a script in scripts/ that you can re-run. The Accu-Track build used a build_backend.py that injected the whole tracking and conversion layer across all pages in one idempotent pass.
Why this is non-negotiable: when you regenerate the design from Claude Design (§14.4), a fresh export overwrites the HTML. If your tracking and SEO were hand-typed into those files, they are gone, silently, and you will not notice until conversions stop reporting. If they live in scripts, you re-run the scripts after the new export and you are back to production in minutes. The compile step (§5.2) already works this way. Tracking, schema, and any other post-export layer should too.
6. Phase 3: Deploy to Cloudflare Pages
6.1 Push to GitHub
# At your site root
git add .
git commit -m "Initial site build"
# Create a new repo at https://github.com/new (empty, no README/license)
# Then copy the URL and push:
git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO.git
git branch -M main
git push -u origin main
6.2 Connect Cloudflare Pages
- Go to https://dash.cloudflare.com → Workers & Pages
- Click Create application → Pages tab → Connect to Git
- Authorize Cloudflare to access GitHub (first time only)
- Select your repository
- Build settings:
- Project name: anything (becomes your
*.pages.devURL) - Production branch:
main - Framework preset: None
- Build command: leave blank
- Build output directory:
/(just a forward slash)
- Project name: anything (becomes your
- Click Save and Deploy
In ~30 seconds, you'll get a URL like your-project.pages.dev. Open it. Your site is live.
6.3 Why no build command on Cloudflare
You're committing the compiled JS files directly to the repo. Cloudflare just serves them. No Cloudflare build step = no CI environment mismatches, no surprise breakages.
The build step runs LOCALLY on your machine before each push.
7. Phase 4: DNS + custom domain
7.1 Critical: don't break your email
Before touching DNS, document what's currently in your DNS records. Especially MX records (email). Take a screenshot or export the zone file from your registrar.
If you have email at [email protected]:
- You MUST preserve the existing MX records
- You MUST preserve any SPF, DKIM, DMARC TXT records
- You MUST preserve any platform verification records (Microsoft 365, Google Workspace, etc.)
7.2 The simple path: CNAME only (no full DNS migration)
This path keeps your existing DNS provider (GoDaddy or wherever) and just points www to Cloudflare Pages. Doesn't touch email. Recommended for most.
At Cloudflare:
- Your Pages project → Custom domains tab → Set up a custom domain
- Enter
www.yourdomain.com(subdomain, not apex) - Cloudflare shows a CNAME target like
your-project.pages.dev - Copy that target
At your DNS provider (GoDaddy in this example):
- Log into GoDaddy → My Products → find your domain → click DNS
- Find the existing CNAME record for
www(or A record). Edit it:- Type: CNAME
- Name:
www - Value: paste the Cloudflare target
- TTL: 1 hour
- Save
For the apex domain (yourdomain.com without www):
GoDaddy doesn't support CNAME at the apex. Two options:
- Option A (recommended): GoDaddy domain forwarding from
yourdomain.com→https://www.yourdomain.com. In GoDaddy → Domain settings → Forwarding → Add forwarding.- Destination:
https://www.yourdomain.com(HTTPS, withwww). NEVERhttp://yourdomain.com— that's a forward-to-self loop that returns405 Method Not Allowedand silently fails SEO audits. The "URL canonicalization" audit fail almost always traces to this exact misconfiguration. - Type: Permanent (301). GoDaddy's default is 302 Temporary — toggle it.
- Forward only: OFF. Forward with masking: OFF. (You want a real 301, not an iframe wrapper.)
- Update my DNS to support forwarding: ON. GoDaddy will swap the apex A records to its forwarding-server IPs automatically.
- Verify after 5-30 min propagation:
curl -I https://yourdomain.com/should return301withLocation: https://www.yourdomain.com/. Andhttps://yourdomain.com/methodshould redirect tohttps://www.yourdomain.com/method(path preserved, not dropped).
- Destination:
- Option B: Migrate full DNS to Cloudflare (more complex, requires preserving all your records exactly). Unlocks Cloudflare Redirect Rules at the edge (faster than GoDaddy's HTTP forwarder) + WAF + Email Routing. Worth it long-term if you'll grow; skip for v1.
7.3 Wait for DNS propagation
5-30 minutes typically. Check by visiting https://www.yourdomain.com. Cloudflare auto-provisions SSL within a few minutes after DNS resolves.
7.4 Common pitfalls
- Don't add a
_redirectsfile for.htmlstripping. Cloudflare Pages already auto-serves/method.htmlwhen you request/method. Adding a redirect rule causes an infinite loop (we hit this — it took an hour to figure out). - Don't migrate full DNS to Cloudflare if you don't need to. If email is on the same domain, the migration risk is real. Verify every MX/TXT record copies over before changing nameservers.
7.5 Taking over a client who already has a site: repoint a subdomain
When you build for a client who already has a live website, you usually do NOT touch their apex domain. The cleanest path, used on the Accu-Track build, is to put the new site on a subdomain and leave the old site alone:
- Pick a subdomain the client controls (Accu-Track used
go.accutracksolutions.com, already pointed at a rented funnel platform). - At the client's DNS, change that subdomain's CNAME from the old platform to the Cloudflare Pages target. This is the simplest possible DNS case: a single CNAME swap, no apex forwarding gymnastics, no email risk (you are not touching MX records).
- Leave the apex (their main WordPress or whatever) live and untouched. If they later kill the old site, you do a Cloudflare custom-domain swap to the apex plus a 301 from the subdomain, and the SEO equity transfers. No rework.
This also sidesteps a fight: you are not asking to move their whole domain, just to point one subdomain you already run the ads to. Run the full SEO/AEO moat on the subdomain anyway. If the apex gets near-zero traffic (common for a stale client site), cannibalization is a non-issue.
8. Phase 5: SEO + AEO infrastructure
This is where most sites stop. Going further here is what separates a "live site" from a "discoverable site."
8.1 robots.txt
Save as robots.txt at repo root:
User-agent: *
Allow: /
# Allow all major LLM crawlers explicitly
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: Bingbot
Allow: /
User-agent: anthropic-ai
Allow: /
Sitemap: https://www.yourdomain.com/sitemap.xml
8.2 sitemap.xml
Save as sitemap.xml at repo root:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.yourdomain.com/</loc>
<lastmod>2026-06-09T00:00:00+00:00</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://www.yourdomain.com/about</loc>
<lastmod>2026-06-09T00:00:00+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<!-- repeat for each page -->
</urlset>
8.3 llms.txt — for AI crawlers
Save as llms.txt at repo root. This is the Anthropic-proposed standard for telling LLMs about your site:
# [Your Business Name]
> [One-line description of what your business does and who it serves]
## What you do
[2-3 paragraphs describing your services]
## Who you work with
[Describe your audience, geography, fit criteria]
## How to engage
[Booking, contact, etc.]
## Contact
- Email: [email protected]
- Phone: (XXX) XXX-XXXX
- Location: [City, Province, Country]
- Website: https://www.yourdomain.com
8.4 llms-full.txt — deeper AI content
Same idea but longer. Include common questions and answers. This is what ChatGPT browse / Perplexity / Claude bot read to understand your business in depth. Aim for 1500-3000 words.
8.5 Per-page hidden body SEO content (THE SECRET WEAPON)
This is the trick that separates this stack from generic Claude Design output.
Create folder scripts/body-seo/ with one HTML file per page. Each file contains:
<aside class="vx-seo-content" style="position:absolute;left:-10000px;top:-10000px;width:1px;height:1px;overflow:hidden;clip:rect(1px,1px,1px,1px);">
<h1>[Page-specific descriptive H1 — not your brand name]</h1>
<p>[1-2 paragraphs introducing the page]</p>
<h2>[ONE topic-specific H2 wrapper, e.g. "About this page" or "About this service"]</h2>
<h3>[Subsection 1]</h3>
<p>[Content with at least 1 internal anchor link like <a href="/method">read about the method</a>]</p>
<h3>[Subsection 2]</h3>
<p>[More content]</p>
<h3>References</h3>
<ul>
<li><a href="[external URL]" rel="noopener">[Description]</a></li>
<li><a href="[external URL]" rel="noopener">[Description]</a></li>
</ul>
</aside>
Why this works:
- Visually hidden via the canonical
.sr-onlyaccessibility pattern (offscreen withcliprule). Same pattern Bootstrap, Tailwind, Reach UI use. Not cloaking. - Readable by crawlers that don't run JavaScript. They see the H1, heading hierarchy, body content, internal links, and external citations.
- Page-specific content means no duplicate-content penalty across pages.
- Real users never see it — the React app on top renders the actual visual UI.
This is what gets you from a 67/100 to 100/100 on AEO checkers, and what makes ChatGPT / Perplexity recommend your site by name.
Recommended structure per page:
- 1 descriptive H1 (no brand name in it)
- ONE H2 wrapper that sits between the H1 and the H3 subsections. Topic-specific phrasing works best — e.g. "About this page," "About this service," "About the strategy call." This is the critical detail. Without it, you have an H1 → H3 skip that Seobility and most accessibility-aware audits flag as "missing heading levels." With it, the outline reads cleanly: H1 page title → H2 visible page sections (5-6, from your React UI) → H2 hidden wrapper (1) → H3 hidden subsections (5-9). Total H2 count stays under 10 across visible + hidden, so SEO Site Checkup's "too many H2 tags" also passes. Both audits agree.
- 5-9 H3 subsections under the H2 wrapper, each with its own paragraph
- 600-900 words of real content. The visible page may be intentionally tight (per direct-response doctrine); the hidden block is where you bulk up topic depth for AI crawlers + ranking without polluting visible UX. Treat it as the long-form companion to the lean visible page, not as filler.
- 5-10 internal anchor links spread across the body — cross-link aggressively between every page (/method, /about, /contact, /, /privacy, /terms). Audits flag "too few internal links" when count < 4. The hidden block is where most of these links live.
- 2-4 external citation links to authoritative sources. Use
rel="noopener"only — do NOT addnofollow. These are proof-points (FTC, government, platform docs) that strengthen your content's credibility signal; nofollow tells search engines "I don't endorse this" which is the opposite of what you want.
Critical: do not name platform vendors in the hidden block. GoHighLevel, Manychat, Make.com, Zapier, Webflow, Framer, etc. should never appear in hidden body SEO (or anywhere else client-facing). Crawlers + AI tools index this content; the moment a competitor or prospect reads "powered by GoHighLevel" in your site source, you've leaked operational stack. Use neutral phrasing: "third-party booking platform," "CRM and follow-up infrastructure," "behavioral analytics tools."
The build script (Phase 2.2) injects these into the HTML automatically.
8.6 Per-page meta + Schema.org JSON-LD
First, pick the right business schema type. The example below uses LocalBusiness, which is wrong for many businesses. Get this right before copying the JSON-LD:
LocalBusiness(plus a subtype): a business serving customers from a physical storefront in a local area. Set a real address andareaServedcities.["ProfessionalService", "Organization"]: a national or B2B service firm with no local-storefront sales motion. Accu-Track used this (national US B2B), withareaServedset to the country, not a city list, plusfoundingDate. Do NOT useLocalBusinessfor a national firm.- Service-area business (no public address): a mobile or in-home business (The InjectR is mobile across the GTA). Use
LocalBusiness/MedicalBusinessbut omit the street address and listareaServedregions, and set Google Business Profile up as a service-area business (§11.2), not a storefront. - Layer specific types where they fit:
MedicalBusiness,Person(a named practitioner),Serviceon each service page,BreadcrumbListon deep pages.
In every HTML file's <head>, include:
<!-- Page-specific -->
<title>[Unique title under 60 chars, describes the page]</title>
<meta name="description" content="[Unique description under 155 chars, includes a call-to-action]">
<link rel="canonical" href="https://www.yourdomain.com/[page-path]">
<!-- OpenGraph (page-specific) -->
<meta property="og:title" content="[Same as title]">
<meta property="og:description" content="[Same as description]">
<meta property="og:url" content="https://www.yourdomain.com/[page-path]">
<meta property="og:type" content="website">
<meta property="og:image" content="https://www.yourdomain.com/assets/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<!-- Twitter Cards (mirror OG) -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="[Same as title]">
<meta name="twitter:description" content="[Same as description]">
<meta name="twitter:image" content="https://www.yourdomain.com/assets/og-image.png">
<!-- Schema.org structured data (LocalBusiness — appears on every page) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": ["LocalBusiness", "ProfessionalService"],
"name": "[Business Name]",
"description": "[Business description]",
"url": "https://www.yourdomain.com",
"telephone": "+1-XXX-XXX-XXXX",
"email": "[email protected]",
"address": {
"@type": "PostalAddress",
"addressLocality": "[City]",
"addressRegion": "[Province/State]",
"addressCountry": "[Country code, e.g. CA or US]"
},
"areaServed": [
{ "@type": "City", "name": "[City 1]" },
{ "@type": "City", "name": "[City 2]" }
],
"founder": {
"@type": "Person",
"name": "[Founder Name]"
}
}
</script>
<!-- Schema.org FAQPage — ONLY on the home page (or wherever FAQ lives) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "[Question 1]?",
"acceptedAnswer": { "@type": "Answer", "text": "[Answer 1]" }
}
/* 5-7 FAQ items total */
]
}
</script>
8.7 Audit-pass essentials (the difference between 80/100 and 95+/100)
Standard SEO audits (SEO Site Checkup, Sitechecker, Lighthouse, Framer's AEO checker) flag a predictable set of failures unless these are baked into every build. None take more than 10 minutes total. Skip them and you ship at ~75–80/100. Include them and you ship at 95+/100.
1. Async-load Google Fonts (kills "render-blocking resources" fail)
The default Google Fonts <link> is render-blocking — the browser waits for the CSS download before painting anything. Swap to media-print trick + noscript fallback:
<!-- BEFORE (render-blocking) -->
<link href="https://fonts.googleapis.com/css2?family=Inter..." rel="stylesheet">
<!-- AFTER (async, non-blocking) -->
<link href="https://fonts.googleapis.com/css2?family=Inter..." rel="stylesheet"
media="print" onload="this.media='all'">
<noscript><link href="https://fonts.googleapis.com/css2?family=Inter..." rel="stylesheet"></noscript>
The media="print" tells the browser the stylesheet is for print → doesn't block render. onload swaps to all once loaded. <noscript> fallback covers JS-disabled users.
Apply to every HTML file. Keep preconnect lines for fonts.googleapis.com and fonts.gstatic.com above — they're still useful.
2. ads.txt (kills the "Authorized Digital Sellers" audit fail)
Most audits flag missing ads.txt even if you don't sell ad inventory. 10-second fix — save at repo root:
# [Business Name] is not an ad publisher.
# This file exists to satisfy IAB Authorized Digital Sellers spec
# for sites that do not sell advertising inventory.
# https://iabtechlab.com/ads-txt/
3. Strip bundler-artifact meta tags from HTML head
Claude Design's exported HTML often contains 15–25 <meta name="ext-resource-dependency" ...> tags per page — bundler hints that have ZERO effect in production deploys but bloat HTML size by 10–15% and clutter audit reports. Strip them in your build pipeline:
# Add to your build script (Python or Node)
import re
s = re.sub(r'<meta name="ext-resource-dependency"[^>]*/>\n', '', s)
4. No nofollow on authoritative reference citations
Covered in §8.5 — repeated here because it's the easiest miss. Hidden body SEO references to FTC, government docs, platform documentation, etc. should use rel="noopener" only. Adding nofollow tells search engines "I don't endorse this source" — the opposite of the proof-point signal you're trying to send.
5. Heading hierarchy: cap visible H2s at ~6 per page
Covered in §8.5 — visible page should have 5–7 H2s for visual section heads. Hidden body SEO content uses H3s. Total H2 count per page > 10 triggers "too many H2 tags" on most audits and dilutes the semantic outline that helps both Google and AI crawlers parse the page.
6. Conscious skips (audit fails that aren't worth fixing)
Not every audit checkbox should be ticked. Two examples of warnings that look bad in the report but are actually correct as-is:
- "Empty alt attribute" on decorative images (cursor accents, page-wipe overlays, anything that's pure visual filler).
alt=""is the WCAG-recommended pattern — it tells screen readers "skip this, no info loss." Adding a descriptive alt to a cursor sparkle is worse accessibility, not better. Audit is being over-eager; leave it. - "Plaintext email exposed" warning when you have
[email protected]in the footer. JS-obfuscating the mailto link hurts screen readers and assistive tech. Catch-all inbox absorbs the spam. Tradeoff isn't worth it for a marketing site; the warning is a relic of the 2005 spam-harvester era.
Document which fails you're consciously skipping in your repo README so they're not "fixed" on the next pass.
9. Phase 6: Tracking stack
Set up before launch. Free tools, ~30 minutes total.
9.1 Google Tag (gtag.js) + Google Analytics 4
- Go to https://analytics.google.com → create property for your site
- Note your Measurement ID (starts with
G-) - Optional: also set up a Google Tag at https://tagmanager.google.com (different from GA4)
- Add to every HTML file's
<head>:
<script async src="https://www.googletagmanager.com/gtag/js?id=GT-XXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GT-XXXXXXXX');
gtag('config', 'G-XXXXXXXXXX');
</script>
9.2 Meta Pixel
- https://business.facebook.com/events_manager → create Pixel
- Note your 16-digit Pixel ID
- Add to every HTML file's
<head>:
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'YOUR_PIXEL_ID');
fbq('track', 'PageView');
</script>
<noscript><img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=YOUR_PIXEL_ID&ev=PageView&noscript=1"/></noscript>
9.3 Microsoft Clarity (free heatmaps + session recordings)
- https://clarity.microsoft.com → create project
- Note the project ID (alphanumeric like
wwph26h1i2) - Add to every HTML file's
<head>:
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "YOUR_CLARITY_ID");
</script>
9.4 Critical: only inject tracking scripts ONCE
If you're using a SHARED head-snippet file, make sure tracking scripts only fire once per page load. Pre-rendered HTML files should each have the analytics scripts directly in the <head> — no need for runtime injection.
If you're using a build script to inject head content into BOTH the outer HTML and an inner template (e.g., if your bundler swaps the document), make sure analytics scripts are OUTER ONLY. Otherwise you'll get "Duplicate Pixel ID" warnings and your tracking will double-fire.
9.5 Phone call tracking (the conversion the pixel stack misses)
Everything above tracks web events. For any business where customers pick up the phone (home services, contractors, B2B, local trades), the phone call is often the primary conversion, and none of the pixels above catch it. Add call tracking or you are optimizing ad spend against half the conversions.
For Google Ads, use a Google Forwarding Number (GFN) with number insertion, not clicks-to-call:
- Set up a "Calls from a number on your website" conversion action. Google provisions a forwarding number and a snippet that swaps the displayed number on the page.
- Use GFN, not clicks-to-call. Clicks-to-call only fires when someone taps a
tel:link on mobile. Desktop-heavy traffic (most B2B) never taps, it reads the number and dials from a desk phone. GFN tracks the actual call regardless of device. This was the Accu-Track case (desktop-majority financial-services traffic). - The displayed number text must match the site's format exactly or the swap will not fire. If the site shows
(860) 236-8002, enter the conversion's phone number in that same format. - After install, verify the swap on desktop and confirm the mobile
tel:link still routes to the GFN, not the raw line. Google provisions and verifies the number over 24 to 48 hours, so the action shows Inactive at first. Default call-duration threshold is 60 seconds.
This is account-side setup (Phase 8) as much as code, but it belongs with the tracking stack so it does not get forgotten.
10. Phase 7: Performance optimization
10.1 PNG → WebP conversion + resize (kills 60–80% of image weight)
The single highest-ROI performance fix on any Claude Design build. Default exports use PNG everywhere (often 2,000×2,000 source files where 800px is displayed). Converting to WebP + resizing to actual display sizes typically cuts total image weight by 80%+ and passes both "Modern Image Format" and "Properly Sized Images" audit fails.
Real-world result from versorix.com: 23 assets, 2,219 KB → 361 KB (83% reduction). Biggest single win: a hero portrait went 705 KB → 39 KB.
# scripts/optimize-images.py
"""
PNG → WebP conversion + resize for oversized originals. Run after dropping new
assets into assets/. Idempotent: skips WebP files newer than their PNG source.
Per-directory quality rules: small icons get higher quality, large hero images
get more aggressive compression. WebP saved alongside PNG so JSX can reference
.webp directly while PNG stays on disk as legacy fallback.
"""
from PIL import Image
from pathlib import Path
SITE = Path(__file__).parent.parent
ASSETS = SITE / 'assets'
# (glob_relative_to_assets, max_edge_px, webp_quality)
RULES = [
('hero-portrait.png', 1000, 82), # adjust filename to your hero photo
('visuals/*.png', 1000, 82), # in-page visual assets
('logos/*.png', 500, 85), # logo wall / client logos
('brand/*.png', 512, 88), # cursor accents, brand marks, decorative SVGs
('logo-mark-*.png', 1024, 88), # large logo marks for OG / loading screens
]
# DO NOT convert: social-share images (platforms inconsistent on WebP) and
# browser/PWA icons (specs expect PNG).
SKIP = {'og-image.png', 'og-image-twitter.png', 'apple-touch-icon.png',
'android-chrome-192x192.png', 'android-chrome-512x512.png',
'favicon-16x16.png', 'favicon-32x32.png'}
def resize_to_max(im, max_edge):
w, h = im.size
if max(w, h) <= max_edge:
return im
if w >= h:
return im.resize((max_edge, int(h * max_edge / w)), Image.LANCZOS)
return im.resize((int(w * max_edge / h), max_edge), Image.LANCZOS)
def process(png_path, max_edge, quality):
if png_path.name in SKIP:
return
webp_path = png_path.with_suffix('.webp')
if webp_path.exists() and webp_path.stat().st_mtime >= png_path.stat().st_mtime:
print(f' [skip] {png_path.relative_to(SITE)}')
return
im = Image.open(png_path)
if max(im.size) > max_edge:
im = resize_to_max(im, max_edge)
im.save(png_path, format='PNG', optimize=True)
save_kwargs = {'quality': quality, 'method': 6}
if im.mode == 'RGBA':
save_kwargs['lossless'] = False
im.save(webp_path, format='WebP', **save_kwargs)
print(f' [ok] {png_path.relative_to(SITE)} -> webp {webp_path.stat().st_size // 1024}KB')
def main():
for pattern, max_edge, quality in RULES:
for png in ASSETS.glob(pattern):
if png.is_file():
process(png, max_edge, quality)
if __name__ == '__main__':
main()
Then point JSX + Schema.org to the .webp versions:
// BEFORE
<img src={assetUrl("assets/visuals/dashboard-stack.png")} alt="..." loading="lazy"/>
// AFTER
<img src={assetUrl("assets/visuals/dashboard-stack.webp")} alt="..." loading="lazy"/>
WebP browser support is 96%+ globally. For the 4% on ancient browsers, the PNG fallback file stays on disk but isn't served. If you need true fallback for 100% coverage, use <picture>:
<picture>
<source srcSet={assetUrl("assets/visuals/dashboard-stack.webp")} type="image/webp"/>
<img src={assetUrl("assets/visuals/dashboard-stack.png")} alt="..." loading="lazy"/>
</picture>
Size rules of thumb (after resize + WebP):
- Logo / icon: < 30 KB
- In-page visual: < 60 KB
- Hero photo: < 100 KB
- OG image (keep PNG, social-platform compat): < 200 KB
If anything blows past these after conversion, the source dimensions or quality settings need another pass.
10.2 Preload the LCP image
LCP (Largest Contentful Paint) is one of Google's three Core Web Vitals. On most marketing sites, the LCP element is the hero logo or hero photo — and the browser doesn't know to fetch it until React renders. Tell it upfront with a <link rel="preload"> in every page's <head>:
<!-- Add right after Fonts block, before stylesheets -->
<link rel="preload" as="image" href="/assets/brand/your-hero-image.webp" type="image/webp">
This single line typically cuts LCP by 200–500 ms on cold loads. Identify your LCP element from PageSpeed Insights or Lighthouse → "Largest Contentful Paint" → check what <img> it points to.
10.3 Generate favicon set
You need:
favicon.ico(multi-resolution 16/32/48 — at repo root)favicon-16x16.png,favicon-32x32.png(in assets/)apple-touch-icon.png(180x180)android-chrome-192x192.png,android-chrome-512x512.png
Easiest: start with a single 1024x1024 source image (your logo), use Python + Pillow to generate all sizes via LANCZOS resampling.
10.4 Generate OG image
The OG image is what shows in social previews (LinkedIn, iMessage, Slack, etc.). 1200x630 PNG. Best practices:
- Brand-recognizable from far away
- No essential text smaller than 24pt
- Your own signature accent or visual that makes it pop (use your brand color, not the example's yellow)
- Save to
assets/og-image.png
10.5 Web manifest
Save as site.webmanifest at root:
{
"name": "[Business Name]",
"short_name": "[Short Name]",
"description": "[Description]",
"start_url": "/",
"display": "standalone",
"background_color": "[YOUR background color, e.g. #0E0E0E or #FAF7F2]",
"theme_color": "[YOUR brand color, not the example's #FFE100]",
"icons": [
{ "src": "/assets/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/assets/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
]
}
10.6 Security headers
Save as _headers at root (Cloudflare Pages reads this):
/*
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Cross-Origin-Opener-Policy: same-origin
X-DNS-Prefetch-Control: on
/assets/*
Cache-Control: public, max-age=31536000, immutable
/*.html
Cache-Control: public, max-age=300, must-revalidate
NOTE: Skip Content-Security-Policy initially. The bundler architecture has runtime patterns that strict CSP blocks. Add CSP later only after auditing every script/style/connect endpoint the page actually uses.
10.7 Lazy-load heavy third-party embeds
Embedded forms, calendars, and booking widgets (the iframe kind) are heavy and load-blocking, and they will tank LCP and the page-experience score if they sit in the initial render. On a conversion-driven site that page-experience hit also drags your ad Quality Score. Defer them: load the embed on scroll-into-view, on interaction, or after the main content paints, rather than inline on first load. The Accu-Track contact page lazy-loads its form embed for exactly this reason. The visible page should paint fast; the embed can arrive a beat later.
11. Phase 8: Account-side setup (post-launch)
After the site is live, ~90 minutes of clicking through dashboards to set up everything that lives outside the code.
11.1 Google Search Console (10 min)
- https://search.google.com/search-console
- Add property → URL prefix →
https://www.yourdomain.com - Verify via DNS TXT record OR HTML tag (you already have it in head)
- Submit your sitemap:
sitemap.xml - Use URL Inspection to request indexing for each page (forces Google to crawl within 24-48h)
11.2 Google Business Profile (20 min — biggest local-SEO win)
- https://business.google.com → create profile
- Pick Marketing agency (or your category) as primary
- Add 2-3 secondary categories
- Service area: list all the cities you serve (this is critical for local-pack rankings)
- Description: paste your llms.txt overview (up to 750 chars)
- Add photos: logo, owner portrait, work samples, NO promotional text overlays (they cause suspension)
- Phone, email, hours
- Google mails a postcard for verification (5-14 days)
11.3 Bing Webmaster Tools (5 min)
- https://www.bing.com/webmasters
- Add site → import from Google Search Console (1-click)
- Bing covers ChatGPT search, DuckDuckGo, Yahoo, Ecosia — substantial traffic that 99% of sites miss
11.4 GA4 conversion events (15 min)
In GA4 admin → Events → mark these as conversions:
clickon your booking CTA buttonform_submit(if you have a form)scrollat 90% depth (engagement signal)
Without conversion events, your analytics can't report ROI on ads.
Google Ads conversions, two traps that cost the Accu-Track engagement eight weeks (see Bug 9 and Bug 10 in §13): (1) An existing conversion action of type "Website (Import from clicks)" does NOT fire from a website tag, it expects an offline GCLID upload from your CRM. To track a tag on the page you must create a NEW website conversion action to get a firing snippet and label. (2) If your conversion fires on a /thank-you page load, the form MUST redirect there on submit. An embedded (iframe) form that shows an inline "thanks" never triggers it, because the submit is cross-origin and the parent page's gtag cannot see it.
11.5 Meta Events Manager (10 min)
In Events Manager → your Pixel → Configure web events:
- Set up
Leadevent for booking CTA clicks - Set up
Scheduleevent for completed bookings - Set up
ViewContentevent for high-engagement pages
Without these, Meta's ad algorithm can't optimize for conversions — only impressions.
11.6 Brand consistency across platforms
Your LinkedIn, YouTube, GBP, and any other directory listings need to match the website's current positioning. Mismatched positioning across platforms confuses both users and search engines.
Update:
- LinkedIn company page tagline + description
- YouTube channel name + About section
- Any directory listings (Yelp, Yellow Pages, etc.)
This is silent SEO that compounds over months.
11.7 Why "External factors" / backlinks score is low at launch (and how to fix it)
Every SEO audit returns a section called "External factors," "Backlinks," "Off-page SEO," or similar. This score is always low at launch and cannot be fixed from inside the site. A backlink is another domain linking to yours. You need other people's websites to link to you. That work happens outside your codebase.
The good news: most service businesses already own 4-8 properties that can become backlink sources — they just haven't been linked properly. The next subsections walk through the tiered approach.
11.8 Tier 1: Owned-media link audit (do this in one afternoon)
You probably already have these properties. They need to link to your site. Audit each one:
Google Business Profile (already covered in §11.2) — biggest single backlink. Confirm website field is set to https://www.yourdomain.com. Every GBP post you publish should link back to the site. Service descriptions can link to /method or /contact. This single profile being verified + linked is worth more than 50 random directory backlinks.
LinkedIn — personal profile (your highest authority property)
- About section: include the website URL in the first 3 lines (only first ~220 chars show without "see more")
- Featured section: pin a website link card or recent post about the company
- Experience entry for the company: link to website
- Posts: when linking to your own site, put the link in the FIRST COMMENT, not the post body. LinkedIn deprioritizes posts with external links in the body but boosts reach when links go in comments.
LinkedIn — company page
- Website URL field
- Tagline should match site H1 or value prop
- Description includes the URL in the body once
- Posts that drive traffic should use specific page links (
/method,/about) not just homepage
Podcast (if you host one — your biggest under-leveraged backlink source)
- Every episode's show notes: link to your site in the first paragraph
- Episode descriptions on Spotify / Apple Podcasts / YouTube — same
- Host bio across every podcast platform: site URL
- If your podcast has its own website, the About page + footer should link your main site
- Each episode page is a DIFFERENT backlink. 50 episodes = 50 backlinks if structured right.
- When you have guests on, send them a "thanks for being on" email asking them to share the episode + tag you — that's referral backlinks from their audiences
Instagram bio
- Bio link = website URL (or Linktree pointing to it). If only one slot, use the homepage.
YouTube channel (if you have one)
- About section: website link
- Banner: include the URL graphically + as a clickable link
- Every video description: link in first 3 lines
Personal Twitter / X profile
- Bio: website URL
Tier 1 alone, audited in one afternoon, typically produces 20-30 backlinks from properties you already own.
11.9 Tier 2: Directory submissions (5-10, NOT 500)
Skip the spammy "200 citations for $50" services — they hurt more than they help (Google's Penguin algorithm penalizes link spam). Pick high-trust sources, manual signup, real profile content. The targets vary by industry but the principle is the same: quality over quantity, 10 trusted directories beat 200 spammy ones.
Universal directories that work for most service businesses:
- Clutch.co — agency / service-provider directory, used by real buyers. Free profile, manual approval.
- UpCity — same kind, free tier
- Sortlist — agency-finder, free tier with paid upgrades
- Better Business Bureau (your country) — local trust signal
- Your local chamber of commerce (city or region) — local backlink + community angle
- Yelp for Business — even low traffic, the link counts
- Industry-specific directories — search "[your industry] directory" and pick the top 2-3 with real content (not just listings)
Budget 1 directory per day for 1-2 weeks. NAP consistency (next subsection) is critical — fill out each profile with identical info every time.
11.10 Tier 3: Active backlink outreach (ongoing, 3-6 months out)
These are higher-effort and pay off over months, but they produce backlinks from real publications and high-authority sites:
HARO / Help A B2B Writer / Qwoted — answer journalist queries about topics in your niche. Get quoted with a backlink in real publications. ~30 min/day = 2-4 backlinks/month from real outlets. Free tier on all three platforms.
Guest posts on niche-relevant blogs — write 1500-word articles for blogs that target your buyer. Each post = a dofollow backlink. Pitch via email; usually 1 in 10 lands. Aim for 2-4 placements/quarter.
Podcast guesting (you as a guest, not just host) — every show notes link counts. The contractor-coaching / agency-owner / industry-specific podcast space is dense. Find 5-10 podcasts that match your buyer profile, pitch them with a specific angle, get on as a guest.
Partnership cross-links — vendors, partners, complementary service providers in your niche. "I'll add your link to my partners page if you add mine" works when it's a real partnership, not a link exchange scheme.
11.11 NAP consistency (boring but critical)
NAP = Name, Address, Phone. Across every platform — GBP, LinkedIn, Instagram, podcast bio, directories, every property — your NAP must be identical character-for-character.
Example template (your own):
Name: Versorix Inc.
Address: Aurora, Ontario, Canada
Phone: (647) 527-8700
Email: [email protected]
Website: https://www.versorix.com
Google cross-references these across the web. Mismatch ("Versorix Inc" vs "Versorix LLC" vs "Versorix Marketing") = trust ding and confusion in the local pack. Identical NAP everywhere = trust signal that compounds across months.
Maintain a single text file with your canonical NAP. Copy-paste it every time you fill out a new profile. Never type it from memory.
11.12 Realistic backlink growth timeline
| Timeframe | What's done | Expected backlinks |
|---|---|---|
| Day 1 (afternoon) | Tier 1 audit complete: GBP + LinkedIn personal + company + IG bio + podcast show notes + YouTube + Twitter | ~20-30 |
| Weeks 1-2 | Tier 2 directory submissions (1/day) | +5-10 |
| Months 1-3 | Tier 3 ongoing (HARO + guest posts + podcast guesting) | +6-12/month |
| Month 3 total | All three tiers active | 50-80 backlinks from 20-30 referring domains |
This moves the "External factors" audit score from 18% (typical launch baseline) to 70%+ without doing anything spammy.
Most operators skip this work because it's slow and feels less productive than building features. It's the highest-leverage marketing work that exists. Treat it as a 30-min/day discipline, not a one-time push.
12. Phase 9: Verification + monitoring
After deploy + account setup, verify everything works.
12.1 External audits
Run these and aim for green:
| Tool | URL | Target |
|---|---|---|
| PageSpeed Insights | https://pagespeed.web.dev | Mobile 80+, Desktop 90+ |
| SEO Site Checkup | https://seositecheckup.com | SEO 90+, AI Visibility 85+ |
| Security Headers | https://securityheaders.com | Grade A or better |
| SSL Labs | https://www.ssllabs.com/ssltest | Grade A+ |
| Schema Validator | https://validator.schema.org | All schemas detected, no errors |
| Rich Results Test | https://search.google.com/test/rich-results | FAQ + LocalBusiness eligible |
| AEO Checker (Framer) | https://framerai.com/aeo-checker | 90+/100 (100/100 ideal) |
| Mobile Friendly | https://search.google.com/test/mobile-friendly | Pass |
If SEO Site Checkup flags any of these specific fails, see §8.7 (audit-pass essentials) for the fix: "too many H2 tags," "render-blocking resources," "modern image format," "URL canonicalization," "nofollow tag," "ads.txt missing."
12.2 Tracking verification
Install browser extensions:
- Meta Pixel Helper — verify pixel fires on every page
- Google Tag Assistant — verify GA4 + Google Tag fire
- Microsoft Clarity — log into dashboard, confirm sessions appear within 30 min of first traffic
12.3 Cross-device QA
Open the site on:
- Desktop Chrome
- Desktop Safari (Mac users) / Firefox
- Phone (iOS + Android if possible)
- Tablet
Check on each:
- All 6 pages render
- All internal navigation works
- Forms / booking widget functional
- No console errors (F12 → Console tab)
- Page transitions smooth
- Images load correctly
13. Build bugs to avoid
These are the specific bugs we hit building versorix.com and then the Accu-Track and The InjectR sites. If you follow this SOP, you'll skip them. Bugs 1 through 7 are from the original Versorix build; 8 through 10 came from the sites built after it.
Bug 1: Prerender captures dead blob URLs
Symptom: After "pre-rendering" the React app via Playwright, the live site shows just a black loading screen.
Cause: Claude Design's bundler uses blob:null/ URLs at runtime to load React + JSX pieces. These blob URLs are scoped to the original document — they die when served from a different origin.
Fix: Don't pre-render via Playwright. Use Claude Design's multi-page output, or pre-render with a tool that knows about the bundler architecture.
Bug 2: CSP blocks blob: in connect-src
Symptom: Live site shows the bundler loading screen forever, never renders content.
Cause: Strict CSP with connect-src 'self' blocks the bundler's fetch(blob:...) calls.
Fix: Either remove CSP entirely (Cloudflare provides DDoS at the edge so security baseline is still OK), or include blob: in connect-src, script-src, and img-src.
Bug 3: $ chars in regex replace corrupt output
Symptom: Bundler template JSON corrupted at random positions.
Cause: String.prototype.replace(regex, '$1...') treats $ in the replacement string as a back-reference. If your replacement string contains literal $ characters (from CSS, copy, or JSON), they get expanded into matched-substring text.
Fix: Use a function replacer: html.replace(regex, (full, g1, _, g3) => g1 + content + g3). Function replacers don't interpret $.
Bug 4: Image regex corrupts JSON-encoded template
Symptom: Live site shows "Bad control character in string literal in JSON at position XXXX" error.
Cause: A regex that adds attributes to <img> tags runs after the bundler template has been re-stringified into JSON. The regex finds <img> tags inside the JSON string and modifies them with unescaped quotes, corrupting the JSON.
Fix: Apply image modifications to the PARSED inner HTML BEFORE re-stringifying it. Never let regex helpers walk the entire output if it contains JSON-encoded HTML.
Bug 5: Analytics scripts double-injected
Symptom: Console shows [Meta Pixel] Duplicate Pixel ID warning, possible [bundle] error, animations stop working.
Cause: Bundler swaps the document via document.documentElement.replaceWith(). If analytics scripts are in BOTH the outer HTML AND the post-swap template, they re-initialize after the swap, triggering errors that cascade.
Fix: Wrap analytics blocks in <!-- OUTER-ONLY-START --> ... <!-- OUTER-ONLY-END --> markers. Build script strips these for the inner-template injection. Meta/SEO/schema content is safe to duplicate; analytics scripts are not.
Bug 6: iOS phantom hover blocks animations
Symptom: Hero animation that works on desktop is blank on mobile until you tap.
Cause: iOS Safari emulates mouseenter on first touch. If your component uses onMouseEnter to control state, the phantom hover triggers state changes that block the animation.
Fix: Either use @media (hover: hover) CSS to ensure hover-dependent behavior only fires on devices that actually have hover, or wrap the component in @media (hover: none) { display: none; } to hide on touch devices entirely.
Bug 7: injectBeforeBodyClose matches wrong </body>
Symptom: HTML body injection corrupts the template script tag.
Cause: A bundler template script tag contains JSON-stringified HTML with a literal </body> inside the string. A regex looking for </body> finds the one inside the script first, not the real document close.
Fix: Use lastIndexOf('</body>') instead of regex. The real outer document </body> is always the LAST occurrence in the file.
Bug 8: iOS won't autoplay video when muted is an attribute, not a property
Symptom: A hero or background video that autoplays fine on desktop is blank or frozen on mobile until the user taps. (The The InjectR mobile site: "none of the videos play on mobile.")
Cause: Two things stack. (1) iOS Safari ignores the HTML muted attribute once JavaScript assigns the src (common with lazy-load: the script sets src and calls play() but never sets the muted property). An unmuted video cannot autoplay, and AI-generated clips (Higgsfield, Seedance) ship with an audio track, so iOS blocks them. (2) A prefers-reduced-motion gate that skips ambient or decorative loops entirely, so they never start.
Fix: Set muted and playsInline as DOM properties (v.muted = true; v.playsInline = true), not just attributes, before calling play(), and also setAttribute('muted','') for good measure. Retry play() on the loadeddata event. Do not gate purely decorative or ambient loops behind reduced-motion (reserve that gate for motion that actually conveys meaning).
Bug 9: an "Import from clicks" conversion action never fires from a website tag
Symptom: Google Ads shows zero conversions for weeks even though the campaign is getting clicks and the page clearly works. (Accu-Track ran at "0 conversions" for eight weeks on this.)
Cause: The existing Google Ads conversion action is of type "Website (Import from clicks)," which expects an offline GCLID upload from your CRM. It looks like a configured conversion, but no website tag or label was ever generated for it, so nothing on the page can fire it. If the CRM-to-Google upload is not running either, you get silent zero.
Fix: Create a NEW conversion action of type Website (page-load or click). That gives you a real firing snippet and a label (AW-XXXX/yyyy). Install the global tag on every page and the conversion event on the success page only. Set the old import action to Secondary so you do not double-count.
Bug 10: a thank-you-page conversion never fires because the form shows an inline confirmation
Symptom: The conversion tag is installed on /thank-you and validates in Tag Assistant, but real form submissions never register a conversion.
Cause: The conversion fires on /thank-you page LOAD, but the form does not actually navigate there. An embedded (iframe) form submit is cross-origin, so it shows an inline "thanks" inside the iframe and the parent page's gtag or pixel can never see it. No navigation to /thank-you means no page-load conversion.
Fix: Set the form to redirect to a same-domain /thank-you URL on submit (then the page-load tag fires and gtag captures the GCLID, since static hosting preserves URL params and auto-tagging is on). If a redirect is not possible, switch to a submit-event or button-click trigger instead of a page-load trigger. Confirm the actual post-submit behavior before launch, do not assume the form redirects.
Bug 11: scroll-animation frames excluded from the git push, animation dies live
Symptom: A scroll-driven video or frame animation works on localhost but is blank or frozen on the deployed site.
Cause: The frames folder (often a few hundred WebP files) was gitignored or otherwise left out of the push, so the live deploy has no frames to render.
Fix: Commit the frames to the repo (or host them and reference absolute URLs), and verify they are present in the deployed build, not just locally. From the scroll-driven video technique (see the Design & Copy Rulebook §8.4).
14. Ongoing workflow
After launch, day-to-day operations.
14.1 Update content
To change copy on any page:
- Edit the JSX file in
src/(e.g.,src/home.jsx) - Run
npm run buildfromscripts/directory git add .,git commit -m "Update home copy",git push- Cloudflare auto-deploys in 30-60 seconds
14.2 Add a new page
- Create the JSX file in
src/(e.g.,src/blog.jsx) - Create the HTML entry file at root (e.g.,
blog.html) — copy from existing page, update title/description/canonical/window.__route - Add to
app.jsxrouter - Add to nav in
shared.jsx - Add body SEO content in
scripts/body-seo/blog.html - Add to
sitemap.xml - Run build, commit, push
14.3 Update tracking
If you get new tracking IDs or want to add a new tool:
- Edit each HTML file's
<head>to add the new script - (Or write a head-injection script that walks all HTML files — saves manual work)
- Run build, commit, push
14.4 Regenerate from Claude Design
When you make major design changes in Claude Design:
- Export the new files to a folder
- Carefully copy over: HTML files, src/ folder, styles/ folder
- PRESERVE: your build pipeline (scripts/), your favicons (favicon.ico + assets/favicon-*), your llms.txt, your sitemap.xml, your _headers, your body SEO content (scripts/body-seo/)
- Run build, verify, commit, push
Because every post-export edit lives in a re-runnable script (§5.5), step 4 restores tracking, schema, and body-SEO automatically. If any of that was hand-typed into the old HTML, the new export just deleted it. Check that conversions still fire after a regenerate.
15. Cost breakdown
| Item | Cost | Notes |
|---|---|---|
| Domain registration | $10-30/year | Buy from any registrar. Cloudflare is cheapest. |
| Cloudflare Pages hosting | $0/year | Free tier covers everything for a marketing site. |
| GitHub repo | $0/year | Free for private repos. |
| Cloudflare DDoS / WAF | $0/year | Free tier. |
| Cloudflare SSL | $0/year | Auto-provisioned. |
| Google Analytics 4 | $0/year | Free. |
| Google Tag Manager | $0/year | Free. |
| Microsoft Clarity | $0/year | Free unlimited heatmaps + recordings. |
| Meta Pixel | $0/year | Free. |
| Google Search Console | $0/year | Free. |
| Bing Webmaster Tools | $0/year | Free. |
| Google Business Profile | $0/year | Free. |
| Claude Design | $20/month | Subscription, but you can pause between projects. |
| Total recurring | $10-30/year + Claude Pro subscription |
Compare to:
- Framer: $30+/month = $360+/year
- Squarespace: $16+/month = $192+/year
- Webflow: $14+/month = $168+/year + transaction fees on commerce
This stack saves $150-350/year over the popular alternatives, while being faster, more customizable, and better optimized for AI-driven discovery.
16. Appendix A: File structure reference
Final repo structure for reference:
your-site/ (git repo root)
├── .gitignore (excludes node_modules, etc.)
├── 404.html (branded 404 page)
├── _headers (Cloudflare cache + security headers)
├── about.html (multi-page entries)
├── contact.html
├── favicon.ico (root-level, browsers auto-fetch)
├── index.html (home page)
├── llms-full.txt (deeper AI crawler doc)
├── llms.txt (AI crawler standard)
├── method.html
├── privacy.html
├── README.md (deploy + update workflow doc)
├── robots.txt (crawler instructions)
├── site.webmanifest (PWA manifest)
├── sitemap.xml (search engine sitemap)
├── terms.html
│
├── assets/
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico (also a copy here)
│ ├── og-image.png (1200x630 social preview)
│ ├── brand/ (logo variants)
│ ├── logos/ (your logos + client logos if any)
│ └── visuals/ (custom illustrations, screenshots)
│
├── scripts/
│ ├── body-seo/ (per-page hidden SEO content)
│ │ ├── about.html
│ │ ├── contact.html
│ │ ├── index.html
│ │ ├── method.html
│ │ ├── privacy.html
│ │ └── terms.html
│ ├── compile-jsx.mjs (the main build script)
│ ├── optimize-images.py (image compression — optional)
│ ├── package.json (Babel + Playwright deps)
│ └── package-lock.json
│
├── src/
│ ├── about.jsx (JSX page components — source of truth)
│ ├── app.jsx (router)
│ ├── contact.jsx
│ ├── home.jsx
│ ├── layered-text.jsx (animated components)
│ ├── legal.jsx (privacy + terms in one file)
│ ├── loading-screen.js
│ ├── method.jsx
│ ├── motion.jsx (animation hooks)
│ ├── page-transitions.js (wipe transition logic)
│ ├── perspective-marquee.jsx
│ ├── shared.jsx (Nav, Footer, common components)
│ │
│ └── compiled/ (build output — git-tracked)
│ ├── about.js
│ ├── app.js
│ └── ... (one .js per .jsx)
│
└── styles/
├── colors_and_type.css (design tokens)
├── site.css (all site styles)
└── fonts/ (font files)
The component file names above (
layered-text.jsx,perspective-marquee.jsx,page-transitions.js,loading-screen.js) are from the example build. YOURsrc/folder will be named for YOUR design and should look different. The folders, the build files (scripts/,compile-jsx.mjs), and the token file are the parts that stay identical across every site.
17. Appendix B: Decision tree for architectural choices
When facing common decisions:
Single-page vs multi-page React
- Single-page (one HTML file, hash routes): only if your entire site fits one viewport scroll and you have no SEO ambitions. Almost never the right answer for marketing sites.
- Multi-page (separate HTML files): default. Real URLs, sitelinks possible, route-level meta, browser history works.
Pre-rendered vs client-rendered
- True pre-rendered HTML (content baked into markup): best for SEO/AEO. Harder to get from Claude Design — usually requires a custom build step or rewriting in Next.js / Astro.
- Client-rendered with hidden body SEO content (what this SOP uses): pragmatic middle ground. JS-less crawlers see hidden semantic content; users see the React UI on top.
Cloudflare Pages vs Vercel / Netlify
- Cloudflare Pages: best free tier (unlimited bandwidth, requests). What this SOP uses.
- Vercel / Netlify: easier Next.js integration, but their free tiers have bandwidth/build minute limits that can bite at scale.
Custom domain via CNAME-only vs full DNS migration
- CNAME-only: keep your registrar's DNS, just add a CNAME for
www. Safer if you have email on the domain. - Full DNS migration: needed only if you want native apex (no
www) resolution OR want to use Cloudflare's full CDN features. Requires preserving every existing DNS record perfectly.
When to compile JSX → JS at build time
- Always, unless you're prototyping. In-browser Babel runtime is ~500KB of overhead per page load. Removing it via build-time compilation is one of the highest-ROI optimizations.
When to add Content-Security-Policy
- Eventually, not initially. CSP requires knowing every script, style, and connect endpoint your site uses. Get the site working first, then audit and add CSP later. Other security headers (HSTS, X-Frame, X-Content-Type, Referrer, Permissions) are higher ROI and lower risk.
React/JSX export vs static HTML export
- React/JSX export (the common case): you get HTML entries plus a
src/of JSX. Run the full Phase 2 pipeline (compile JSX, React dev to prod, strip Babel). - Static HTML/CSS/JS export (what The InjectR got): no JSX, no bundler. The JSX compile in §5.2 has nothing to do. Skip it. You still run everything else: body-SEO injection, meta/schema, image optimization, tracking,
_headers, deploy. Check which one you have before starting Phase 2 so you do not hunt for JSX that is not there.
18. Strategic lessons (not technical bugs)
§13 catalogs the technical bugs. This section is the strategic layer — what I'd tell someone starting fresh that isn't a discrete bug. The meta-learnings.
18.1 Ship 6 pages, not 12
The instinct is comprehensive: home, about, multiple services pages, case studies, blog, pricing, FAQ, contact, privacy, terms. Twelve pages.
Resist this. Ship six. Home, about, method, contact, privacy, terms. That's it.
Every additional page is more copy to write, more SEO to maintain, more places where positioning can drift. A six-page site that's tight beats a twelve-page site that's mid. You can always add later. You can't retract.
Pages NOT to ship at launch:
- Pricing page — gives prospects an out before the conversation. Handle pricing on the call.
- Case studies — only useful if they're great. Mid case studies actively hurt.
- Blog — high overhead, unclear ROI. Ship if you'll post 2x/month. Otherwise skip.
- FAQ — fold into the home page or method page.
18.2 Ship ugly, iterate on data
The strongest predictor of website success isn't quality at launch. It's iteration speed after launch. Sites that ship rough and iterate weekly outperform sites that ship perfect and never touch again.
Your first version should make you slightly uncomfortable. If you wait until you're proud of it, you've waited too long.
This is the website parallel to the "trial is the proposal" principle. Get it live, learn from real visitor data, fix what's broken in week two.
18.3 The hidden body SEO is the moat
Most sites built on Framer / Webflow / Squarespace can't add hidden body SEO content the way described in §8.5. Their templates don't allow it cleanly.
That means the moment your site has 1 H1, 5–7 H2s, 5–10 internal links, and 2–4 external citations per page — all readable by JS-less crawlers — you've already outranked 99% of competitor sites in your niche.
This is the highest-ROI 60 minutes of the entire build. Don't skip it.
18.4 Owned beats rented over 3+ years
Framer at $30/mo × 36 months = $1,080. Webflow similar. This stack: $30 total for 3 years (just domain renewal).
That's a $1,050 difference. But the bigger cost of rented platforms is migration risk. The day Framer doubles pricing, raises feature gating, or shuts down, every site on the platform has 90 days to relocate.
Owned infrastructure has no migration day. You move when you want to.
18.5 AI slop detection
Claude Design produces good visual design but generic copy by default. Flag and rewrite:
- Em-dashes everywhere (—) — signals AI writing. Replace with periods.
- "Industry-leading," "best-in-class," "transformative" — generic. Replace with specifics.
- "Empower your business" / "unlock your potential" — bullshit verbs. Kill them.
- Lists of 3 things connected by " — " — AI rhythm. Break into separate sentences.
- "We help X achieve Y" patterns — flatten to "X gets Y."
- "Trusted by industry leaders" — only if literally true. Otherwise cut.
If your copy reads like every other agency site, no one remembers it. Specifics win. Numbers win. Names win.
18.6 Strategy → copy → design. Never the reverse.
The biggest mistake people make: open Claude Design first, design something pretty, then try to retrofit copy into the design.
The result: copy that has to fit the layout, not copy that says what you mean. Every word becomes a compromise.
Reverse it. Write the copy in a plain text doc. Then ask Claude Design to design AROUND the copy. The design is the servant of the words, not the master.
18.7 AI crawlers first, humans second
Two years from now, more new client research will start in ChatGPT / Perplexity / Claude than in Google Search. The infrastructure to be found by AI tools is different from classic SEO infrastructure.
This SOP builds for both. But if you have to prioritize, prioritize AI crawlers. llms.txt, llms-full.txt, hidden body SEO with semantic structure, Schema.org JSON-LD, descriptive H1s without brand names.
The AEO score isn't a vanity metric. It's the leading indicator of whether your site gets cited when an AI tool recommends services in your category.
18.8 Sequencing of post-launch tasks matters
Not all account-side setup is equal value per hour. Sequence:
- Hour 1: GSC verify + sitemap submit + URL inspection for every page. (Highest value — gets you indexed.)
- Hour 2: GBP setup with full city list + descriptions + photos. (Highest local-search ROI.)
- Hour 3: GA4 conversion events + Meta Events Manager Lead/Schedule events. (Without these, ads can't optimize.)
- Hour 4: Bing Webmaster Tools (5 min) + LinkedIn/YouTube brand consistency.
Most operators do these out of order — they spend 4 hours on the LinkedIn page before the site is even indexed. Don't.
18.9 Audit checkboxes are not all equal value
A standard SEO audit returns 8–15 "failures" the first time you run it. Three things to know:
- Most fails have known fixes (§8.7 audit-pass essentials covers ~80% of them). Knock those out and you'll go from ~80/100 to 95+/100 in under an hour.
- Some "fails" are correct as-is. Empty alt attributes on decorative imgs are WCAG-correct. Plaintext mailto in your footer is intentional for accessibility + catch-all spam handling. The audit tool can't tell the difference between "you forgot this" and "you decided against this." Document the conscious skips in your README.
- A few fails aren't worth the engineering cost. The ">20 HTTP requests" test is failed by 93% of even the top 100 sites — third-party tracking (GTM + Pixel + Clarity) alone is ~6 requests you can't bundle away. If hitting it would require self-hosting fonts and dropping observability tools, that's not a real win.
The goal is substantive performance + real discoverability, not an arbitrary green score. A site that loads in 1.5s with proper Schema.org + body SEO + LCP preload beats a site that scores 100/100 on a checkbox audit but takes 4s to paint. Use the audit as a checklist, not a god.
18.10 The 60/30/10 rule for build time
A first build, if you follow this SOP exactly, breaks down as:
- 60% Phase 0 (strategy + copy). If you skip this, you'll spend it elsewhere — usually as ten rewrites mid-build.
- 30% Phases 1–4 (design, build pipeline, deploy, DNS). The actual technical work. Less than people expect.
- 10% Phases 5–9 (SEO, tracking, performance, account setup, verification). Mechanical. Just follow the checklist.
If your build feels heavy on the technical side, it's because Phase 0 wasn't done.
18.11 The stack repeats, the design never does
This is the one that took longest to learn, and the reason this SOP was rewritten. The technical stack is meant to be copied byte-for-byte: pipeline, SEO, tracking, deploy. That's the value, and it's identical on every site.
The design is the opposite. Reuse the last site's palette, layout, type, and motion and you've shipped a clone, and a clone signals "template" to the exact buyers who are paying for "custom." The plumbing is invisible. The look is the entire first impression. So treat Phase 1 (§4) as the place you deliberately diverge: a fresh Design DNA every time, and the anti-clone gate (§4.8) before every export. Same engine, different car, every single build.
18.12 Diagnose before you rebuild a "zero-converting" site
When a site is taking traffic and converting nobody, the instinct is to rebuild it prettier. Resist until you have separated the three possible causes, because a rebuild only fixes one of them:
- Page experience and trust. Slow, heavy, "rented-funnel" feel. A fast, branded, credible custom site fixes this, and it is also the Quality Score lever. This is usually the right bet.
- Form friction. Too many fields for the intent level. Fixed by the form, not the design.
- Traffic quality. If the ads are sending researchers, students, or tire-kickers instead of buyers, a perfect page still converts zero. Pull the search-terms report. A rebuild does NOT fix this.
The Accu-Track site had genuinely zero leads over months. The rebuild was the right call, but only after confirming it was a page-experience problem and auditing the traffic in parallel. Do not just ship a prettier zero.
18.13 At zero volume, ship a light form; add qualifying friction once you have leads
A heavy qualifying form ("three silvers, not a hundred bronzes") is correct only once you have volume to filter. At zero leads it strangles the only signal you have. You cannot qualify leads you do not have. Launch with a light form (name, email, company, phone, one dropdown), prove the page converts, then add the qualifying fields. This is the form-level version of "ship ugly, iterate on data" (§18.2).
End of SOP
This document is a snapshot of what works as of 2026-06-23. The tooling will evolve. Update this document when patterns shift. The architecture is opinionated but the principles are general: fast, owned, discoverable, free.