A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Add design spec for Open Graph meta tags on static pages

+109
+109
docs/superpowers/specs/2026-06-09-open-graph-meta-tags-design.md
··· 1 + # Open Graph meta tags for the static pages 2 + 3 + **Date:** 2026-06-09 4 + **Status:** Approved (brainstorm) 5 + 6 + ## Goal 7 + 8 + Give SkyPress's public, shareable static pages rich link previews on social 9 + platforms (Open Graph + Twitter Cards), with an on-brand default share image. 10 + 11 + ## Scope 12 + 13 + In scope — the three public-facing static pages a logged-out visitor would share: 14 + 15 + - `/` — home (`src/pages/index.astro`) 16 + - `/lexicon` — `src/pages/lexicon.astro` 17 + - `/preview` — `src/pages/preview.astro` 18 + 19 + Out of scope (follow-ups): 20 + 21 + - The app screens `/editor` and `/dashboard` — tools behind sign-in, not share targets. 22 + - The dynamic `[author]` publication/article pages — they need per-record titles, 23 + descriptions, and images; a different shape, tracked separately. 24 + - Per-page custom OG images — all three pages share one default image for now. 25 + 26 + ## Approach 27 + 28 + Extend the shared `Base.astro` layout, which already owns `<head>` and takes 29 + `title`/`description`. The pages then inherit OG/Twitter tags from data they 30 + already pass. (Rejected: a separate `<Seo>` component or inline per-page meta — 31 + both duplicate what `Base` already centralises.) 32 + 33 + ## Design 34 + 35 + ### A. Tag-building helper — `src/lib/seo/meta.ts` 36 + 37 + A pure function that returns the list of `<meta>` attribute objects, kept out of 38 + the `.astro` template so it is unit-testable (the project is TDD-locked). 39 + 40 + ```ts 41 + interface MetaInput { 42 + title: string; 43 + description?: string; 44 + url: string; // absolute canonical URL 45 + image: string; // absolute URL 46 + siteName: string; // "SkyPress" 47 + type?: string; // default "website" 48 + imageAlt?: string; 49 + } 50 + // returns Array<{ property?: string; name?: string; content: string }> 51 + buildMetaTags(input): MetaTag[] 52 + ``` 53 + 54 + Tags produced: 55 + 56 + - `og:title`, `og:url`, `og:type`, `og:site_name` 57 + - `og:description` — **omitted entirely when `description` is absent** (no empty tag) 58 + - `og:image`, `og:image:width` (`1200`), `og:image:height` (`630`), `og:image:alt` 59 + - `twitter:card` (`summary_large_image`), `twitter:title`, `twitter:image` 60 + - `twitter:description` — omitted when `description` is absent 61 + 62 + `og:*` use the `property` attribute; `twitter:*` and `name`-style use `name`. 63 + 64 + ### B. `Base.astro` renders them 65 + 66 + New optional props (additions only — existing `title`/`description`/`phase` unchanged): 67 + 68 + - `image?: string` — default `/og-default.png` 69 + - `ogType?: string` — default `"website"` 70 + - `imageAlt?: string` — default a brand-appropriate string 71 + 72 + `Base` computes the canonical absolute URL from `Astro.url` resolved against 73 + `Astro.site` (`https://skypress.blog`, overridable via `PUBLIC_SITE_URL`), resolves 74 + the image to an absolute URL the same way, calls `buildMetaTags`, and renders the 75 + tags in `<head>`. It also emits `<link rel="canonical" href={url} />` since the 76 + absolute URL is already computed. 77 + 78 + ### C. The three static pages 79 + 80 + - `index.astro` and `lexicon.astro` already pass `title` + `description` — no change 81 + needed; they inherit the default image automatically. 82 + - `preview.astro` currently passes only `title`; add a `description` so its card has 83 + body text (e.g. "A sample SkyPress article, rendered server-side from stored AT 84 + Protocol blocks — zero editor JavaScript."). 85 + 86 + ### D. Shared OG image — `public/og-default.png` 87 + 88 + 1200×630 PNG, committed as a static asset. On-brand: the SkyPress sky gradient (a 89 + warm sun/golden phase), the logo sun-mark + **SkyPress** wordmark, and the tagline 90 + "A writing studio for the open social web." Authored as an HTML/SVG poster using the 91 + real palette (`global.css`) and self-hosted fonts (Overused Grotesk, IBM Plex Mono), 92 + then rasterized to PNG at exactly 1200×630 via a headless browser so the custom 93 + fonts render. The authoring source need not ship; only the PNG is required. 94 + 95 + ### E. Testing & verification 96 + 97 + - **Unit (TDD, red first):** `src/lib/seo/meta.test.ts` for `buildMetaTags` — 98 + asserts the full tag set, absolute image URL passthrough, `summary_large_image`, 99 + and clean omission of description tags when absent. 100 + - **Build/type:** `npm run test`, `npm run check`, `npm run build`. 101 + - **Rendered output:** confirm each of the three built pages' `<head>` contains the 102 + OG/Twitter tags with absolute `og:url` and `og:image`. 103 + 104 + ## Constraints honoured 105 + 106 + - Reading pages must not import `@wordpress/*` — the helper is dependency-free and 107 + the rendering lives in `Base.astro`. ✓ 108 + - Additions-only to `Base.astro`'s prop interface; no behaviour change for callers 109 + that don't pass the new props. ✓