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. */}
+
+
+
+
+
+
{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() {