Two SEO/UX gaps closed:
1. Per-page link previews. Every page now emits its own og:title,
og:description, og:url, og:image, og:type, og:site_name + matching
twitter:* tags via @unhead/vue. Sharing /about to iMessage/Slack/X
shows the page-specific card instead of a site-wide one. Pages can
override image/ogType/twitterCard via the meta object.
- New src/utils/socialMeta.js (pure, 19 tests).
- usePageMeta calls it and pushes the result into useHead's meta[].
- templates/index.html drops the EJS-rendered <title>, meta
description, og:*, and twitter:* lines (now per-page from
usePageMeta). Site-wide JSON-LD Organization snippet stays.
2. Real 404 page. usePageConfig.selectPage returns a __not_found__
sentinel with isNotFound:true and components:[NotFound] for any
non-root path that doesn't match a page. Sites can author
pages/404.json to override. The plugin pre-renders /404 (via
includedRoutes) and copies dist/404/index.html → dist/404.html
in ssgOptions.onFinished, so AWS Amplify auto-serves it for
unmatched URLs with HTTP 404.
- New src/components/NotFound.vue (theme-token CSS, WCAG 2.2 AA).
- usePageMeta emits "Page not found — {site}" title, suppresses
description, emits noindex meta, skips canonical/hreflang/OG.
BREAKING (observable): unknown URLs no longer silently render the home
page (HTTP 200). They now render the bundled NotFound (or per-site
override) and Amplify serves dist/404.html with HTTP 404. The previous
behavior was almost always wrong for SEO; sites that depended on it
should audit (rare).
Tests: 479 passing (19 new for socialMeta, 8 added to usePageMeta for
OG/Twitter, 7 for 404 handling).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>