SEO technical foundation: canonicals, local-entity schema, sitemap/robots, per-route OG images - #53
Merged
Merged
Conversation
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>
Contributor
🔍 Preview Build ReadyBuild Status: ✅ Success 📦 Preview artifact uploaded - check the Actions tab to download.
|
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
<link rel=canonical href=https://bidwell.info>sitewide, telling Google all sub-pages were duplicates. Every page now declares a relative self-canonical; a newmetadata-policy.test.tsglobs every page module and permanently enforces complete metadata + title/description/canonical uniqueness.status: 'active'./career-guidancewas missing from the sitemap entirely; the sitemap now lists only indexable pages, drops build-time-fabricatedlastModified, and its health-check contract (absoluteurl+ numericpriority) is test-guarded./_next/disallowed) and blocked all AI crawlers — now open (deliberate: AI answers are a discovery channel).new Date()) and its JSON-LD author was the company — real publish date (2025-11-27) in aposts.tsregistry, valid article metadata, entity-linked JSON-LD.Local-entity structured data (typed)
New
lib/structured-data.tsbuilders (viaschema-dts, dev-only dep),@id-linked: ProfessionalService with city-level Minneapolis address + Twin CitiesareaServed(wasOrganization / 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 caughtserviceTypebeing invalid on Organization. Orgemaildeliberately withheld until the forwarding alias exists (PR 2).Share cards
The
/ogroute ignored its?title=param — one identical card everywhere. Replaced with build-timeopengraph-image.tsxfiles per route (verified underoutput: 'export'), globaltwitter:card. Subtle gotcha found by the new e2e spec: a page-levelopenGraphconfig 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 nonexistentfavicon.ico, placeholder verification tokens removed, viewport moved to theviewportexport.lib/site-config.tsbecomes the single source of truth for identity/origin/contact facts.Test plan
npm run qa:fullgreen (type-check, Biome, coverage, production build)npm run test:e2e11/11 — newe2e/seo.spec.tsasserts the rendered export: single self-canonicals, stub noindex, PNG-valid og:image, open robots.txt, pruned sitemap, JSON-LD entity graphnpx tsx scripts/generate-health-checks.tsproduces correct checks from the new sitemap🤖 Generated with Claude Code