···11+# Open Graph meta tags for the static pages
22+33+**Date:** 2026-06-09
44+**Status:** Approved (brainstorm)
55+66+## Goal
77+88+Give SkyPress's public, shareable static pages rich link previews on social
99+platforms (Open Graph + Twitter Cards), with an on-brand default share image.
1010+1111+## Scope
1212+1313+In scope — the three public-facing static pages a logged-out visitor would share:
1414+1515+- `/` — home (`src/pages/index.astro`)
1616+- `/lexicon` — `src/pages/lexicon.astro`
1717+- `/preview` — `src/pages/preview.astro`
1818+1919+Out of scope (follow-ups):
2020+2121+- The app screens `/editor` and `/dashboard` — tools behind sign-in, not share targets.
2222+- The dynamic `[author]` publication/article pages — they need per-record titles,
2323+ descriptions, and images; a different shape, tracked separately.
2424+- Per-page custom OG images — all three pages share one default image for now.
2525+2626+## Approach
2727+2828+Extend the shared `Base.astro` layout, which already owns `<head>` and takes
2929+`title`/`description`. The pages then inherit OG/Twitter tags from data they
3030+already pass. (Rejected: a separate `<Seo>` component or inline per-page meta —
3131+both duplicate what `Base` already centralises.)
3232+3333+## Design
3434+3535+### A. Tag-building helper — `src/lib/seo/meta.ts`
3636+3737+A pure function that returns the list of `<meta>` attribute objects, kept out of
3838+the `.astro` template so it is unit-testable (the project is TDD-locked).
3939+4040+```ts
4141+interface MetaInput {
4242+ title: string;
4343+ description?: string;
4444+ url: string; // absolute canonical URL
4545+ image: string; // absolute URL
4646+ siteName: string; // "SkyPress"
4747+ type?: string; // default "website"
4848+ imageAlt?: string;
4949+}
5050+// returns Array<{ property?: string; name?: string; content: string }>
5151+buildMetaTags(input): MetaTag[]
5252+```
5353+5454+Tags produced:
5555+5656+- `og:title`, `og:url`, `og:type`, `og:site_name`
5757+- `og:description` — **omitted entirely when `description` is absent** (no empty tag)
5858+- `og:image`, `og:image:width` (`1200`), `og:image:height` (`630`), `og:image:alt`
5959+- `twitter:card` (`summary_large_image`), `twitter:title`, `twitter:image`
6060+- `twitter:description` — omitted when `description` is absent
6161+6262+`og:*` use the `property` attribute; `twitter:*` and `name`-style use `name`.
6363+6464+### B. `Base.astro` renders them
6565+6666+New optional props (additions only — existing `title`/`description`/`phase` unchanged):
6767+6868+- `image?: string` — default `/og-default.png`
6969+- `ogType?: string` — default `"website"`
7070+- `imageAlt?: string` — default a brand-appropriate string
7171+7272+`Base` computes the canonical absolute URL from `Astro.url` resolved against
7373+`Astro.site` (`https://skypress.blog`, overridable via `PUBLIC_SITE_URL`), resolves
7474+the image to an absolute URL the same way, calls `buildMetaTags`, and renders the
7575+tags in `<head>`. It also emits `<link rel="canonical" href={url} />` since the
7676+absolute URL is already computed.
7777+7878+### C. The three static pages
7979+8080+- `index.astro` and `lexicon.astro` already pass `title` + `description` — no change
8181+ needed; they inherit the default image automatically.
8282+- `preview.astro` currently passes only `title`; add a `description` so its card has
8383+ body text (e.g. "A sample SkyPress article, rendered server-side from stored AT
8484+ Protocol blocks — zero editor JavaScript.").
8585+8686+### D. Shared OG image — `public/og-default.png`
8787+8888+1200×630 PNG, committed as a static asset. On-brand: the SkyPress sky gradient (a
8989+warm sun/golden phase), the logo sun-mark + **SkyPress** wordmark, and the tagline
9090+"A writing studio for the open social web." Authored as an HTML/SVG poster using the
9191+real palette (`global.css`) and self-hosted fonts (Overused Grotesk, IBM Plex Mono),
9292+then rasterized to PNG at exactly 1200×630 via a headless browser so the custom
9393+fonts render. The authoring source need not ship; only the PNG is required.
9494+9595+### E. Testing & verification
9696+9797+- **Unit (TDD, red first):** `src/lib/seo/meta.test.ts` for `buildMetaTags` —
9898+ asserts the full tag set, absolute image URL passthrough, `summary_large_image`,
9999+ and clean omission of description tags when absent.
100100+- **Build/type:** `npm run test`, `npm run check`, `npm run build`.
101101+- **Rendered output:** confirm each of the three built pages' `<head>` contains the
102102+ OG/Twitter tags with absolute `og:url` and `og:image`.
103103+104104+## Constraints honoured
105105+106106+- Reading pages must not import `@wordpress/*` — the helper is dependency-free and
107107+ the rendering lives in `Base.astro`. ✓
108108+- Additions-only to `Base.astro`'s prop interface; no behaviour change for callers
109109+ that don't pass the new props. ✓