# SEO_SPEC.md

## 1. Goals & reality check

Primary discovery is *direct* (links from applications/LinkedIn/CV), not organic search. SEO effort therefore targets: (1) branded queries ("Connor Babbington developer") ranking the new site #1 in both languages, (2) clean link-preview cards everywhere recruiters paste URLs (Slack/Teams/LinkedIn/ATS), (3) not losing the existing domain's standing during the WordPress → new-site cutover.

## 2. Per-page metadata (both locales)

- **Titles** (patterns fixed in CONTENT_STRATEGY §7): home `Connor Babbington — Systems Developer (.NET, React) · Tønsberg, Norway` / `… Systemutvikler … · Tønsberg`; inner pages `<Page> · Connor Babbington`. ≤ 60 chars target.
- **Descriptions:** handwritten per page per locale, ≤ 155 chars, answer who/what/where. Stored in page-meta content (DATA_MODEL §6), length CI-checked.
- **Canonical:** self-referencing per locale page. **hreflang:** `en`, `nb`, `x-default → en` emitted from the slug map on every page — the twin-URL contract guarantees correctness; CI tests hreflang reciprocity.
- **robots:** index,follow everywhere except 404/colophon-draft states; `robots.txt` allows all + sitemap pointer.

## 3. Structured data (JSON-LD, build-time)

| Page | Schema |
|---|---|
| Home (both locales) | `Person`: name, jobTitle "Systems Developer"/"Systemutvikler", address (locality Tønsberg, country NO), email, sameAs [LinkedIn, git.cesnimda.uk], knowsLanguage [en, nb], knowsAbout [C#, .NET, React, Docker, …] |
| Case studies | `CreativeWork` (subtype `SoftwareApplication` where honest — JobTrack yes, InboxIntel as `SoftwareSourceCode`): name, description, author → Person, programmingLanguage, dateModified |
| CV hub | `Person` + `subjectOf` links to CV files |
| All | `WebSite` + `BreadcrumbList` on nested pages |

Builders are typed utilities with unit tests (TECH_SPEC §9.2) — malformed JSON-LD is worse than none.

## 4. OpenGraph / link previews

- Per-page, per-locale OG: `og:locale` (`en_GB`/`nb_NO`) + `og:locale:alternate`, title/description mirroring meta, `og:type` profile (home) / article (case studies).
- **OG images pre-generated at build** (one template per page kind, design-system styled: dark surface, name/page title, role line, trace motif, monogram; case studies add project name + stack line). 1200×630 PNG, per locale. Twitter/X summary_large_image tags mirrored.
- This directly fixes the current site's empty `og:title` embarrassment noted in Phase 1 research.

## 5. Sitemaps

- Build-generated sitemap index → one sitemap per locale; entries carry `xhtml:link` alternates (belt-and-braces with hreflang tags); `lastmod` from git commit dates of content files.
- Submitted via Search Console (both properties/domains as relevant) at launch; the old WP sitemap URLs left to 404→410 after cutover inventory.

## 6. Migration / cutover protection

1. **Inventory before cutover** (Phase 3 task): crawl current WP site, list indexed URLs (`site:cesnimda.co.uk`), old anchors, and any URL known to be printed/shared (esp. `/Linkedin` redirect — preserved per ROUTING_SPEC).
2. 301 map for anything with inbound value; 410 for WP cruft (`/feed/`, `/wp-json/`, `/comments/`…). Never soft-404.
3. Keep domain, keep HTTPS, no `www`/apex flip during migration (site currently canonicalises to apex — retain).
4. Verify: Search Console coverage watch for 4 weeks post-launch; link-preview spot checks in LinkedIn/Slack debuggers.

## 7. Performance & crawlability as SEO

Static HTML, content not JS-gated (crawler parity guaranteed by architecture), CWV budgets in CI (LCP < 1.8 s, CLS < 0.02) — the technical-SEO checklist is mostly satisfied *by construction*; this spec adds only the markup/meta layer above.
