# Social-Sharing / Link-Preview Audit — three storefronts

**Date:** 2026-06-06 · **Targets:**
`fabular.pages.dev` (Fresh Haven — vendor reference **demo**) ·
`isarland.de` (Isarland Ökokiste — **live** production storefront) ·
`webshop.christoph-trappl.workers.dev` (Isarland headless rebuild — the **CTO build**) ·
**Genre:** single-angle, three-target widen · **Method:** [`README.md`](README.md) — read-only
`curl` probes (homepage + one product page per site), asset HEAD checks, multi-UA probes.
No data written to Supabase.
**Companion to:** the [Isarland pre-launch audit](2026-06-06c-isarland-headless-prelaunch-audit.md)
and its three-way comparison — *same three targets, new angle: how a shared link renders.*

> **Read me first (sensitivity).** Two of these three targets are **not** fair game for a
> forwardable briefing. `fabular.pages.dev` is fab4minds' own reference demo. But `isarland.de`
> is a **real prospect's live shop** and `webshop.christoph-trappl.workers.dev` is a build by a
> **named developer** (Christoph Trappl). Per the c-audit, this is **internal / formative**
> readiness feedback — not a forwardable artifact as-is. All probes are read-only.

---

## TL;DR — three ways to fail the WhatsApp test, and the headline inverts on product pages

The question this round answers: *paste a link to each shop into WhatsApp (or Signal, iMessage,
Slack, LinkedIn, X) — what card shows up?* The shared link people actually send for a shop is a
**product**, not the homepage, so we probed both.

The headline **inverts** between homepage and product page:

- **The demo looks the most polished and shares the worst.** `fabular.pages.dev` has the richest
  meta scaffolding (OG + Twitter + canonical + theme-color) — but **no `og:image` anywhere**, on
  homepage *or* product. Its only image is a 512×512 square app **logo**, declared via
  `twitter:image` only. WhatsApp/Facebook/LinkedIn read `og:image`, **not** `twitter:image`
  (Twitter is the one that falls back the other way). Net: a shared Fresh Haven link shows
  **text + bare domain, no picture** in WhatsApp — even for a product. The carrot is never seen.

- **The two real builds DO produce real product-photo cards — each with one defect.**
  - `isarland.de` (live): product pages carry a **real 1200×1097 product-photo `og:image`**
    (✅, 116 KB JPEG, under WhatsApp's size ceiling). But (a) the **homepage `og:image` is broken**
    — it resolves to an HTML page, not an image; (b) product meta is **mis-keyed** — shared product
    links show the *wrong* product title/photo; (c) the meta is **UA-gated** — only allowlisted
    crawlers get it.
  - `webshop.…workers.dev` (CTO build): product pages carry a full `summary_large_image` card with
    both `og:image` and `twitter:image` (✅, cleanest tag structure of the three — `og:site_name`,
    `og:locale`). But (a) the image host is **`testshop.isarland.de`** (staging, not production);
    (b) the delivered image is **200×200**, not the 1200 it requests; (c) the **homepage has no
    image at all** → text-only card.

None of the three currently produces a correct, image-rich WhatsApp card on *both* its homepage
and its product pages.

---

## What a WhatsApp / link-preview card actually needs

So the findings below read against a fixed bar (this is *spec + documented crawler behaviour*,
not something observed in a renderer — there is no public WhatsApp preview validator):

| Need | Why it matters |
|---|---|
| **`og:image`** (absolute `https://`) | The **primary** image source for WhatsApp, Facebook, LinkedIn, Signal, iMessage, Telegram. These crawlers key on `og:image`. **They do not read `twitter:image`.** |
| Image returns an **image content-type** | A `200 text/html` at the `og:image` URL = no picture renders. Reachable without auth/cookies. |
| Image **≥ 300 px**, ideally ~1200×630 | WhatsApp shows a large card for big images, a small square (or nothing) for tiny ones. Twitter `summary_large_image` wants ≥ 300×157. |
| Image **under ~600 KB** (WhatsApp), few MB (FB) | Oversized images get dropped → no thumbnail. (None of our three is too big — the failures are the opposite: missing or too small.) |
| `og:title`, `og:description`, `og:url` | The card's headline/sub/canonical. `twitter:card` only governs X. |
| Meta served to the **crawler's UA** | SPA/prerender setups that UA-sniff must allowlist *every* crawler, or off-list apps get the bare shell. |

---

## The matrix

`H` = homepage card · `P` = product card (the real share target). ✅ works · ⚠️ degraded · ❌ broken/absent.

| Signal | **fabular.pages.dev** (demo) | **isarland.de** (live) | **webshop.…workers.dev** (CTO) |
|---|---|---|---|
| `og:title` / `og:description` | ✅ H + P, product-specific | ✅ H + P (but P mis-keyed, below) | ✅ H + P, product-specific |
| `og:image` present | ❌ **absent H + P** | H: ❌ broken · P: ✅ present | H: ❌ absent · P: ✅ present |
| `og:image` resolves to a real image | n/a | H: ❌ `text/html` · P: ✅ `image/jpeg` 1200×1097 | P: ✅ `image/jpeg` but **200×200** |
| Image host | — | `www.isarland.de` (prod ✅) | **`testshop.isarland.de`** (staging ⚠️) |
| `twitter:card` | `summary_large_image` (img = square logo) | ❌ none | `summary_large_image` |
| `twitter:image` | ⚠️ 512² **logo**, generic on P | ❌ none | ✅ = product `og:image` |
| `og:url` | ✅ | ❌ | ❌ (canonical present) |
| `og:site_name` / `og:locale` | ❌ / ❌ | ❌ / ❌ | ✅ / ✅ (`de_DE`) |
| Meta served to all UAs | ✅ static (Cloudflare Pages) | ⚠️ **UA-gated** (bots only) | ✅ static (Worker) |
| **WhatsApp card, homepage** | text only, no img | **text only** (img broken) | **text only**, no img |
| **WhatsApp card, product** | **text only, no img** | photo ✅ — *but wrong product* | photo ✅ — *but 200px, staging host* |

---

## Per-site findings

### 1 · `fabular.pages.dev` — the demo: polished scaffolding, no picture (observed)

The most complete tag layer of the three, and the only one that's fully static (so every crawler,
every UA, gets it). Yet it never shows an image in a WhatsApp/FB/LinkedIn card:

- **No `og:image`** on the homepage *or* on `/produkt/bio-karotten-1kg` (`grep -c og:image` = 0 both).
- The only image is `twitter:image = /icons/icon-512.png` — a **512×512 square app logo** (13.8 KB),
  which (a) WhatsApp/FB ignore (wrong property), and (b) even on X renders as a center-cropped logo,
  not a product, despite `twitter:card=summary_large_image` (which wants a wide image).
- On the **product page**, `twitter:title` / `twitter:description` are the **generic homepage
  strings** ("Fresh Haven – Frische Lebensmittel…"), not the product's — so even the X card for a
  carrot shows the site tagline + logo.

*Inferred:* the OG layer was authored once for the homepage and the product template inherits the
text fields but was never given a per-product image. Lowest-effort, highest-leverage fix of the three.

### 2 · `isarland.de` — the live shop: real product photos, three defects (observed)

This is an Angular SPA (`data-beasties-container`) with **prerendered** social meta — a genuine
contrast with the documented fab4minds JS-wall. Product pages carry a real photo card. But:

- **(a) Homepage `og:image` is broken.** It points at
  `https://www.isarland.de/assets/icon/app_logo-192x192.png`, which returns **`200 text/html`** (the
  SPA shell), not a PNG — that asset path doesn't exist, and the server soft-404s every unmatched
  path with the app shell (confirmed: `/favicon.ico` and `/assets/icon/app_logo.png` also return
  `text/html`; only `/assets/icon/favicon-32x32.png` is a real image). Its `og:image:width/height`
  also **lie**: declared 1062×759 for a file named "192×192". → Homepage share = no picture.
