
OG-Image Best Practices for SPAs: Making Your Vibe Coding Projects Shareable
TL;DR: „SPAs don't deliver OG meta tags – social previews are empty. The fix: pre-rendering for static tags + dynamic OG image generation via Edge Functions for individual pages."
— Till FreitagThe Problem: Your Link Looks Like Spam
You built an app with Lovable or Bolt. It works. It looks great. But when you share the link on LinkedIn:
- No image – just a generic placeholder icon
- No title – or worse: "React App"
- No description – LinkedIn shows nothing
The problem isn't your content. The problem is the architecture.
Why SPAs Have No Social Previews
Single Page Applications render everything in the browser. When LinkedIn, Twitter, or WhatsApp crawl your link, they don't execute JavaScript. They only get the empty index.html:
<!DOCTYPE html>
<html>
<head>
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script src="/assets/index.js"></script>
</body>
</html>No og:title. No og:image. No og:description. No preview.
What Open Graph Tags Are – And Why They Matter
Open Graph (OG) tags are meta tags in your HTML <head> that social media platforms read:
<meta property="og:title" content="Your Page Title" />
<meta property="og:description" content="Description for the preview" />
<meta property="og:image" content="https://example.com/og-image.jpg" />
<meta property="og:url" content="https://example.com/page" />
<meta property="og:type" content="website" />The SPA problem: These tags are set at runtime via JavaScript (e.g., with react-helmet-async). But crawlers don't wait for JavaScript.
The Solution: 3-Layer Strategy
Layer 1: Pre-Rendering for Static OG Tags
Playwright SSG pre-renders each page and bakes the meta tags into HTML:
// playwright-ssg.ts
const pages = ['/blog/my-article', '/services/consulting'];
for (const path of pages) {
const page = await browser.newPage();
await page.goto(`http://localhost:5173${path}`);
await page.waitForSelector('meta[property="og:title"]');
const html = await page.content();
// Save as static HTML file
}Result: Crawlers get fully rendered pages with all OG tags.
→ Playwright SSG Tutorial: Step by Step
Layer 2: Dynamic OG Images with Edge Functions
For blog posts or product pages, you want individual preview images. Vercel Edge Functions generate these on-the-fly:
// api/og-image.tsx (Vercel Edge Function)
import { ImageResponse } from '@vercel/og';
export const config = { runtime: 'edge' };
export default function handler(req: Request) {
const { searchParams } = new URL(req.url);
const title = searchParams.get('title') || 'Default Title';
return new ImageResponse(
<div style={{
display: 'flex',
fontSize: 48,
background: 'linear-gradient(135deg, #1a2744, #0d1b2a)',
color: 'white',
width: '100%',
height: '100%',
padding: 60,
alignItems: 'center',
}}>
{title}
</div>,
{ width: 1200, height: 630 }
);
}In your HTML:
<meta property="og:image" content="https://example.com/api/og-image?title=My+Article" />Layer 3: Fallback Image as Safety Net
Always define a default OG image in your index.html:
<meta property="og:image" content="https://example.com/og-default.jpg" />If pre-rendering or Edge Function fails, LinkedIn at least shows your brand image instead of an empty box.
The OG-Image Checklist
Format & Size
- 1200×630px – the universal OG image format
- < 1 MB file size (LinkedIn crops larger images)
- WebP or JPG – PNG only when transparency is needed
- No important content in the outer 10% – platforms crop differently
Meta Tags
og:imagewith absolute URL (not relative!)- Set
og:image:widthandog:image:height og:image:altfor accessibility- Set
twitter:cardtosummary_large_image
Technical
- HTTPS mandatory – HTTP images get blocked
- Set cache headers (
Cache-Control: public, max-age=31536000) - No redirects – OG image URL must point directly to the image
- Use a CDN – fast load times for crawlers
Common Mistakes in Vibe Coding Projects
1. react-helmet-async Alone Isn't Enough
react-helmet-async sets tags client-side. Crawlers still don't see them. You always need pre-rendering.
2. Relative Image URLs
<!-- ❌ Wrong -->
<meta property="og:image" content="/images/preview.jpg" />
<!-- ✅ Correct -->
<meta property="og:image" content="https://example.com/images/preview.jpg" />3. No Twitter Card Fallback
Twitter/X reads og:image but prefers twitter:image. Set both:
<meta property="og:image" content="https://example.com/og.jpg" />
<meta name="twitter:image" content="https://example.com/og.jpg" />
<meta name="twitter:card" content="summary_large_image" />4. OG Image Gets Cached
LinkedIn and Facebook aggressively cache OG images. After changes, manually invalidate the cache:
- LinkedIn: Post Inspector
- Facebook: Sharing Debugger
- Twitter: Card Validator
Our Implementation: till-freitag.com
On our own website, we use exactly this strategy:
- react-helmet-async sets OG tags per page in React code
- Playwright SSG pre-renders all pages – OG tags are in static HTML
- Static OG images in WebP for blog posts from the asset registry
- Absolute URLs via a central
BASE_URLconfiguration
Result: Every shared link shows a perfect preview image on LinkedIn, Twitter, and WhatsApp.
Quick Win: OG Image in 5 Minutes
If you want to start immediately, here's the minimal approach:
- Create a 1200×630px image with your brand
- Place it in
/public/og-image.jpg - Add to
index.html:
<meta property="og:image" content="https://your-domain.com/og-image.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://your-domain.com/og-image.jpg" />This doesn't replace dynamic OG images – but it's much better than nothing.
Next Steps
OG images are just one piece of the SEO puzzle for vibe coding projects. The complete stack includes pre-rendering, schema markup, and edge delivery.
→ Vibe Coding SEO Guide: The Complete Overview → Automate JSON-LD Schema for SPAs → Lovable → GitHub → Vercel: The Production Workflow








