Skip to content

SEO technical foundation: canonicals, local-entity schema, sitemap/robots, per-route OG images - #53

Merged
dralgorhythm merged 8 commits into
mainfrom
claude/seo-technical-foundation
Jul 21, 2026
Merged

SEO technical foundation: canonicals, local-entity schema, sitemap/robots, per-route OG images#53
dralgorhythm merged 8 commits into
mainfrom
claude/seo-technical-foundation

Conversation

@dralgorhythm

Copy link
Copy Markdown
Owner

Summary

PR 1 of 2 of the Twin Cities SEO update (plan: artifacts/plan_seo_optimization.md). Pure technical foundation — no positioning/copy changes (those ship with the content in PR 2, so titles never mismatch page content).

Indexing bugs fixed

  • Every page canonicalized to the homepage — the root layout hardcoded <link rel=canonical href=https://bidwell.info> sitewide, telling Google all sub-pages were duplicates. Every page now declares a relative self-canonical; a new metadata-policy.test.ts globs every page module and permanently enforces complete metadata + title/description/canonical uniqueness.
  • 12 duplicate-title experiment stubs (no metadata exports, indexable, sitemapped) now get registry-driven unique metadata + noindex until status: 'active'.
  • /career-guidance was missing from the sitemap entirely; the sitemap now lists only indexable pages, drops build-time-fabricated lastModified, and its health-check contract (absolute url + numeric priority) is test-guarded.
  • robots.txt starved Google's renderer (/_next/ disallowed) and blocked all AI crawlers — now open (deliberate: AI answers are a discovery channel).
  • Blog post re-dated itself every build (new Date()) and its JSON-LD author was the company — real publish date (2025-11-27) in a posts.ts registry, valid article metadata, entity-linked JSON-LD.

Local-entity structured data (typed)

New lib/structured-data.ts builders (via schema-dts, dev-only dep), @id-linked: ProfessionalService with city-level Minneapolis address + Twin Cities areaServed (was Organization / areaServed: Worldwide), Person = Jordan Winters working from Minneapolis (was company-named, occupationLocation: Remote), WebSite without the SearchAction that targeted a nonexistent /search, plus FAQPage (live on career-guidance), Service/Breadcrumb/BlogPosting builders. schema-dts caught serviceType being invalid on Organization. Org email deliberately withheld until the forwarding alias exists (PR 2).

Share cards

The /og route ignored its ?title= param — one identical card everywhere. Replaced with build-time opengraph-image.tsx files per route (verified under output: 'export'), global twitter:card. Subtle gotcha found by the new e2e spec: a page-level openGraph config replaces the inherited parent object including ancestor file-convention images — the policy test now enforces a colocated image for every such page.

Hygiene

Real PNG icons (were 376 KB JPEGs masquerading as .png), manifest no longer references a nonexistent favicon.ico, placeholder verification tokens removed, viewport moved to the viewport export. lib/site-config.ts becomes the single source of truth for identity/origin/contact facts.

Test plan

  • npm run qa:full green (type-check, Biome, coverage, production build)
  • npm run test:e2e 11/11 — new e2e/seo.spec.ts asserts the rendered export: single self-canonicals, stub noindex, PNG-valid og:image, open robots.txt, pruned sitemap, JSON-LD entity graph
  • npx tsx scripts/generate-health-checks.ts produces correct checks from the new sitemap
  • Unit suite 240/240 (was 200 — 40 new assertions)
  • Post-merge: health-check workflow green against the live site

🤖 Generated with Claude Code

dralgorhythm and others added 8 commits July 21, 2026 16:53
Single dependency-free source of truth for name, origin, contact, founder,
and location facts. baseUrl moves out of app/sitemap.ts; all importers
migrate. Relative import in the sitemap graph keeps the tsx-executed
health-check script working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the hardcoded type-switch component with pure builders in
lib/structured-data.ts, linked by stable @ids: ProfessionalService with a
city-level Minneapolis address and Twin Cities areaServed (was
Organization/areaServed: Worldwide), Person = Jordan Winters working from
Minneapolis (was company-named person working Remote), WebSite without the
SearchAction that pointed at a nonexistent /search route. schema-dts types
caught serviceType being invalid on Organization (moved to knowsAbout;
Service schemas carry serviceType per page). FAQPage schema now emitted
from the career-guidance FAQ. JSON-LD output escapes < to prevent script
breakout. Org email deliberately withheld until the forwarding alias
exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ON-LD

posts.ts becomes the post registry with the true publish date (2025-11-27
per git history) — both the index and the post re-dated themselves to the
build date via new Date(). The post's metadata drops the invalid
publishedAt/image keys for a typed Metadata with article openGraph and a
self-canonical; its inline JSON-LD (author: company) is replaced by
blogPostingSchema with @id links to the Person/Organization entities.
Breadcrumb (Home > Blog > post) now renders on the post.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove the root layout's hardcoded homepage canonical (every route
declared https://bidwell.info as its canonical, suppressing sub-page
indexing) in favor of metadata.alternates.canonical on every page. The 12
coming-soon experiment stubs — which shared one inherited title and
description — get registry-driven metadata via experimentMetadata():
unique title/description, self-canonical, noindex until active. A new
metadata-policy test globs every page module and permanently enforces
complete metadata, relative self-canonicals, and title/description/
canonical uniqueness sitewide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ages

robots.txt drops the AI-crawler block (GPTBot, ChatGPT-User, CCBot,
Claude, Google-Extended — deliberate: AI answers are a discovery channel)
and the /_next/ disallow that hid CSS/JS from Google's renderer. The
sitemap gains /career-guidance (the sales page was missing entirely),
drops the 12 noindexed coming-soon stubs, stops fabricating lastModified
from the build date (real publish date on blog entries only), and keeps
the absolute-url + numeric-priority contract the CI health-check
generator depends on — now guarded by tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The old /og route ignored its ?title= param, so every page shared one
identical share card. Replace it with build-time opengraph-image.tsx
files (shared ogCard factory in lib/og-image.tsx), verified working under
output: 'export' (hashed per-route PNGs in out/). Config
openGraph.images references to the deleted route are removed; Next
injects correct absolute og:image tags per route. twitter:card
summary_large_image is set globally.

Gotcha caught by the new e2e spec: a page-level openGraph config replaces
the inherited parent openGraph ENTIRELY, discarding ancestor file-
convention images — so every openGraph-configured page needs a colocated
opengraph-image.tsx (home and the agentic-framework page were shipping no
og:image). The metadata-policy test now enforces that invariant, and
e2e/seo.spec.ts asserts the rendered export: single self-canonicals, stub
noindex, PNG-valid og:image, open robots.txt, pruned sitemap, and the
JSON-LD entity graph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
app/icon.png and app/apple-icon.png were 1024px JPEGs (~376 KB each)
masquerading as PNGs — regenerated as real PNGs at 512x512 / 180x180;
the optimizer's generated WEBP variants and hash manifest resync
accordingly. The web manifest stops referencing the nonexistent
/favicon.ico and lists both real icons. Placeholder search-engine
verification tokens (literal 'google-site-verification' strings) are
removed. Viewport/theme-color move from hand-written meta tags to the
Next viewport export; dead X-UA-Compatible dropped. The SEO plan lands
as artifacts/plan_seo_optimization.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Preview Build Ready

Build Status: ✅ Success
Node Version: 24

📦 Preview artifact uploaded - check the Actions tab to download.

This preview was built from commit 5cc53ca

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant