AmoleoFamily

Marketing Surfaces

Live

Amoleo·Website

Amoleo-Website · adopted

Amoleo·Pets

Amoleo-Pets · adopted

Amoleo·Connections

Amoleo-Connections · adopted

Amoleo·Rissbrook

Amoleo-Rissbrook · adopted

Amoleo·Accounts

Amoleo-Accounts · adopted

Mocked at the real 1200×630 aspect ratio using each product's accent — not the actual shipped artwork.

The OG image — what a shared link looks like

The picture that shows up when someone pastes a product's URL into Slack, iMessage, Discord, or a Twitter/X post. Nobody visits it as a page; it exists entirely for the moment a link gets shared, which is also why it is easy to ship nothing and never notice — the gap only shows up in somebody else's chat window, not in your own browser.

The shapes and filenames below are also held in machine-readable form in tools/og-image-manifest.json, which tools/check-og-image.mjs enforces. Change a value in both, in the same commit. Run the checker after touching a product's <head> or its OG image:

node tools/check-og-image.mjs

Amoleo-Website is the reference implementation. Its card is generated by scripts/make-og.py from the same theme tokens as the site itself, rather than designed once by hand and left to rot — regenerate it when the brand tokens change, the same discipline check-brand.mjs enforces on everything else.


0. Status — who ships what

Repo Site og:image
Amoleo-Website www.amoleo.com Real. 1200×630, family-listing layout, regenerated 5 Aug 2026.
Amoleo-Pets pets.amoleo.com Real. 1200×630, product-wordmark, light background, shipped 5 Aug 2026.
Amoleo-Connections connections.amoleo.com Real. 1200×630, product-wordmark, dark background, shipped 5 Aug 2026 — replaced the 512×512 app icon it used to point at.
Amoleo-Rissbrook www.rissbrook.co.uk Real. 1200×630, product-wordmark, light background, shipped 7 Aug 2026 (recorded as 5 Aug in error — see manifest note).
Amoleo-Accounts accounts.amoleo.com Real. 1200×630, product-wordmark, dark background (this app never sets data-theme, so base.css's own unconditioned default applies), shipped 27 Aug 2026 with a drawn tagline.

All five products now ship a real card, generated by the one shared script (§3) rather than hand-built ones. Pets, Connections and Rissbrook are still missing a drawn tagline — see §1.5's "open: the tagline" — which isn't a defect, it's a deliberately deferred decision the layout is already built to accommodate. Accounts' card carries one (its mission statement's One-Liner, verbatim) because it was designed after that decision had somewhere to land, not because the question is now settled family-wide.


1. The one size

CSS/social size Ratio
OG image 1200 × 630 ~1.91:1

This is the size every major client (Facebook, Slack, iMessage, Discord, LinkedIn) renders full-bleed under summary_large_image. There is no 2× variant the way the marketing screenshots have one — link-preview surfaces are small and compressed hard by the platform regardless, so a sharper source file buys nothing.

Format: PNG. Scrapers will not render SVG, and most give up on anything that isn't a PNG or JPEG.

Content, briefly: the wordmark, the product name, and enough of the brand background to be recognisable at thumbnail size — not a screenshot, and not an app icon centred on a blank field (see Connections, above; a square icon dropped into a 1.91:1 frame is either heavily letterboxed or cropped, neither of which anyone chose on purpose). AmoleoWebsite/scripts/make-og.py is the reference: it draws from the same theme tokens as the CSS, so the card cannot drift from the brand the way a hand-designed one eventually would.


1.5 Two layouts, not one

Website's card lists the family; every product's card shows only itself. This isn't a new decision — it's family-wordmark.md §8 applied to a new surface: "Only the Website has [the multi-product] pattern — it's the one repo whose page lists more than one product. It isn't expected to appear in Pets, Connections, or Rissbrook, each of which only ever talks about itself." An OG card is exactly that kind of surface, so it inherits the same rule.

Amoleo-Website — the family-listing layout, already shipped:

AMOLEO
{tagline}
· Pets · Connections · Rissbrook · Accounts

The family name alone at the top (Website is not itself a product, so there is no suffix to hang off it), the tagline, and the product list along the bottom — in lockup.json's order, left to right (see docs/family-product-order.md). scripts/make-og.py is the reference implementation.

Every product — the product-wordmark layout:

AMOLEO · PETS

Its own wordmark, hero-sized (family-wordmark.md's markup and colour rule: the family name in --brand-amoleo, the product name in its own fixed accent — --brand-pets, --brand-connections, --brand-rissbrook), auto-shrinking to fit the card (CONNECTIONS is the name that needs it). No family list — a product card that also lists its three siblings answers a question nobody asked at the exact moment someone is deciding whether to click through to this product, and it's a second place the priority order could quietly drift out of sync with the footer's.

Background is that product's own default theme, not a fixed choice: the canonical Amoleo Light (#f7f7fb bg, #1c1c2e ink, #6b6b7a muted) or Dark (#101013 bg, #ffffff ink, #9a9aa8 muted) pair — identical values in Website's and Connections' own CSS, not a per-product guess. Pets and Rissbrook are light (Rissbrook has no dark theme at all); Connections is dark. Accounts has no shipped default yet, so its card waits rather than guessing.

The left-edge stripe is the product's own accent, not the family's neutral one Website's card uses — there's no family row of colours at the bottom of a product's own card to carry the "which product is this" signal, so the stripe carries it instead.

Open: the tagline. Every product's og:description tag already has good copy, but the card itself doesn't draw any yet — deliberately reserved rather than assumed, because it isn't settled whether the tagline should be the product's mission statement One-Liner verbatim or separate copy written for this card specifically (see docs/mission-statements.md; the One-Liners themselves vary in voice today — Connections' and Rissbrook's already read like taglines, Pets' reads as an internal value statement, which is itself evidence for that decision rather than something to paper over here). The generator lays out either state correctly: with no tagline, the wordmark centres as the card's one element; set tagline in the manifest and rerun, and it moves near the top with the tagline filling the space below, matching Website's card's own proportions. No layout changes needed either way.

This is a content/layout rule, not something check-og-image.mjs can verify from pixels — the same limit the screenshot checker has ("whether the shot is any good" is never checked). It's written down here so a reviewer knows what "right" looks like, and so nobody designs a future product's card by copying Website's and swapping the tagline.


2. The five required tags

<meta property="og:image" content="https://pets.amoleo.com/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Amoleo · Pets — free health tracking for cats, dogs, and any other pet.">
<meta name="twitter:card" content="summary_large_image">

3. Generating one

Every product-wordmark card comes from one shared script: tools/make-og-image.py, in this repo.

python tools/make-og-image.py pets
python tools/make-og-image.py connections
python tools/make-og-image.py rissbrook

It reads colours out of tools/tokens/*.tokens.json and layout settings (background, tagline) out of og-image-manifest.json, and writes straight into the product's own repo (client/public/og.png or equivalent, resolved via repos.json's dir mapping — override the root with AMOLEO_ROOT, same as the checkers). One script, not a reimplementation per repo: make-og.py (Website's own, family-listing-only) hardcoded its four product colours directly, and they went stale three days later when the family's brand policy changed — check-brand.mjs never scans .py files, so nothing caught it until it was found by hand. Reading tools/tokens/*.tokens.json at generation time instead of copying its values in means that specific failure can't recur here: change a colour once, regenerate, done — no fourth place a hex can go stale.

Website's own card stays on its bespoke AmoleoWebsite/scripts/make-og.py — the family-listing layout is a genuinely different shape (the product list along the bottom, the order check in check-product-order.mjs) and only ever has the one instance, so a shared script would add a branch for no reuse.

A hand-designed one-off is acceptable for a first pass on a new product — better than nothing — but record it as debt: it will not track a token change, and someone has to remember to update it by hand when the brand does. Better to add the product to make-og-image.py's reach (a background entry in the manifest is normally all that's needed) than to hand-design it once.


4. What the checker actually checks

Rule
R1 og:image is present and is an absolute https:// URL on the product's own domain
R2 og:image:width is 1200 and og:image:height is 630
R3 og:image:alt is present and non-empty
R4 twitter:card is summary_large_image
R5 The referenced file exists
R6 The file's actual pixels are exactly 1200×630
R7 A placeholder/adopted:false product is not asserted as done, and vice versa

Not checked, and deliberately the same exclusions as the screenshot checker: whether the card looks good, and staleness as a hard failure — a card that still matches the product is still correct at any age.


5. Adopted vs pending

Same idiom as the screenshot manifest and the footer lockup. A product recorded adopted: false in og-image-manifest.json prints its findings as pending and does not fail the run — that is recorded follow-up work, not a regression. Flip adopted to true in the same commit that ships the real tags and image; a placeholder under an adopted product is exactly the drift this file exists to catch.

Generated from docs/family-og-image.md + tools/og-image-manifest.json. Edit there and re-run npm run build in docs-site-src/ — never hand-edit a page.