- **(b) Product meta is mis-keyed — shared product links show the WRONG product.** Probed five
  sitemap URLs (with a bot UA):

  | URL slug | `og:title` returned | verdict |
  |---|---|---|
  | `samba-500g-rapunzel/samba` | "Tagescreme Granatapfel" (a face cream) | ❌ wrong |
  | `alkoholfreies-bier-traeger/alkoholfreies-bier` | "Querbeet Gemüse" | ❌ wrong |
  | `weissbier-alkoholfrei-traeger/weissbier-alkoholfrei` | "Suchergebnisse" | ❌ wrong (search-results placeholder) |
  | `rezeptdetail/avocado-kichererbsen-brot` | "Avocado-Kichererbsen Brot" | ✅ correct |
  | `rezeptdetail/spargel-feta-aufstrich` | "Spargel-Feta-Aufstrich" | ✅ correct |

  **Recipe** routes prerender correctly; **product** routes return stale/placeholder meta (one
  literally "Suchergebnisse"). The product photo *does* resolve to a real 1200×1097 JPEG (116 KB) —
  but it's the photo for the *wrong* product. **Verified these are live, valid products, not dead
  sitemap entries:** loaded both in a real browser (Playwright, runs the SPA's JS) — `samba…` renders
  title/H1 "Samba" ✅ and `weissbier…` renders "Weißbier, alkoholfrei" ✅, both correct to a human.
  So it is **browser-right, crawler-wrong** — an unambiguous prerender-snapshot bug, not stale URLs.
  *Inferred cause* (not pinned): the prerender snapshot is captured before the product route's async
  data resolves, so it freezes a prior/placeholder state; recipe routes resolve in time. **This is
  the biggest finding here** — the links a customer is most likely to share render a *different*
  product's name and image to every social crawler, while the page itself is correct.
- **(c) Meta is UA-gated.** Prerendered OG is served only to allowlisted crawler UAs — confirmed:
  `og:title` count = 1 for `WhatsApp/…` and `facebookexternalhit/1.1`, but **0** for a normal
  `Mozilla/5.0` browser UA. WhatsApp and Facebook are covered; any crawler *off* the allowlist
  (some Signal/Telegram/Slack/Mastodon/LinkedIn fetchers) gets the empty shell → **no preview at
  all**. *Inferred:* prerender middleware keyed on a UA allowlist.
- No `twitter:*` tags, no `og:url`, no `og:site_name/locale`.

### 3 · `webshop.christoph-trappl.workers.dev` — the CTO build: cleanest tags, staging image (observed)

The **best-structured** tag set of the three (only one with `og:site_name` + `og:locale`), served
statically to all UAs (no gating). Product pages emit a full large-image card. But:

- **(a) Homepage has no image at all** — no `og:image`, no `twitter:image`, `twitter:card=summary`.
  → homepage share = text-only card.
- **(b) Product image host = `testshop.isarland.de`** — the **staging** backend, the same
  staging-host fragility flagged in the c-audit (and the open `testshop` *image* risk there). Every
  product card's picture depends on the staging server staying reachable; at go-live this must
  repoint to production or all social previews break.
- **(c) Delivered image is 200×200, not 1200.** The `og:image` URL requests `width=1200` but carries
  `allowUpscale=false`, and the source asset for the probed product (`obst-und-gemuese-kiste`) is only
  200 px — so the crawler gets a **200×200, 6 KB** thumbnail under a `summary_large_image` banner.
  WhatsApp shows a small square; X likely downgrades the card. *Observed on one product; inferred to
  recur* wherever the source asset is small (the `allowUpscale=false` + small-source combination).
- No `og:url` (canonical present).

---

## The cross-cutting pattern

Same shape as the c-audit's "ceiling vs substance" inversion, now on the sharing layer:

- **The demo proves the *scaffolding* and skips the *payload*.** Perfect tag structure, zero product
  imagery — it reads like a shop in the markup and looks like a logo in the card.
- **The live shop has the *payload* and breaks the *plumbing*.** Real product photos exist, but the
  homepage image 404s, product meta is mis-keyed to the wrong item, and the whole layer is UA-gated.
- **The CTO build has the *cleanest tags* and the *most fragile source*.** Best-formed OG, but the
  picture comes from staging at thumbnail size, and the homepage is imageless.

A correct card needs scaffolding **and** a reachable, correctly-keyed, adequately-sized product
image served to every crawler. No target has all four at once.

---

## Fix lists (concrete, cheap)

**`fabular.pages.dev` (demo):**
1. Add `og:image` (homepage: a real branded share card 1200×630; product: the product photo).
2. Give product pages **per-product** `twitter:title`/`twitter:description` (currently generic).
3. Swap the square logo for a 1.91:1 image so `summary_large_image` isn't a cropped logo.

**`isarland.de` (live):**
1. **Fix product prerender keying** — ensure the snapshot waits for product data so `og:title`/image
   match the URL (today `samba…`→Tagescreme, `weissbier…`→"Suchergebnisse"). Highest priority.
2. Fix the homepage `og:image` (point at a real image asset; correct/remove the false
   `og:image:width/height`).
3. Drop UA-gating for the meta-bearing snapshot, or widen the crawler allowlist (Signal, Telegram,
   Slack, LinkedIn, Mastodon, Discord), so off-list apps still get a preview.
4. Add `twitter:card=summary_large_image` + `twitter:image`, `og:url`, `og:site_name`, `og:locale`.

**`webshop.christoph-trappl.workers.dev` (CTO build):**
1. Repoint the product image host off `testshop.isarland.de` to production before go-live (same
   migration item as the c-audit image-host risk).
2. Drop `allowUpscale=false` *or* request a width the source supports, so cards aren't 200 px under a
   large-image banner.
3. Add a homepage `og:image`/`twitter:image` (today text-only).
4. Add `og:url`.

---

## Evidence (observed)

- All meta read from live HTML via `curl -L --compressed` (WhatsApp/facebookexternalhit UAs);
  homepage + one product page per site; isarland UA-matrix (WhatsApp/FB = 1 `og:title`, Mozilla = 0).
- isarland product mis-keying cross-checked in a **real browser** (Playwright): `samba…` → rendered
  title/H1 "Samba", `weissbier…` → "Weißbier, alkoholfrei" — both correct, so the wrong crawler
  `og:title` is a prerender bug on a live product, not a dead URL.
- Asset checks (HEAD + dimensions): isarland homepage `og:image` → `200 text/html`; isarland product
  `og:image` → `image/jpeg` 1200×1097, 116 KB; webshop product `og:image` → `image/jpeg` 200×200,
  6 KB; fabular `twitter:image` → `image/png` 512×512, 13.8 KB; fabular `og:image` count = 0 (H + P).
- Platform-renderer behaviour (which crawler reads which tag, size/ratio thresholds) is stated per
  the OG/Twitter spec + documented Facebook-crawler behaviour — **inferred**, not observed in a
  renderer (no public WhatsApp validator exists).
