diff --git a/apps/og-generator/app/InContextPreview.tsx b/apps/og-generator/app/InContextPreview.tsx index 3e2a7882c99..c42b3e904c0 100644 --- a/apps/og-generator/app/InContextPreview.tsx +++ b/apps/og-generator/app/InContextPreview.tsx @@ -12,18 +12,26 @@ * as authentic, not just "dark card on our UI". */ -export type InContextMode = 'none' | 'twitter' | 'linkedin' | 'blog' +export type InContextMode = 'none' | 'twitter' | 'linkedin' | 'blog-post' | 'blog' type PreviewTheme = 'light' | 'dark' export const IN_CONTEXT_OPTS: { value: InContextMode; label: string }[] = [ { value: 'none', label: 'None' }, { value: 'twitter', label: 'Twitter / X' }, { value: 'linkedin', label: 'LinkedIn' }, + { value: 'blog-post', label: 'Blog post' }, { value: 'blog', label: 'Blog listing' }, ] interface Props { imgUrl: string | null + /** + * The Thumb render, when the active format has one — the real blog post + * page's hero crops whichever image it's given to a fixed 1.91:1 box, so + * passing this through the Blog-post mockup shows the actual crop instead + * of assuming the OG image's own aspect ratio survives untouched. + */ + thumbImgUrl?: string | null headline: string eyebrow: string | null aspect: string @@ -152,6 +160,60 @@ function LinkedInCardMockup({ imgUrl, headline, aspect }: Props) { ) } +// Real supabase.com/blog/[slug] hero crops whatever image it's given into a +// fixed 1.91:1 box via object-cover — independent of the source image's own +// aspect ratio (brief: apps/www/app/blog/[slug]/BlogPostRenderer.tsx). +const BLOG_POST_HERO_ASPECT = '1.91 / 1' + +function BlogPostMockup({ imgUrl, thumbImgUrl, headline }: Props) { + const heroSrc = thumbImgUrl ?? imgUrl + return ( +
+ {/* Sticky site nav — just enough to anchor "above the fold", not a + pixel-match of the real header. */} +
+
+ Supabase +
+ +
+
+ + + + Blog +
+ +

{headline}

+ +
+ Jul 10, 2026 + · + 4 minute read +
+ +
+
+
+ Supabase Team + Marketing +
+
+ + {heroSrc && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+ )} +
+
+ ) +} + function BlogListingMockup({ imgUrl, headline, eyebrow, aspect }: Props) { const fillerCards = [ { eyebrow: 'Engineering', title: 'Scaling Postgres connections with Supavisor' }, @@ -205,6 +267,7 @@ export function InContextPreview({ mode, ...rest }: Props & { mode: InContextMod return (
{mode === 'linkedin' && } + {mode === 'blog-post' && } {mode === 'blog' && }
) diff --git a/apps/og-generator/app/page.tsx b/apps/og-generator/app/page.tsx index be734488e9b..7c699fa2d2c 100644 --- a/apps/og-generator/app/page.tsx +++ b/apps/og-generator/app/page.tsx @@ -649,6 +649,10 @@ export default function Page() { const showOg = view !== 'thumb' const showThumb = view !== 'og' && hasSecondSlot + // The Blog-post in-context mockup mirrors the real page's hero, which + // prefers the Thumb image — fetch it even if the OG/Thumb toggle itself + // isn't showing Thumb right now. + const wantsThumbForBlogPost = inContext === 'blog-post' && hasSecondSlot // Content controls (Layout/Eyebrow/Headline) stay visible whenever a full // composition is on screen — that includes viewing just the second slot // when it's a full composition too (e.g. Social's Instagram), unlike the @@ -696,7 +700,7 @@ export default function Page() { }, [brandId, formatId, hasSecondary, headline, eyebrow, template, icon, scale]) const og = useRenderedImage(ogEndpoint, showOg) - const thumb = useRenderedImage(thumbEndpoint, showThumb) + const thumb = useRenderedImage(thumbEndpoint, showThumb || wantsThumbForBlogPost) const count = [...headline].length const counterColor = @@ -796,6 +800,7 @@ export default function Page() {