+ Enlightened library to convert HTML and CSS to SVG
+
+
+
+
+ TypeScript
+
+ ★ 11.8k
+ ⑂ 312
+
+
+);
+
+const Benchmark = (): React.ReactElement => (
+
+
+ The question that matters isn’t how much traffic a platform sends us —
+ it’s whose links get shared everywhere else: pasted into
+ WhatsApp, posted on X, dropped in a Slack channel. So we looked at the
+ platforms whose links travel the most across the web and studied how they
+ unfurl. The pattern is stark: the most-shared platforms have{' '}
+ rich, open, contextual previews — and the ones that gate
+ their metadata get shared less because their links look broken.
+
+
+ Whose links get shared the most
+
+
+
+
+
+ The three to model
+
+ YouTube, Reddit and GitHub are where the most-shared, best-unfurling links
+ live. Here’s how each looks and exactly what we should take.
+
+
+
+
+ YouTube — the thumbnail IS the product
+
+
+
+
+
+
+ GitHub — contextual + live stats
+
+
+
+
+
+
+
+
+
+ Reddit — between GitHub and X
+
+ You called it: for developers Reddit sits right between GitHub’s
+ credibility and X’s reach. It’s a social network and a dev
+ watering hole, and its link cards are a masterclass in context — exactly
+ the balance daily.dev wants.
+
+
+
+ How a daily.dev link looks on Reddit
+
+
+
+
+
+
+
+
+
+ The anti-pattern: don’t be Instagram or TikTok
+
+
+ The takeaway for us:{' '}
+ openness + complete, valid tags is itself a growth lever.{' '}
+ Every daily.dev link should unfurl richly for every scraper (Facebook, X,
+ Slack, Discord, and Reddit’s Embedly) — never gated, never half-formed.
+
+
+
+
+ Enhanced principles (from the most-shared platforms)
+
+
+
+
+ Sources
+
+
+);
+
+const meta: Meta = {
+ title: 'Open Graph/6. How the Best Platforms Share',
+ component: Benchmark,
+ parameters: { layout: 'fullscreen' },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Benchmarks: Story = { name: 'Platform benchmark' };
diff --git a/packages/storybook/stories/open-graph/CoverDna.stories.tsx b/packages/storybook/stories/open-graph/CoverDna.stories.tsx
new file mode 100644
index 00000000000..a4c9000e49a
--- /dev/null
+++ b/packages/storybook/stories/open-graph/CoverDna.stories.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { COVER_DNA } from './coverDna';
+import { Cover } from './cover';
+import {
+ Bullets,
+ Divider,
+ Heading,
+ Muted,
+ Page,
+ PageHeader,
+} from './ogStoryLayout';
+
+const SANS =
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
+
+const label = (text: string): React.ReactElement => (
+
+ {text}
+
+);
+
+const CoverDna = (): React.ReactElement => (
+
+
+ Clean slate. Base = the clean Layout A you liked (source
+ left, real logo right, cover art bottom-right, glass upvote + comment
+ bar). Each variation adds one recognizability device so
+ the cover instantly reads as daily.dev. Grouped into four{' '}
+ systems — tell me which system feels right, then we
+ refine.
+
+
+ Baseline (no branding device)
+ The clean cover, unchanged — everything below builds on this.
+
+
+ ))}
+
+
+ Pick a direction
+
+
+);
+
+const meta: Meta = {
+ title: 'Open Graph/7 Recognizable Covers ★',
+ component: CoverDna,
+ parameters: { layout: 'fullscreen' },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Recognizable: Story = { name: '20 branding devices' };
diff --git a/packages/storybook/stories/open-graph/CurrentOpenGraph.stories.tsx b/packages/storybook/stories/open-graph/CurrentOpenGraph.stories.tsx
new file mode 100644
index 00000000000..44ea412f0b9
--- /dev/null
+++ b/packages/storybook/stories/open-graph/CurrentOpenGraph.stories.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { MetaTagsTable, PlatformGrid } from './platformCards';
+import { USE_CASES } from './useCases';
+import {
+ Bullets,
+ Divider,
+ Heading,
+ Muted,
+ Page,
+ PageHeader,
+} from './ogStoryLayout';
+
+const CurrentPreviews = (): React.ReactElement => (
+
+
+ Every place daily.dev content gets shared, rendered the way it actually
+ unfurls on X, LinkedIn, Facebook, Slack, Discord, WhatsApp and iMessage.
+ These are the real, live images daily.dev serves today —
+ pulled straight from the actual URLs (e.g.{' '}
+ og.daily.dev/api/posts/{id} for posts,{' '}
+ daily.dev/og-image.png for the homepage), not re-creations.
+ The tell is obvious: only posts get a real card — profile, source, tag,
+ squad, invite and Plus all fall back to the same generic image.
+
+
+ {USE_CASES.map((uc, i) => (
+
+
+ {i + 1}. {uc.name}
+
+ {uc.what}
+
+
+
+
+
+
+
+
+
+
+
+ {i < USE_CASES.length - 1 && }
+
+ ))}
+
+);
+
+const meta: Meta = {
+ title: 'Open Graph/1. Current Share Previews',
+ component: CurrentPreviews,
+ parameters: { layout: 'fullscreen' },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const AllUseCases: Story = { name: 'All use cases' };
diff --git a/packages/storybook/stories/open-graph/LinkCopyBehavior.stories.tsx b/packages/storybook/stories/open-graph/LinkCopyBehavior.stories.tsx
new file mode 100644
index 00000000000..01cd7b9d5ea
--- /dev/null
+++ b/packages/storybook/stories/open-graph/LinkCopyBehavior.stories.tsx
@@ -0,0 +1,314 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ Bullets,
+ CodeBlock,
+ Divider,
+ Heading,
+ Muted,
+ Page,
+ PageHeader,
+ SpecTable,
+ TwoCol,
+} from './ogStoryLayout';
+
+const SANS =
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
+
+/** Anatomy of a link preview, labeling the parts that are NOT the image. */
+const PreviewAnatomy = (): React.ReactElement => (
+
+
+
+
+
+ d.
+
+
+ app.daily.dev · favicon + domain
+
+
+
+ og:title — the headline
+
+
+ og:description — the supporting line shown on FB, Slack, Discord,
+ WhatsApp.
+
+
+
+);
+
+const SHARE_TEXT_CODE = `// Today — shared/src/lib/share.ts
+getTwitterShareLink(link, text) // → "{text} via @dailydotdev"
+// "text" is usually just the post title, so a tweet reads:
+// "How we cut edge cold-starts… via @dailydotdev app.daily.dev/posts/…"
+
+// Recommended — context-aware, pre-filled share copy (still user-editable)
+share.article = \`\${title}\\n\\nvia @dailydotdev\`;
+share.invite = \`I use daily.dev to keep up with dev news — join me 👇\`;
+share.profile = \`My developer profile on @dailydotdev 👇\`;
+share.squad = \`We're sharing the best \${topic} content in this Squad 👇\`;
+// Keep "via @dailydotdev" for attribution; add 1 relevant emoji max; no hashtag spam.`;
+
+const LinkCopyBehavior = (): React.ReactElement => (
+
+
+ The image gets the attention, but half of a great link preview is
+ everything around it: the title wording, the description, the little
+ favicon and brand avatar, the URL that shows, and the message we pre-fill
+ when someone taps “share”. This page covers all of it — current state vs.
+ what we should do — for the parts that live outside the Open
+ Graph image.
+
+
+ Anatomy — the non-image parts
+
+
+
+
+ On messengers and Slack/Discord/Facebook the text{' '}
+ does most of the work — the image may be small or absent. The favicon
+ and domain are the trust signal. The pre-filled share message is what
+ actually frames the link for the recipient.
+
+
+
+
+
+
+ Title & description copy
+
+
+
+
+ Favicon, site name & brand avatar
+
+ Slack, Discord, iMessage and Reddit show a small favicon next to the
+ domain; X shows the @dailydotdev profile avatar. These are
+ tiny but they’re the brand’s handshake.
+
+
+
+
+
+ The pre-filled share message
+
+ When a user shares from inside daily.dev, we pre-fill the text. Today it’s
+ basically the title plus “via @dailydotdev”. Tailoring it per context (and
+ keeping it editable) makes the share feel personal and lifts CTR.
+
+ {SHARE_TEXT_CODE}
+
+ }
+ right={
+
+ }
+ />
+
+
+
+ URLs & link behavior
+
+
+
+
+
+ Priorities (beyond the image)
+
+
+);
+
+const meta: Meta = {
+ title: 'Open Graph/8. Link Copy, Metadata & Behavior',
+ component: LinkCopyBehavior,
+ parameters: { layout: 'fullscreen' },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const CopyAndBehavior: Story = { name: 'Copy, metadata & behavior' };
diff --git a/packages/storybook/stories/open-graph/OpenGraphComparison.stories.tsx b/packages/storybook/stories/open-graph/OpenGraphComparison.stories.tsx
new file mode 100644
index 00000000000..74210b94538
--- /dev/null
+++ b/packages/storybook/stories/open-graph/OpenGraphComparison.stories.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ LinkedInCard,
+ MetaTagsTable,
+ WhatsAppCard,
+ XCard,
+} from './platformCards';
+import type { OgData } from './platformCards';
+import { USE_CASES } from './useCases';
+import {
+ Bullets,
+ Divider,
+ Heading,
+ Muted,
+ Page,
+ PageHeader,
+ TwoCol,
+} from './ogStoryLayout';
+
+const Stack = ({ data }: { data: OgData }): React.ReactElement => (
+
+
+
+
+
+
+
+
+);
+
+const Comparison = (): React.ReactElement => (
+
+
+ A side-by-side of what we ship today (the{' '}
+ real, live images, pulled from the actual URLs) against a
+ single, unified template system that adapts per share type. Each surface
+ keeps the same visual language so a daily.dev link is instantly
+ recognizable, while the headline, attribution and context change to fit
+ the object being shared. Red column = today; green column = proposed.
+
+
+ {USE_CASES.map((uc, i) => (
+
+
+ {i + 1}. {uc.name}
+
+ {uc.what}
+
+
+ }
+ right={}
+ />
+
+
+ }
+ right={}
+ />
+
+ {i < USE_CASES.length - 1 && }
+
+ ))}
+
+);
+
+const meta: Meta = {
+ title: 'Open Graph/2. Current vs Recommended',
+ component: Comparison,
+ parameters: { layout: 'fullscreen' },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const SideBySide: Story = { name: 'Side by side' };
diff --git a/packages/storybook/stories/open-graph/OpenGraphGuidelines.stories.tsx b/packages/storybook/stories/open-graph/OpenGraphGuidelines.stories.tsx
new file mode 100644
index 00000000000..367bcbb2615
--- /dev/null
+++ b/packages/storybook/stories/open-graph/OpenGraphGuidelines.stories.tsx
@@ -0,0 +1,314 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import {
+ Bullets,
+ CodeBlock,
+ Divider,
+ Heading,
+ Muted,
+ Page,
+ PageHeader,
+ SpecTable,
+} from './ogStoryLayout';
+
+const Guidelines = (): React.ReactElement => (
+
+
+ A condensed field guide to how link previews actually work across
+ platforms in 2026, the technical details that matter, and the concrete
+ changes we should make to daily.dev’s sharing. Pair this with the “Current
+ vs Recommended” page for the per-surface mock-ups.
+
+
+ The one image spec that covers everything
+
+ Ship a single 1200 × 630 px (1.91:1) image. It satisfies
+ Facebook, LinkedIn, X large cards, Slack, Discord, WhatsApp and iMessage
+ without cropping the important bits. Keep the file under{' '}
+ ~500 KB (hard ceiling ~1 MB or some crawlers skip it),
+ prefer PNG for text-heavy cards / JPEG for photographic ones, and keep all
+ critical content inside a centered ~80% safe area since
+ some surfaces center-crop toward square.
+
+
+
+
+
+ Practical takeaway: write for the strictest platform.{' '}
+ Keep og:title ≤ ~60 chars and og:description ≤ ~110 chars so nothing
+ important is clipped on LinkedIn/X while still reading well on
+ Slack/Facebook where more is shown.
+
+
+
+
+ Tags we should always emit
+ ',
+ 'Lets Reddit (Embedly), Discord & others build richer cards',
+ '✗ Likely missing — add for the Reddit/dev audience',
+ ],
+ ]}
+ />
+
+ {`
+
+
+
+
+
+
+
+
+
+
+
+
+
+`}
+
+
+
+ Design principles for the generated image
+
+
+
+
+ Lessons from the most-shared platforms
+
+ We studied the platforms whose links travel the most across the web (see
+ the Benchmark page). The winners — YouTube, Reddit, GitHub — all share
+ rich, open, contextual cards; the gated ones (Instagram, TikTok) get
+ shared less because their links look broken. What that means for us:
+
+
+
+
+
+ Caching & invalidation
+
+ Crawlers cache aggressively: Facebook and LinkedIn hold previews for{' '}
+ up to ~7 days, and they fetch your tags exactly once per
+ URL. Two consequences:
+
+
+
+
+
+ Recommended rollout order
+
+
+
+
+ Sources
+
+
+);
+
+const meta: Meta = {
+ title: 'Open Graph/3. Guidelines & Research',
+ component: Guidelines,
+ parameters: { layout: 'fullscreen' },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Research: Story = { name: 'Guidelines & research' };
diff --git a/packages/storybook/stories/open-graph/Overview.stories.tsx b/packages/storybook/stories/open-graph/Overview.stories.tsx
new file mode 100644
index 00000000000..b35b48e58fa
--- /dev/null
+++ b/packages/storybook/stories/open-graph/Overview.stories.tsx
@@ -0,0 +1,166 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { RecommendedOg } from './dailyOgImages';
+import {
+ Bullets,
+ Divider,
+ Heading,
+ Muted,
+ Page,
+ PageHeader,
+} from './ogStoryLayout';
+
+const SANS =
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
+
+const TOC: Array<{ n: string; name: string; what: string }> = [
+ {
+ n: '1',
+ name: 'Current Share Previews',
+ what: 'Every share type as it unfurls today on 7 platforms, with a meta-tag inspector.',
+ },
+ {
+ n: '2',
+ name: 'Current vs Recommended',
+ what: 'Side-by-side mock-ups and the fix for each surface.',
+ },
+ {
+ n: '3',
+ name: 'Guidelines & Research',
+ what: 'Platform spec cheat-sheet, tags to emit, caching, priorities.',
+ },
+ {
+ n: '4',
+ name: 'X (Twitter) Deep Dive',
+ what: 'How X actually renders links and how to win the surface where the image is everything.',
+ },
+ {
+ n: '5',
+ name: 'Recommended Template Spec',
+ what: 'The real @vercel/og template, tokens, text rules and rollout.',
+ },
+ {
+ n: '6',
+ name: 'How the Best Platforms Share',
+ what: 'What GitHub, X, Reddit & co. do — and which platforms drive the most shares.',
+ },
+ {
+ n: '7',
+ name: 'Design Directions',
+ what: 'Five creative directions for the image — pick one with me.',
+ },
+ {
+ n: '8',
+ name: 'Link Copy, Metadata & Behavior',
+ what: 'Everything outside the image: titles, descriptions, favicon, URLs, share text.',
+ },
+];
+
+const Overview = (): React.ReactElement => (
+
+
+ When a developer shares a daily.dev link — an article, their profile, a
+ squad, an invite — that preview is often the first time someone sees us.
+ It’s free, high-intent distribution, and right now we’re leaving most of
+ it on the table: titles get truncated by a “| daily.dev” suffix, several
+ share types fall back to one generic image, and nothing feels distinctly
+ ours. This initiative fixes the whole surface — the image, the copy, and
+ the link behavior — across every place we’re shared.
+
+
+
+
+
+
+ Why this matters now
+
+
+
+
+ What’s in this section
+
+ {TOC.map((t) => (
+
+
+ {t.n}
+
+
+
+ {t.name}
+
+
+ {t.what}
+
+
+
+ ))}
+
+
+ Open the numbered pages under “Open Graph” in the sidebar in order. Start
+ with the design directions (7) if you just want to react to visuals.
+
+
+);
+
+const meta: Meta = {
+ title: 'Open Graph/0. Overview',
+ component: Overview,
+ parameters: { layout: 'fullscreen' },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Start: Story = { name: 'Start here' };
diff --git a/packages/storybook/stories/open-graph/RecommendedSpec.stories.tsx b/packages/storybook/stories/open-graph/RecommendedSpec.stories.tsx
new file mode 100644
index 00000000000..d64444e3e69
--- /dev/null
+++ b/packages/storybook/stories/open-graph/RecommendedSpec.stories.tsx
@@ -0,0 +1,470 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import { RecommendedOg } from './dailyOgImages';
+import {
+ Bullets,
+ CodeBlock,
+ Divider,
+ Heading,
+ Muted,
+ Page,
+ PageHeader,
+ SpecTable,
+} from './ogStoryLayout';
+
+const SANS =
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
+
+const Callout = ({
+ n,
+ top,
+ left,
+}: {
+ n: number;
+ top: string;
+ left: string;
+}): React.ReactElement => (
+
+ {n}
+
+);
+
+const Anatomy = (): React.ReactElement => (
+
+ {/* Ambient backdrop — a pre-blurred cover (Satori can't blur at runtime),
+ else a brand-gradient glow. A dark gradient on top keeps text legible. */}
+ {p.cover
+ ?
+ : }
+
+
+ {/* Top bar: identity (left) + the real daily.dev logo (right) */}
+
+
+
+
+
+ {/* Left content: headline + subtitle on top, the glass bar pinned to the bottom */}
+
+
+
{p.title}
+ {p.subtitle &&
{p.subtitle}
}
+
+ {/* Glass bar — pre-composited translucent fill (no runtime backdrop-blur in Satori) */}
+
+ {engage
+ ? <>>
+ : {p.meta}}
+
+
+
+ {/* Art bottom-right: cover thumbnail, avatar, or brand tile (radius 54) */}
+
+
+
+
+ );
+}
+
+// Satori clamps text with the webkit box trio:
+const clamp = (lines: number) =>
+ ({ display: '-webkit-box', WebkitBoxOrient: 'vertical', WebkitLineClamp: lines, overflow: 'hidden' });`;
+
+const ROUTE_CODE = `// /api/share route — edge runtime. Generates 1200×630 once, then CDN-cached.
+import { ImageResponse } from '@vercel/og'; // or 'next/og'
+
+export const runtime = 'edge';
+
+const FONT = (w: string) =>
+ fetch(\`https://og.daily.dev/fonts/Inter-\${w}.ttf\`).then((r) => r.arrayBuffer());
+
+export async function GET(req: Request) {
+ const u = new URL(req.url);
+ const kind = (u.searchParams.get('kind') ?? 'generic') as Kind;
+ const props = await resolveProps(kind, u.searchParams); // fetch post/squad/user, truncate title
+
+ const [regular, bold, extrabold] = await Promise.all([FONT('Regular'), FONT('Bold'), FONT('ExtraBold')]);
+
+ return new ImageResponse(, {
+ width: 1200,
+ height: 630,
+ fonts: [
+ { name: 'Inter', data: regular, weight: 400, style: 'normal' },
+ { name: 'Inter', data: bold, weight: 700, style: 'normal' },
+ { name: 'Inter', data: extrabold, weight: 800, style: 'normal' },
+ ],
+ headers: {
+ // version the URL with ?v={contentHash}; then this immutable cache is safe.
+ 'cache-control': 'public, immutable, no-transform, max-age=31536000',
+ },
+ });
+}`;
+
+const META_CODE = `// Webapp side — point og:image at the versioned generator and keep tags lean.
+const v = post.contentHash; // bust X/LinkedIn/FB caches on edit or redesign
+const img = \`https://og.daily.dev/api/share?kind=article&id=\${post.id}&v=\${v}\`;
+
+const seo: NextSeoProps = {
+ title: post.title, // NO "| daily.dev" suffix
+ description: clamp(post.summary, 110),
+ openGraph: {
+ type: 'article',
+ title: post.title,
+ description: clamp(post.summary, 110),
+ siteName: 'daily.dev',
+ images: [{ url: img, width: 1200, height: 630, alt: post.title }],
+ },
+ twitter: { cardType: 'summary_large_image', site: '@dailydotdev' },
+ additionalMetaTags: [
+ { name: 'twitter:image:alt', content: post.title },
+ ...(post.author?.twitter ? [{ name: 'twitter:creator', content: post.author.twitter }] : []),
+ { name: 'twitter:label1', content: 'Author' }, { name: 'twitter:data1', content: post.source.name },
+ { name: 'twitter:label2', content: 'Reading time' }, { name: 'twitter:data2', content: \`\${post.readTime} min\` },
+ ],
+};`;
+
+const RecommendedSpec = (): React.ReactElement => (
+
+
+ A single, contextual template that renders every share type — driven by a{' '}
+ kind parameter — plus the text/naming rules and the actual{' '}
+ @vercel/og (Satori) implementation it should ship as on{' '}
+ og.daily.dev. The goal: a daily.dev link is recognizable at a
+ glance on any platform, and reads perfectly even on X where the image is
+ the entire message.
+
+
+ Anatomy of the card
+
+
+
+
+ Identity — source / author / sharer top-left, where
+ the context lives (a small avatar + name).
+
+
+ daily.dev logo — top-right, the real wordmark;
+ present but secondary, never competes with the message.
+
+
+ Headline — the hero. ≤ 3 lines, the only thing X
+ shows; sits over an ambient backdrop derived from the cover.
+
+
+ Engagement bar — avocado upvote + comment in a
+ glass/blur pill; the ownable daily.dev signal.
+
+
+ Cover art — rounded thumbnail bottom-right (becomes
+ an avatar/tile for profiles, squads, tags…).
+
+
+
+
+
+
+ Design tokens
+
+
+
+
+ The text system — titles, descriptions, names
+
+ The copy matters as much as the picture. Three global rules, then a
+ pattern per surface.
+
+
+
+
+
+
+ 1 · The Satori template
+
+ One component, switched on kind. Satori supports flexbox,
+ inline styles, gradients, borders and the webkit line-clamp trio — no CSS
+ grid, no stylesheets. Fonts must be supplied explicitly (TTF/OTF/WOFF; not
+ WOFF2).
+
+ {TEMPLATE_CODE}
+
+ 2 · The edge route
+
+ Generate once at the edge, then serve immutable from the CDN. The{' '}
+ ?v= content hash is what lets us safely use a one-year
+ immutable cache while still busting stale previews on edit.
+
+ {ROUTE_CODE}
+
+ 3 · Meta tags on the webapp
+
+ Point og:image at the versioned generator and keep the tags
+ lean. Note: no title suffix, explicit width/height, alt text, and the
+ Slack-only label/data pairs.
+
+ {META_CODE}
+
+
+
+ Rollout against the existing code
+
+
+
+
+ Sources
+
+
+);
+
+const meta: Meta = {
+ title: 'Open Graph/5. Recommended Template Spec',
+ component: RecommendedSpec,
+ parameters: { layout: 'fullscreen' },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Spec: Story = { name: 'Template spec & code' };
diff --git a/packages/storybook/stories/open-graph/XDeepDive.stories.tsx b/packages/storybook/stories/open-graph/XDeepDive.stories.tsx
new file mode 100644
index 00000000000..0b883bbdf2c
--- /dev/null
+++ b/packages/storybook/stories/open-graph/XDeepDive.stories.tsx
@@ -0,0 +1,376 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import type { ReactNode } from 'react';
+import { XCard } from './platformCards';
+import type { OgData } from './platformCards';
+import { RecommendedOg } from './dailyOgImages';
+import { USE_CASES } from './useCases';
+import {
+ Bullets,
+ Divider,
+ Heading,
+ Muted,
+ Page,
+ PageHeader,
+ SpecTable,
+ TwoCol,
+} from './ogStoryLayout';
+
+const SANS =
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
+
+const article = USE_CASES.find((u) => u.id === 'article');
+
+const labelStyle: React.CSSProperties = {
+ fontFamily: SANS,
+ fontSize: 12,
+ fontWeight: 700,
+ textTransform: 'uppercase',
+ letterSpacing: 0.6,
+ color: '#71717a',
+ marginBottom: 8,
+};
+
+/** Realistic X timeline chrome wrapped around a link card. */
+const Tweet = ({
+ data,
+ tweetText,
+}: {
+ data: OgData;
+ tweetText: string;
+}): React.ReactElement => (
+
+
+
+
+
+
+ Ido Shamun
+
+
+ @idoshamun · 2h
+
+
+
+ {tweetText}
+
+
+
+ 💬 24
+ 🔁 108
+ ♥ 642
+ 📊 23K
+
+
+
+
+);
+
+/** Overlay the 80% safe area + rounded-corner danger zones on an image. */
+const SafeAreaFrame = ({
+ children,
+}: {
+ children: ReactNode;
+}): React.ReactElement => (
+
+
{children}
+ {/* 80% safe area */}
+
+
+ 80% safe area
+
+ {/* corner danger markers */}
+ {['0 0 auto auto', '0 auto auto 0', 'auto 0 0 auto', 'auto auto 0 0'].map(
+ (inset) => (
+
+ ),
+ )}
+
+);
+
+const XDeepDive = (): React.ReactElement => (
+
+
+ X is the highest-leverage surface for developer sharing — and the most
+ unforgiving. On a modern summary_large_image card, X shows{' '}
+ only the image and a small domain pill: no title, no
+ description, no body text outside the picture. Whatever you want a reader
+ to know has to live inside the 1200×630 frame. This page covers exactly
+ how X behaves and how our generated image should be built for it.
+
+
+ Why X is where the image has to do all the work
+
+
+
+
+ The same link, in an X timeline
+
+ Today the card leads with the publisher’s cover and a small daily.dev mark
+ — the headline is barely legible and the brand is easy to miss. The
+ recommended card bakes the headline, author and brand into the image, so
+ it reads at a glance even at timeline scale.
+
+ {article && (
+
+
+ }
+ right={
+
+ }
+ />
+
+ )}
+
+
+
+ Design for the X thumbnail, not the full-size image
+
+ Keep everything that matters inside the centered{' '}
+ 80% safe area, and out of the four corners (rounded +
+ domain pill). Headline ≥ 48px at 1200px width, bold, with a hard contrast
+ floor so it survives next to vibrant posts in the feed.
+
+
+
+
+
+
+
+
+
+
+
+
+ X-specific tag & image spec
+
+
+
+
+ Large vs summary card
+
+ We should always request summary_large_image. The small{' '}
+ summary card (square thumbnail + text) is the weak fallback —
+ shown here only so the difference is obvious. With the small card the
+ image shrinks to a tile and the text X shows is our og:title/description,
+ so those still need to read well.
+
+ {article && (
+
+
+
summary_large_image (use this)
+
+
+
+
summary (avoid)
+
+
+
+ )}
+
+
+
+ X recommendations
+
+
+
+
+ Sources
+
+
+);
+
+const meta: Meta = {
+ title: 'Open Graph/4. X (Twitter) Deep Dive',
+ component: XDeepDive,
+ parameters: { layout: 'fullscreen' },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Optimizing: Story = { name: 'Optimizing for X' };
diff --git a/packages/storybook/stories/open-graph/cover.tsx b/packages/storybook/stories/open-graph/cover.tsx
new file mode 100644
index 00000000000..f6922709c42
--- /dev/null
+++ b/packages/storybook/stories/open-graph/cover.tsx
@@ -0,0 +1,613 @@
+import React from 'react';
+import type { CSSProperties, ReactNode } from 'react';
+import LogoIcon from '@dailydotdev/shared/src/svg/LogoIcon';
+import LogoText from '@dailydotdev/shared/src/svg/LogoText';
+import { UpvoteIcon } from '@dailydotdev/shared/src/components/icons/Upvote';
+import { DiscussIcon } from '@dailydotdev/shared/src/components/icons/Discuss';
+
+/**
+ * The locked "Layout A — clean" baseline cover, self-contained.
+ * Source top-left, real daily.dev logo top-right, headline left, cover art
+ * bottom-right (bottom-aligned with the actions bar), and a glass/blur
+ * upvote + comment actions bar pinned bottom-left. This is the agreed base —
+ * recognizability explorations build on it via the slot props.
+ */
+
+export const SANS =
+ '-apple-system, "Helvetica Neue", Helvetica, Inter, Arial, "Segoe UI", system-ui, sans-serif';
+export const MONO =
+ '"SF Mono", "JetBrains Mono", "Fira Code", ui-monospace, Menlo, monospace';
+
+export const INK = '#0B0E13';
+export const CABBAGE = '#CE3DF3';
+export const ONION = '#8A63F4';
+export const AVOCADO = '#1DDC6F';
+export const CHEESE = '#FFE923';
+export const BLUECHEESE = '#2CDCE6';
+export const BACON = '#FF5C5C';
+export const TEXT = '#FFFFFF';
+export const TERTIARY = '#A6AEBF';
+export const SECONDARY = '#D7DCE6';
+export const GRAD = `linear-gradient(135deg, ${CABBAGE}, ${ONION})`;
+export const white = {
+ ['--theme-text-primary' as string]: '#FFFFFF',
+} as CSSProperties;
+
+const COVER =
+ 'https://media.daily.dev/image/upload/s--P4t4XyoV--/f_auto/v1722860399/public/Placeholder%2001';
+
+export interface CoverData {
+ title: string;
+ source: string;
+ sourceColor: string;
+ readTime: string;
+ upvotes: string;
+ comments: string;
+ cover: string;
+}
+export const XD: CoverData = {
+ title: 'How we cut edge cold-starts by 90% with Rust and Wasm',
+ source: 'The Pragmatic Engineer',
+ sourceColor: '#FF8E3B',
+ readTime: '6m read',
+ upvotes: '312',
+ comments: '448',
+ cover: COVER,
+};
+
+export const clamp = (lines: number): CSSProperties => ({
+ display: '-webkit-box',
+ WebkitLineClamp: lines,
+ WebkitBoxOrient: 'vertical',
+ overflow: 'hidden',
+});
+
+export const Root = ({
+ children,
+}: {
+ children: ReactNode;
+}): React.ReactElement => (
+
+ );
+};
diff --git a/packages/storybook/stories/open-graph/useCases.tsx b/packages/storybook/stories/open-graph/useCases.tsx
new file mode 100644
index 00000000000..3cb768da0b0
--- /dev/null
+++ b/packages/storybook/stories/open-graph/useCases.tsx
@@ -0,0 +1,591 @@
+import React from 'react';
+import {
+ cloudinaryCharmEmptySquads,
+ cloudinaryCharmReadLater,
+ cloudinaryCharmNotEnoughTags,
+} from '@dailydotdev/shared/src/lib/image';
+import type { OgData } from './platformCards';
+import { RecommendedOg } from './dailyOgImages';
+
+export const DEFAULT_OG_IMAGE =
+ 'https://media.daily.dev/image/upload/s--VAY5ToZt--/f_auto/v1724209435/public/daily.dev%20-%20open%20graph';
+
+// The CURRENT column uses the REAL images daily.dev serves today (raw, not
+// re-created). Reality check (verified live 2026-06-17): only POSTS get a
+// generated card; profile, source, tag, squad, invite, Plus and the app
+// default all fall back to the SAME generic image. The marketing homepage
+// serves its own og-image.png.
+const LANDING_OG = 'https://daily.dev/og-image.png';
+
+// A real, live daily.dev post (so the current post card is the genuine asset).
+const POST_OG = 'https://og.daily.dev/api/posts/qojM1enSN';
+// The shared variant adds the sharer (real endpoint, ?userid=).
+const SHARED_OG = `${POST_OG}?userid=u_123`;
+const POST_TITLE = 'How to use traces to avoid breaking changes';
+const POST_SOURCE = 'Community Picks';
+const POST_COVER =
+ 'https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/2a3c2ca8481678b5c9178cd656700187';
+// The post's real source logo (Community Picks) and the real commenter avatar.
+const POST_SOURCE_LOGO =
+ 'https://media.daily.dev/image/upload/t_logo,f_auto/v1655817725/logos/community';
+const COMMENT_AVATAR =
+ 'https://media.daily.dev/image/upload/s--IbwvoTYq--/f_auto/v1772778404/avatars/avatar_giQfoBCN9hYxcvRVo3s95';
+
+// Profile share = the real DevCard "wide" image (ProfileLayout sets og:image to
+// /devcards/v2/{userId}.png?type=wide — a generated DevCard, NOT the generic).
+const PROFILE_OG =
+ 'https://api.daily.dev/devcards/v2/28849d86070e4c099c877ab6837c61f0.png?type=wide';
+// Real assets for the recommended profile: the user's avatar, their rank-based
+// DevCard cover (the bg changes with reputation rank), and real source logos.
+const PROFILE_AVATAR =
+ 'https://media.daily.dev/image/upload/s---xy_OAwk--/f_auto,q_auto/v1703781380/avatars/avatar_28849d86070e4c099c877ab6837c61f0';
+const PROFILE_RANK_COVER =
+ 'https://media.daily.dev/image/upload/s--xDkKz00z--/f_auto/v1707920136/covers/cover_28849d86070e4c099c877ab6837c61f0';
+// The user's real "reads the most" sources, straight from their DevCard.
+const PROFILE_SOURCES = [
+ 'https://media.daily.dev/image/upload/s--fk_6ycEi--/f_auto,q_auto/v1780996001/logos/collections',
+ 'https://media.daily.dev/image/upload/s--stRJbTCn--/f_auto/v1752953684/squads/ad08ba59-6646-487f-a8bd-2147d9e572a6',
+ 'https://media.daily.dev/image/upload/s--V91DY4ls--/f_auto,q_auto/v1772617267/logos/agents_digest',
+ 'https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/hn',
+];
+// Real WebDev squad share image (its own image is what production serves).
+const WEBDEV_OG =
+ 'https://media.daily.dev/image/upload/s--3B1fh4kU--/f_auto,q_auto/v1/squads/94fc7a56-e6d2-403f-acd6-b988b426574f';
+// Real freeCodeCamp source logo.
+const FREECODECAMP_LOGO =
+ 'https://media.daily.dev/image/upload/t_logo,f_auto/v1628412854/logos/freecodecamp';
+
+export interface UseCase {
+ id: string;
+ /** Human label for the share type. */
+ name: string;
+ /** What object is being shared and from where. */
+ what: string;
+ /** ReferralCampaignKey / route that produces it. */
+ source: string;
+ /** Problems with the current treatment. */
+ issues: string[];
+ /** What we should change. */
+ recommendations: string[];
+ current: OgData;
+ recommended: OgData;
+}
+
+export const USE_CASES: UseCase[] = [
+ {
+ id: 'article',
+ name: 'Article / Post',
+ what: 'A developer shares an aggregated article from the feed or post page.',
+ source: '/posts/[slug] · ReferralCampaignKey.SharePost',
+ issues: [
+ 'og:title always gets a " | daily.dev" suffix appended, eating ~12 chars of the headline and pushing it over the 60-char sweet spot.',
+ 'og:description falls back to a truncated, often HTML-stripped excerpt with no consistent value framing.',
+ 'The generated image leads with the publisher’s cover; daily.dev branding is small and easy to miss.',
+ 'No og:image:alt / twitter:image:alt is set.',
+ ],
+ recommendations: [
+ 'Drop the title suffix for shareable content — site_name already conveys the brand. Reserve the suffix for navigational pages.',
+ 'Lead the image with the headline + author + reading time over a consistent branded frame; treat the cover as a supporting thumbnail.',
+ 'Always emit og:image:alt mirroring the post title.',
+ 'Add twitter:label/data (Author, Reading time) for richer Slack/X-adjacent unfurls.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/posts/how-to-use-traces-to-avoid-breaking-changes',
+ title: 'How to use traces to avoid breaking changes | daily.dev',
+ description:
+ 'How distributed traces help you catch breaking changes before they ship…',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: POST_OG,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/posts/how-to-use-traces-to-avoid-breaking-changes',
+ title: 'How to use traces to avoid breaking changes',
+ description:
+ 'How distributed traces help you catch breaking changes before they ship — a practical walkthrough.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'How to use traces to avoid breaking changes',
+ imageNode: (
+
+ ),
+ squareNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'shared-post',
+ name: 'Shared post (with attribution)',
+ what: 'A user shares a post via their personal share link, attributing the share to them.',
+ source:
+ '/posts/[id]/share?userid=… · og.daily.dev/api/posts/{id}?userid={uid}',
+ issues: [
+ 'Attribution ("X shared") is small and inconsistent vs. the standard post image.',
+ 'Same title-suffix and description issues as a normal post.',
+ 'The sharer’s avatar/identity is not reliably surfaced, weakening the social proof that drives the click.',
+ ],
+ recommendations: [
+ 'Make the sharer a first-class element: avatar + "shared by {name}" pill at the top of the image.',
+ 'Keep the rest of the article template identical so the share feels like a native daily.dev object.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/posts/.../share?userid=u_123',
+ title: 'How to use traces to avoid breaking changes | daily.dev',
+ description:
+ 'How distributed traces help you catch breaking changes before they ship…',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: SHARED_OG,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/posts/.../share?userid=u_123',
+ title: 'How to use traces to avoid breaking changes',
+ description:
+ 'Ido shared this on daily.dev — how traces catch breaking changes before they ship.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'Ido shared: How to use traces to avoid breaking changes',
+ imageNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'profile',
+ name: 'Developer profile',
+ what: 'Sharing a user profile / DevCard.',
+ source:
+ '/[username] · DevCard v2 wide image · ReferralCampaignKey.ShareProfile',
+ issues: [
+ 'The profile shares the DevCard image (/devcards/v2/{id}.png?type=wide) — a different visual language from post shares, and not 1.91:1, so it gets letterboxed/cropped in unfurls.',
+ 'Bio (og:description) is often empty; falls back to the generic site description.',
+ 'Title carries the " | daily.dev" suffix.',
+ ],
+ recommendations: [
+ 'Use the same template family as posts, in "developer" mode: avatar, name, @handle, headline stat (streak, reputation).',
+ 'Default description to a generated one-liner ("{name} reads about React, Rust & AI on daily.dev") when bio is empty.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/idoshamun',
+ title: 'Ido Shamun (@idoshamun) | daily.dev',
+ description:
+ 'daily.dev is the easiest way to stay updated on the latest programming news…',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: PROFILE_OG,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/idoshamun',
+ title: 'Ido Shamun (@idoshamun)',
+ description:
+ 'Ido reads about AI, LLMs & web dev on daily.dev — 20.5k posts read, 1,087-day longest streak.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'Ido Shamun on daily.dev',
+ imageNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'squad',
+ name: 'Squad',
+ what: 'Sharing a Squad’s public page.',
+ source:
+ '/squads/[handle] · getSquadOpenGraph() · ReferralCampaignKey.ShareSource',
+ issues: [
+ 'Uses the squad’s own uploaded banner, whose dimensions/quality vary wildly and may not be 1.91:1.',
+ 'Falls back to the generic brand image when no banner is set — zero context.',
+ 'No member count / activity signal in the preview to drive joins.',
+ ],
+ recommendations: [
+ 'Render a generated, on-brand squad card: squad name + avatar + member count + "active today" signal, with the banner as a backdrop.',
+ 'Never fall back to the bare generic image — always generate a contextual squad card.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/squads/webdev',
+ title: 'WebDev | daily.dev',
+ description:
+ 'The official daily.dev web development community. Led by thought leaders and webdev experts. Join the Squad!',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: WEBDEV_OG,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/squads/webdev',
+ title: 'WebDev — a Squad on daily.dev',
+ description:
+ 'The official daily.dev web development community, led by thought leaders and webdev experts.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'WebDev squad on daily.dev',
+ imageNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'squad-invite',
+ name: 'Squad invite link',
+ what: 'A member invites someone to a private/public Squad.',
+ source: '/squads/[handle]/[token]',
+ issues: [
+ 'Title is good ("{inviter} invited you to {squad}") but the image is just the squad banner — the invite framing lives only in text the platform may truncate.',
+ 'No inviter identity in the image; the personal, social hook is lost.',
+ ],
+ recommendations: [
+ 'Bake the invite framing into the image: inviter avatar + "invited you to join {squad}" + member count + CTA chip.',
+ 'Keep the strong title but ensure the image stands on its own when the title is clipped.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/squads/webdev/abc123token',
+ title: 'Ido invited you to WebDev',
+ description:
+ 'The official daily.dev web development community. Led by thought leaders and webdev experts. Join the Squad!',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: WEBDEV_OG,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/squads/webdev/abc123token',
+ title: 'Ido invited you to join WebDev on daily.dev',
+ description:
+ 'Join the official daily.dev web development community. It’s free.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'Ido invited you to join WebDev',
+ imageNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'source',
+ name: 'Source / Publication',
+ what: 'Sharing a publication/source page.',
+ source: '/sources/[source]',
+ issues: [
+ 'Uses the bare generic brand image (defaultOpenGraph) — no indication of which source it is.',
+ 'Title + suffix only; description is the source description if present.',
+ ],
+ recommendations: [
+ 'Generate a source card with the source logo, name and "Followed by N developers on daily.dev".',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/sources/freecodecamp',
+ title: 'freeCodeCamp | daily.dev',
+ description: 'The latest from freeCodeCamp on daily.dev.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: DEFAULT_OG_IMAGE,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/sources/freecodecamp',
+ title: 'freeCodeCamp on daily.dev',
+ description:
+ 'Followed by developers across daily.dev. Get every new post in your feed.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'freeCodeCamp on daily.dev',
+ imageNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'tag',
+ name: 'Tag / Topic feed',
+ what: 'Sharing a tag feed (e.g. /tags/react).',
+ source: 'ReferralCampaignKey.ShareTag',
+ issues: ['Generic brand image; the topic name appears only in text.'],
+ recommendations: [
+ 'Generate a topic card: "#react" + a one-line topic description + a few sample source logos.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/tags/react',
+ title: 'react | daily.dev',
+ description:
+ 'Explore the React JavaScript library for building user interfaces.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: DEFAULT_OG_IMAGE,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/tags/react',
+ title: '#react on daily.dev',
+ description:
+ 'Explore the React JavaScript library for building user interfaces.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'The #react topic on daily.dev',
+ imageNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'comment',
+ name: 'Comment / Discussion',
+ what: 'Sharing a specific comment on a post.',
+ source: 'ReferralCampaignKey.ShareComment',
+ issues: [
+ 'Reuses the post image; the comment that the user actually wanted to highlight is invisible in the preview.',
+ ],
+ recommendations: [
+ 'Generate a discussion card: comment excerpt as the hero, commenter avatar/name, post title as context.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/posts/qojM1enSN#c_456',
+ title: 'How to use traces to avoid breaking changes | daily.dev',
+ description:
+ 'Reuses the post image — the highlighted comment is invisible in the preview.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: POST_OG,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/posts/qojM1enSN#c_456',
+ title: '“Great article! Thanks for sharing 🙌” — sirajju on daily.dev',
+ description:
+ 'sirajju commented on “How to use traces to avoid breaking changes”. Join the discussion.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'Comment by sirajju on a daily.dev discussion',
+ imageNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'invite',
+ name: 'Invite friends / Referral',
+ what: 'Personal referral link (the generic /?cid root link from Invite friends).',
+ source: '/?cid=… · ReferralCampaignKey.Generic',
+ issues: [
+ 'The root referral link (app.daily.dev/?cid=…&userid=…) falls back to the generic brand image — no referrer, no incentive, no personalization.',
+ 'Highest-intent growth surface, weakest preview. (A separate og.daily.dev/api/refs card exists for /join links, but it’s off the unified template and currently returns empty for many users.)',
+ ],
+ recommendations: [
+ 'Generate a referral card with the referrer’s avatar + "{name} invited you to daily.dev" + a clear CTA, on the unified template.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/?cid=generic&userid=u_123',
+ title: 'daily.dev | Where developers grow together',
+ description:
+ 'daily.dev is the easiest way to stay updated on the latest programming news…',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: DEFAULT_OG_IMAGE,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/?cid=generic&userid=u_123',
+ title: 'Ido invited you to daily.dev',
+ description:
+ 'The professional network where developers grow together. Free forever.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'Ido invited you to daily.dev',
+ imageNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'plus',
+ name: 'Plus subscription',
+ what: 'Sharing / gifting daily.dev Plus.',
+ source: '/plus',
+ issues: [
+ 'Generic brand image; nothing communicates the Plus value or the gift.',
+ ],
+ recommendations: [
+ 'Generate a Plus card with the Plus mark, headline benefit and gift framing when applicable.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/plus',
+ title: 'Unlock Premium Developer Features with Plus | daily.dev',
+ description:
+ 'daily.dev is the easiest way to stay updated on the latest programming news…',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: DEFAULT_OG_IMAGE,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/plus',
+ title: 'daily.dev Plus — your feed, supercharged',
+ description:
+ 'AI summaries, advanced filters, and an ad-free feed. Try Plus free for 14 days.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'daily.dev Plus',
+ imageNode: (
+
+ ),
+ },
+ },
+ {
+ id: 'home',
+ name: 'Homepage / default',
+ what: 'The root URL and any page without a specific override.',
+ source: 'next-seo.ts defaultOpenGraph',
+ issues: [
+ 'Single static image; fine as a true fallback but currently does double duty for many specific share types it should not.',
+ ],
+ recommendations: [
+ 'Keep as the genuine last-resort fallback only. Every specific surface above should override it.',
+ ],
+ current: {
+ domain: 'app.daily.dev',
+ path: '/',
+ title: 'daily.dev | Where developers grow together',
+ description:
+ 'daily.dev is the easiest way to stay updated on the latest programming news. Get the best content from the top tech publications on any topic you want.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ image: DEFAULT_OG_IMAGE,
+ },
+ recommended: {
+ domain: 'app.daily.dev',
+ path: '/',
+ title: 'daily.dev | Where developers grow together',
+ description:
+ 'The professional network for developers. One feed for the best engineering content, every day.',
+ cardType: 'summary_large_image',
+ siteName: 'daily.dev',
+ twitterSite: '@dailydotdev',
+ imageAlt: 'daily.dev — where developers grow together',
+ image: LANDING_OG,
+ },
+ },
+];