Four faults in the one build step that decides what a crawler receives.
Closes#233 and #232.
1. Every page preloaded /logo.png. The hero hint was keyed on the first
`loading="eager"` image, and that is the header logo, on every page. So the
image each page actually paints first was never preloaded, and React's own
preload for it was being discarded as a duplicate. It now keys on the image
React marked with a high fetch priority, matched case-insensitively, because
React writes the attribute camelCase in HTML and a case-sensitive match would
have quietly removed every preload instead.
2. The hoist moved ANY title, meta or link out of the body into <head>. An
inline <svg><title> is a picture's label, and microdata rides in
<meta itemprop>: both would have become page-level head tags the moment the
long-form copy carried an icon with a title. SVG blocks are now parked before
the hoist, and itemprop tags stay where they are.
3. `page.replace('</head>', body)` interprets `$&`, `$'` and `$$` INSIDE the
replacement, and the replacement is page copy. React escapes & into &, so
any `$` immediately before an escaped character injected markup. Copy carries
no `$` today; the next page with a price would have. Replacement is now a
split and join, and each marker must appear exactly once.
4. A render error named no route, and a Suspense fallback shipped silently as
an empty page. Both now fail the build and say which route.
Also refuses to run over its own output: dist/index.html is both the template
and the home page, so a second run without a rebuild gave every page two
canonicals.
Proven by mutation, each restored afterwards: a probe <svg><title> in the footer
stays in the body on all 19 pages and no page gains a second title; a Suspense
boundary fails the build naming the route; a second prerender run refuses; copy
reading "Save $10 & more" reaches the page literally with one root div; and
every page now preloads its own hero, with none preloading the logo.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Client directive (Levi Halford, 2026-08-01) ahead of Google/Meta lead forms.
Privacy policy:
- Publish approved policy verbatim at /privacy-policy (src/data/privacyPolicy.js
is the single source of truth; 292/292 source lines verified present)
- Privacy Policy link in the footer of every page
- Effective/Last Updated 2026-07-31, [email protected] as mailto
Remove St. Petersburg street address from every surface named in the brief:
footer, contact page, schema markup, SEO metadata, Google Maps links. Collapse
ProfessionalService + Organization schema into a single Organization with
areaServed: United States; drop geo coordinates, priceRange, openingHours.
Add the approved US-coverage sentence to About. No replacement address.
Crawler visibility (the site previously served 0 bytes of body HTML without JS):
- Prerender all 19 routes at build time via src/entry-server.jsx + scripts/prerender.js
- Hoist title/meta/canonical/JSON-LD into <head>; renderToString does not do this
and react-helmet-async's context is empty under React 19
- Serve prerendered HTML; return a real 404 for unknown paths instead of 200
- Hydrate instead of discarding the prerendered markup
SEO/perf:
- Titles <=60 and descriptions <=160 chars across all pages
- Add BreadcrumbList to interior pages, WebSite to home
- Generate sitemap.xml from the route list with git-derived lastmod
- 301 duplicate URL forms (trailing slash, //, /index.html), preserving query
- Immutable caching for content-hashed assets; no-cache for HTML
- Split the 522 KB bundle into app/react-vendor/router/icons
- loading/decoding/fetchpriority + per-route hero preload; drop unused asset
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>