
Og Image Creator
- 10 installs
- 28 repo stars
- Updated December 5, 2025
- chongdashu/cc-skills
Generate brand-aligned Open Graph social card images per route by studying the codebase and rendering with Playwright and React components.
About
Studies a project's routes and brand identity, then generates contextually appropriate OG images using Playwright and existing components. Used when a developer needs social cards for pages across a site.
- Auto-detects routes and extracts brand identity
- Renders OG images with Playwright and React
Og Image Creator by the numbers
- 10 all-time installs (skills.sh)
- Ranked #1,523 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/chongdashu/cc-skills --skill og-image-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 10 |
|---|---|
| repo stars | ★ 28 |
| Last updated | December 5, 2025 |
| Repository | chongdashu/cc-skills ↗ |
What it does
Generate brand-aligned Open Graph social card images per route by studying the codebase and rendering with Playwright and React components.
Files
OG Image Creator
Generate authentic, brand-aligned Open Graph images by understanding your codebase first, then creating contextually appropriate images for each route using Playwright and your existing components.
Philosophy: Authentic Over Template
Before generating OG images, ask:
- What is the brand identity of this site (colors, fonts, logo, design language)?
- What routes/pages exist and what are their purposes?
- What components and assets can be reused for consistency?
- What page types need different visual treatments?
Core principles: 1. Study before generate: Understand the codebase structure, routing, and brand before creating anything 2. Authenticity over templates: Use actual brand colors, fonts, and components—not generic social card templates 3. Context-aware design: Landing pages, articles, products, and docs need different visual approaches 4. Smart automation: Auto-detect routes and extract brand, but allow user customization
Workflow
1. Discover and Analyze
Route discovery:
- Analyze project structure to determine framework (Next.js, Astro, React, etc.)
- Auto-detect routes based on framework conventions:
- Next.js:
app/*/page.tsxorpages/*.tsx - Astro:
src/pages/*.astro - React Router: Analyze route configuration
- Extract route metadata (title, description) from pages
- Categorize page types (landing, article, product, about, etc.)
Brand extraction:
- Find logo/icon files (SVG preferred)
- Extract color palette from CSS/Tailwind config
- Identify primary fonts from stylesheets or config
- Capture design patterns from existing components
Use scripts/analyze_codebase.py to automate discovery.
2. Design Strategy
Determine appropriate layouts per page type:
- Landing pages: Bold, minimal, emphasize value proposition
- Articles/Blog posts: Title + excerpt, date/author if relevant
- Product pages: Product name, key feature, visual if available
- Documentation: Topic + description, organized/structured feel
- About/Company: Brand-forward, professional
Layout principles:
- Generous whitespace (avoid clutter)
- High contrast for readability when shared
- Brand colors as accents, not overwhelming
- Typography hierarchy that matches site
- 1200x630px (standard OG dimensions)
3. Generate Images
Component-based generation:
- Reuse React components when possible for consistency
- Use Playwright to render components to images
- Apply brand colors, fonts, and assets
- Inject actual page metadata (not placeholders)
Technical approach: 1. Create minimal HTML template with brand styles 2. For each route, generate page-specific markup 3. Use Playwright to capture screenshot at 1200x630 4. Save to public/og/ or appropriate static directory 5. Update page metadata to reference OG image
Use scripts/generate_og_images.py for automation.
4. Verify and Optimize
- Validate OG image metadata in HTML
- Test with OG preview tools (opengraph.xyz, linkedin inspector)
- Ensure file sizes are reasonable (<200KB preferred)
- Check image appearance at different scales
Framework-Specific Guidance
Next.js
App Router:
// app/page.tsx
export const metadata = {
openGraph: {
images: ['/og/home.png'],
},
}Pages Router:
// pages/index.tsx
<Head>
<meta property="og:image" content="/og/home.png" />
</Head>Dynamic OG images: Consider Next.js OG Image Generation (@vercel/og) for dynamic content, but use static generation for brand consistency.
Astro
---
// src/pages/index.astro
const ogImage = '/og/home.png';
---
<head>
<meta property="og:image" content={ogImage} />
</head>React SPAs
Add OG tags to public/index.html or use react-helmet:
<Helmet>
<meta property="og:image" content="/og/home.png" />
</Helmet>Anti-Patterns to Avoid
❌ Generic templates: Don't use stock social card templates that ignore the site's brand
- Why bad: Creates disconnect between site and social sharing experience
- Better: Extract actual brand colors, fonts, and use them consistently
❌ One size fits all: Don't use the same OG image design for every page type
- Why bad: Misses opportunity for context-appropriate presentation
- Better: Different layouts for landing, articles, products, etc.
❌ Placeholder content: Don't use "Lorem ipsum" or generic text
- Why bad: Defeats the purpose of preview images
- Better: Use actual page titles and descriptions from metadata
❌ Overcrowding: Don't cram too much text, too many images, or complex layouts
- Why bad: Illegible when scaled down in social feeds
- Better: Focus on 1-2 key elements with generous whitespace
❌ Ignoring components: Don't create OG images from scratch if components exist
- Why bad: Breaks visual consistency with the site
- Better: Reuse button styles, card layouts, typography from components
❌ Manual generation: Don't create OG images one by one manually
- Why bad: Not scalable, hard to maintain consistency
- Better: Script the generation process to handle all routes
Variation Guidance
IMPORTANT: OG images should vary based on page context, not converge on a single design.
Dimensions to vary:
- Layout: Centered vs left-aligned, single vs split column, text vs visual focus
- Color emphasis: Primary brand color vs neutral, gradient vs solid
- Content hierarchy: Large title vs balanced title+description
- Visual elements: Logo size/placement, decorative elements, icons
- Typography: Size ratios, weight distribution, font pairing
Avoid converging on:
- Same gradient background for everything
- Identical "title + logo" layout
- Single font size ratio
- Fixed logo position
Context drives variation:
- Technical docs → Clean, organized, icon-based
- Marketing landing → Bold, colorful, value-focused
- Blog articles → Reader-friendly, author/date context
- Product pages → Visual product emphasis
Technical Requirements
Dependencies (install as needed):
npm install playwright sharp
# or
pip install playwright PillowPlaywright setup:
playwright install chromiumFile structure:
project/
├── public/og/ # Generated OG images
│ ├── home.png
│ ├── about.png
│ └── ...
└── scripts/ # Generation scripts
├── analyze_codebase.py
└── generate_og_images.pyRemember
OG images are an extension of your brand, not generic social cards.
Study the codebase to understand the design language. Extract the brand identity. Use actual components and content. Create contextually appropriate images that feel native to the site.
When someone shares your page, the OG image should feel like a natural preview—not a template slapped on top.
You're capable of creating sophisticated, brand-aligned OG images that enhance rather than undermine the site's identity.
OG Image Creator Skill
Smart Open Graph image generation that studies your codebase, understands routes and brand identity, then creates contextually appropriate images using Playwright and your existing design system.
What This Skill Does
When you ask Claude to "create og images" or "generate social cards", this skill:
1. Analyzes your codebase to detect framework, routes, and extract brand identity (colors, fonts, logo) 2. Categorizes pages by type (landing, article, product, documentation, etc.) 3. Generates context-appropriate OG images using your actual brand elements 4. Provides framework-specific integration guidance for Next.js, Astro, React, etc.
Philosophy
Authentic over template: OG images should feel like a natural extension of your brand, not generic social cards. The skill studies your codebase first to understand your design language, then creates images that match.
Quick Start
The skill provides two main scripts:
1. Analyze Codebase
python scripts/analyze_codebase.py /path/to/projectOutputs og-analysis.json with:
- Detected framework
- Found routes
- Extracted brand (colors, fonts, logo)
- Page categorization
2. Generate OG Images
python scripts/generate_og_images.py /path/to/projectCreates OG images in public/og/ directory based on the analysis.
Structure
og-image-creator/
├── SKILL.md # Main skill instructions
├── scripts/
│ ├── analyze_codebase.py # Route discovery & brand extraction
│ └── generate_og_images.py # OG image generation
└── references/
├── og-specifications.md # OG image standards & meta tags
├── best-practices.md # Design & content best practices
└── framework-workflows.md # Framework-specific integrationFeatures
- Framework-agnostic: Works with Next.js, Astro, React, Gatsby, static HTML, and more
- Smart route detection: Auto-discovers routes based on framework conventions
- Brand extraction: Pulls colors, fonts, and logo from existing codebase
- Context-aware layouts: Different designs for different page types
- Playwright-powered: Generates high-quality 1200x630px images
- Framework-specific guidance: Integration examples for each framework
Requirements
# Python
pip install playwright
# Install Chromium
playwright install chromiumSkill Quality Score
107/100 (Analyzed by skill-creator-plus)
- ✅ Strong philosophical foundation
- ✅ Comprehensive anti-pattern guidance
- ✅ Variation encouragement
- ✅ Well-organized and actionable
When to Use
Trigger this skill when:
- "create og images"
- "generate social cards"
- "add open graph images"
- Building a new site that needs OG images
- Updating existing OG images to match rebrand
Example Usage
User: "Create OG images for my Next.js site"
Claude:
1. Analyzes your Next.js project structure
2. Finds all routes in app/ or pages/ directory
3. Extracts brand colors from tailwind.config.js
4. Finds logo in public/
5. Generates contextually appropriate OG images for each route
6. Provides Next.js-specific code to integrate the imagesPhilosophy Highlights
Before generating, ask:
- What is the brand identity?
- What routes exist and their purposes?
- What components/assets can be reused?
- What page types need different treatments?
Anti-patterns to avoid:
- Generic templates that ignore brand
- Same design for all pages
- Placeholder content
- Overcrowded layouts
- Ignoring existing components
Variation is key:
- Different layouts for different page types
- Contextually appropriate designs
- Avoid converging on a single "favorite" template
OG Image Best Practices
Design Philosophy
1. Brand Extension, Not Decoration
OG images should feel like a natural preview of your site, not a generic social card slapped on top.
Good: Uses site's actual colors, fonts, and design language Bad: Generic gradient background with standard fonts
2. Context-Appropriate
Different page types deserve different treatments.
Landing page: Bold, minimal, value proposition Blog post: Title + excerpt, publication context Product: Product name + key benefit Documentation: Clean, organized, topic-focused
3. Authentic Content
Use actual page metadata, not placeholders.
Good: Real page title and description Bad: "Lorem ipsum" or generic "Blog Post" text
Visual Design
Typography
Headlines:
- 48-72px for main title
- Bold weight (700-800)
- Max 2-3 lines
- Leave breathing room
Body text:
- 24-32px for descriptions
- Regular/medium weight (400-500)
- Max 2 lines
- High contrast
Font pairing:
- Use site's actual fonts when possible
- Fallback to system fonts for reliability
- Avoid more than 2 font families
Color
Background strategies:
1. Solid brand color
- Simple, recognizable
- Works well if brand has strong color
- Ensure text contrast
2. Subtle gradient
- Add visual interest
- Keep subtle (not rainbow)
- Use brand colors
3. Light background with color accent
- Clean, professional
- Color for emphasis
- Works for corporate sites
Avoid:
- Too many colors competing
- Low contrast text
- Overly bright/saturated backgrounds
Layout
Alignment options:
1. Centered: Clean, symmetrical
- Good for: Landing pages, about pages
- Message feels balanced, authoritative
2. Left-aligned: Modern, readable
- Good for: Articles, documentation
- Feels natural for text-heavy content
3. Split: Visual + text
- Good for: Products, features
- Balances visual and text content
Spacing:
- Generous padding (60-80px minimum)
- Whitespace is your friend
- Don't fill every pixel
Visual Elements
Logo placement:
- Top left or center (if brand-focused)
- Bottom left (if content-focused)
- Keep reasonably sized (40-100px height)
Icons/graphics:
- Use sparingly
- Support the message, don't distract
- Match brand style
Decorative elements:
- Subtle shapes or patterns
- Background only
- Don't compete with content
Content Strategy
Title Treatment
Do:
- Use actual page title
- Edit for length if needed (maintain meaning)
- Emphasize key words with weight/color
- Make it scannable
Don't:
- Truncate with "..."
- Use all caps (unless brand style)
- Cram long titles at small size
- Use vague titles
Description
When to include:
- Articles: Yes (provides context)
- Products: Yes (key benefit)
- Landing: Optional (title may be enough)
- Documentation: Yes (scope/topic)
Best practices:
- 1-2 sentences max
- Complement title, don't repeat
- Increase value of preview
- Edit for clarity
Metadata to Use
Pull from page metadata:
title: Primary headingdescription: Supporting textauthor: For articles/blogsdate: For news/articlescategory: For organization
Technical Excellence
Performance
File size:
- Target: <100KB
- Maximum: 200KB
- Use PNG compression tools
- JPEG at 80-90% quality
Optimization:
- Use sharp contrasts (compresses well)
- Avoid gradients with banding
- Limit transparency (use JPEG if possible)
Accessibility
Alt text:
- Describe image content
- Don't just repeat title
- Be concise but informative
<!-- Good -->
<meta property="og:image:alt" content="Product dashboard showing analytics graphs and metrics" />
<!-- Bad -->
<meta property="og:image:alt" content="OG image" />Consistency
Across pages:
- Same brand elements (logo, colors, fonts)
- Consistent layout approach per page type
- Predictable structure
But also variation:
- Different content per page
- Adapted layout for page type
- Contextually appropriate
Page Type Patterns
Landing Page
Focus: Value proposition Layout: Centered, bold Elements: Title, optional description, logo Tone: Confident, clear
Example structure:
[Centered]
[Logo (optional)]
[Large bold title]
[Short description]Article/Blog Post
Focus: Title and context Layout: Left-aligned, organized Elements: Category/date, title, excerpt, logo Tone: Informative, inviting
Example structure:
[Top: "Article" or category]
[Title - large]
[Excerpt - medium]
[Bottom: Logo or author]Product Page
Focus: Product name and benefit Layout: Split or centered Elements: Product name, key benefit, visual if available Tone: Clear, benefit-focused
Example structure:
[Left: Product name, benefit]
[Right: Product visual (optional)]Documentation
Focus: Topic and clarity Layout: Clean, organized Elements: "Documentation" label, topic, description Tone: Professional, helpful
Example structure:
[Top: "Documentation"]
[Topic - large]
[Description - structured]About/Company
Focus: Brand and identity Layout: Centered, brand-forward Elements: Logo prominent, company name, tagline Tone: Professional, welcoming
Example structure:
[Centered]
[Logo - large]
[Company name]
[Tagline]Workflow Best Practices
1. Analyze First
Don't jump to generation. Understand:
- What framework/structure exists
- What routes need images
- What brand assets are available
- What design patterns are in use
2. Extract Brand
Pull from actual codebase:
- Colors from CSS/Tailwind config
- Fonts from stylesheets
- Logo from assets
- Design patterns from components
3. Categorize Routes
Different page types need different approaches:
- Landing vs article vs product
- Marketing vs documentation
- Static vs dynamic content
4. Generate Contextually
For each route:
- Use appropriate layout for page type
- Pull actual metadata
- Apply brand consistently
- Vary appropriately
5. Review and Refine
Don't just generate and forget:
- Preview in actual social platforms
- Check readability at thumbnail size
- Verify brand alignment
- Test sharing experience
Common Mistakes
Design Mistakes
❌ Template convergence: All images look identical → Vary layout by page type
❌ Over-design: Too many elements, colors, effects → Simplify, focus on 1-2 elements
❌ Tiny text: Unreadable when scaled → Use large, bold typography
❌ Brand disconnect: Looks nothing like the site → Extract and use actual brand elements
Technical Mistakes
❌ Relative URLs: Image doesn't load → Use absolute URLs
❌ Huge file sizes: Slow to load → Optimize images (<200KB)
❌ Missing meta tags: No preview shows → Include all essential OG tags
❌ Not testing: Issues discovered after launch → Test with platform preview tools
Process Mistakes
❌ Manual creation: Not scalable → Script the generation process
❌ One-size-fits-all: Same image for all pages → Generate per-route with context
❌ Ignoring existing assets: Starting from scratch → Reuse components and brand elements
❌ No maintenance plan: Images become outdated → Make generation reproducible and updatable
Quality Checklist
Before finalizing OG images:
Design:
- [ ] Uses actual brand colors and fonts
- [ ] Typography is readable at thumbnail size
- [ ] Layout is appropriate for page type
- [ ] Content uses actual metadata, not placeholders
- [ ] Visual hierarchy is clear
- [ ] Sufficient whitespace and padding
- [ ] High contrast for readability
Technical:
- [ ] 1200x630px dimensions
- [ ] File size <200KB
- [ ] PNG or high-quality JPEG
- [ ] Absolute URLs in meta tags
- [ ] All essential OG tags present
- [ ] Alt text provided
- [ ] Accessible over HTTPS
Content:
- [ ] Title is accurate and clear
- [ ] Description adds value
- [ ] No lorem ipsum or placeholders
- [ ] Context-appropriate for page type
- [ ] Brand elements present (logo, etc.)
Testing:
- [ ] Previewed on Facebook/LinkedIn/Twitter
- [ ] Readable at small sizes
- [ ] No content cut off
- [ ] Cache cleared on platforms
- [ ] Actually appears when sharing
Progressive Enhancement
Start simple, enhance as needed:
1. MVP: Basic title + brand color + logo 2. Enhanced: Add descriptions, better typography 3. Polished: Page-type-specific layouts 4. Advanced: Custom graphics, illustrations
Don't over-engineer on first pass. Get working images, then refine.
Framework-Specific Workflows
Next.js
App Router (Next.js 13+)
Route detection:
- Pages:
app/**/page.{tsx,ts,jsx,js} - Route groups: Ignore
(group)folders - Root:
app/page.tsx→/
OG image integration:
// app/page.tsx
export const metadata = {
title: 'Home',
description: 'Welcome to our site',
openGraph: {
title: 'Home',
description: 'Welcome to our site',
images: ['/og/home.png'],
url: 'https://example.com',
siteName: 'Example Site',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Home',
description: 'Welcome to our site',
images: ['/og/home.png'],
},
}Dynamic routes:
For dynamic routes like app/blog/[slug]/page.tsx:
- Generate OG image per slug if limited number
- Or use Next.js Image Generation API for dynamic content
Next.js OG Image Generation (alternative for dynamic content):
// app/api/og/route.tsx
import { ImageResponse } from 'next/og'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title')
return new ImageResponse(
(
<div style={{ /* ... */ }}>
{title}
</div>
),
{ width: 1200, height: 630 }
)
}When to use static vs dynamic:
- Static: Known routes, brand consistency desired
- Dynamic: User-generated content, many routes
Pages Router (Next.js 12 and earlier)
Route detection:
- Pages:
pages/**/*.{tsx,ts,jsx,js} - Ignore:
pages/_app.tsx,pages/_document.tsx,pages/api/** - Index:
pages/index.tsx→/
OG image integration:
// pages/index.tsx
import Head from 'next/head'
export default function Home() {
return (
<>
<Head>
<title>Home</title>
<meta property="og:title" content="Home" />
<meta property="og:description" content="Welcome to our site" />
<meta property="og:image" content="https://example.com/og/home.png" />
<meta property="og:url" content="https://example.com" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://example.com/og/home.png" />
</Head>
{/* Page content */}
</>
)
}Using next-seo:
import { NextSeo } from 'next-seo'
export default function Home() {
return (
<>
<NextSeo
title="Home"
description="Welcome to our site"
openGraph={{
url: 'https://example.com',
title: 'Home',
description: 'Welcome to our site',
images: [
{
url: 'https://example.com/og/home.png',
width: 1200,
height: 630,
alt: 'Home page preview',
},
],
siteName: 'Example Site',
}}
twitter={{
cardType: 'summary_large_image',
}}
/>
{/* Page content */}
</>
)
}Astro
Route detection:
- Pages:
src/pages/**/*.{astro,md,mdx} - Index:
src/pages/index.astro→/
OG image integration:
---
// src/pages/index.astro
const title = 'Home'
const description = 'Welcome to our site'
const ogImage = new URL('/og/home.png', Astro.site)
---
<html>
<head>
<title>{title}</title>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:url" content={Astro.url} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={ogImage} />
</head>
<body>
<!-- Page content -->
</body>
</html>Using layout component:
---
// src/layouts/BaseLayout.astro
const { title, description, ogImage = '/og/default.png' } = Astro.props
const ogImageUrl = new URL(ogImage, Astro.site)
---
<html>
<head>
<title>{title}</title>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImageUrl} />
<meta property="og:url" content={Astro.url} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<slot />
</body>
</html>Markdown/MDX pages:
---
# src/pages/blog/post.mdx
title: 'Blog Post Title'
description: 'Post description'
ogImage: '/og/blog-post.png'
---
# Blog Post Title
Content here...React Router / React SPA
Route detection:
- Analyze route configuration in app
- Common patterns:
src/routes.tsxsrc/App.tsx- Route components
Example route config:
// src/routes.tsx
export const routes = [
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> },
{ path: '/blog/:slug', element: <BlogPost /> },
]OG image integration:
Since SPAs often have a single index.html, use React Helmet for per-route meta tags:
// src/pages/Home.tsx
import { Helmet } from 'react-helmet-async'
export default function Home() {
return (
<>
<Helmet>
<title>Home</title>
<meta property="og:title" content="Home" />
<meta property="og:description" content="Welcome to our site" />
<meta property="og:image" content="https://example.com/og/home.png" />
<meta property="og:url" content="https://example.com" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
</Helmet>
{/* Page content */}
</>
)
}Important: For SPAs, social crawlers often can't execute JavaScript, so: 1. Use SSR/SSG if possible (Next.js, Gatsby, Astro) 2. Or use prerendering service (prerender.io, rendertron) 3. Or server-side redirects for social crawlers
Static fallback (public/index.html):
<!-- public/index.html -->
<head>
<!-- Default OG tags for homepage -->
<meta property="og:title" content="Example Site" />
<meta property="og:description" content="Welcome to our site" />
<meta property="og:image" content="https://example.com/og/home.png" />
<meta property="og:url" content="https://example.com" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
</head>Gatsby
Route detection:
- Pages:
src/pages/**/*.{jsx,js,tsx,ts} - Programmatic routes: Check
gatsby-node.js
OG image integration:
Using React Helmet:
// src/pages/index.tsx
import { Helmet } from 'react-helmet'
export default function Home() {
return (
<>
<Helmet>
<title>Home</title>
<meta property="og:title" content="Home" />
<meta property="og:description" content="Welcome to our site" />
<meta property="og:image" content="https://example.com/og/home.png" />
<meta property="og:url" content="https://example.com" />
<meta property="og:type" content="website" />
</Helmet>
{/* Page content */}
</>
)
}Using gatsby-plugin-react-helmet:
// gatsby-config.js
module.exports = {
plugins: ['gatsby-plugin-react-helmet'],
}SEO component pattern:
// src/components/SEO.tsx
import { Helmet } from 'react-helmet'
interface SEOProps {
title: string
description: string
ogImage?: string
url?: string
}
export default function SEO({ title, description, ogImage = '/og/default.png', url }: SEOProps) {
const siteUrl = 'https://example.com'
const ogImageUrl = `${siteUrl}${ogImage}`
const pageUrl = url ? `${siteUrl}${url}` : siteUrl
return (
<Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImageUrl} />
<meta property="og:url" content={pageUrl} />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={ogImageUrl} />
</Helmet>
)
}Static HTML Sites
Route detection:
- HTML files in root or structured directories
- Common patterns:
index.html→/about.html→/aboutblog/post.html→/blog/post
OG image integration:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Home</title>
<!-- Open Graph -->
<meta property="og:title" content="Home">
<meta property="og:description" content="Welcome to our site">
<meta property="og:image" content="https://example.com/og/home.png">
<meta property="og:url" content="https://example.com">
<meta property="og:type" content="website">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://example.com/og/home.png">
</head>
<body>
<!-- Page content -->
</body>
</html>Template approach:
For multiple pages, use a build tool or templating:
// build-meta.js
const fs = require('fs')
const pages = [
{ path: 'index.html', title: 'Home', description: 'Welcome', ogImage: '/og/home.png' },
{ path: 'about.html', title: 'About', description: 'About us', ogImage: '/og/about.png' },
]
pages.forEach(page => {
let html = fs.readFileSync(`templates/${page.path}`, 'utf8')
html = html.replace('{{title}}', page.title)
html = html.replace('{{description}}', page.description)
html = html.replace('{{ogImage}}', `https://example.com${page.ogImage}`)
fs.writeFileSync(`dist/${page.path}`, html)
})Workflow Summary
1. Analyze codebase
python scripts/analyze_codebase.py /path/to/projectThis creates og-analysis.json with:
- Detected framework
- Found routes
- Extracted brand (colors, fonts, logo)
2. Review analysis
cat og-analysis.jsonVerify:
- All routes were found
- Brand colors/fonts extracted correctly
- Page types categorized appropriately
3. Generate OG images
python scripts/generate_og_images.py /path/to/projectThis creates images in public/og/
4. Integrate with framework
Follow framework-specific guidance above to add meta tags.
5. Test
Use platform preview tools:
- https://www.opengraph.xyz/
- https://www.linkedin.com/post-inspector/
- Facebook Sharing Debugger
6. Deploy
Ensure:
- OG images are in
public/or static directory - Images are served over HTTPS
- Absolute URLs used in meta tags
- Images are publicly accessible
Troubleshooting
Routes not detected
Check:
- Is framework correctly detected?
- Are pages in standard locations?
- Are naming conventions followed?
Solution: Manually specify routes in analysis.json
Brand not extracted
Check:
- Do Tailwind/CSS config files exist?
- Are colors defined in standard formats?
Solution: Manually add colors to analysis.json
Images not generated
Check:
- Is Playwright installed? (
playwright install chromium) - Are Python dependencies installed?
- Any error messages?
Solution: Check error logs, install missing deps
Meta tags not working
Check:
- Are absolute URLs used?
- Is site deployed and accessible?
- Are images publicly accessible?
Solution: Use platform debug tools to see what they detect
Open Graph Image Specifications
Standard Dimensions
Recommended: 1200 x 630 pixels (1.91:1 ratio)
This is the standard size supported by all major platforms:
- Twitter/X
- Slack
- Discord
Minimum: 600 x 314 pixels Maximum: 8MB file size (but aim for <200KB)
Image Formats
Recommended: PNG
- Better quality for text
- Supports transparency (though not used in OG context)
- Widely supported
Alternative: JPG
- Smaller file size
- Good for photo-heavy images
- 80-90% quality recommended
Avoid: GIF (limited to first frame), WebP (limited support)
Meta Tags
Essential Tags
<!-- Required -->
<meta property="og:title" content="Page Title" />
<meta property="og:image" content="https://example.com/og-image.png" />
<meta property="og:url" content="https://example.com/page" />
<meta property="og:type" content="website" />
<!-- Highly recommended -->
<meta property="og:description" content="Page description" />
<meta property="og:site_name" content="Site Name" />
<!-- Image details -->
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Description of image content" />Twitter-Specific Tags
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://example.com/og-image.png" />
<meta name="twitter:title" content="Page Title" />
<meta name="twitter:description" content="Page description" />Important: Twitter requires absolute URLs for images.
Design Best Practices
Safe Zone
Keep important content within the safe zone to account for platform overlays:
- Top: 60px padding
- Bottom: 60px padding
- Left/Right: 80px padding
Some platforms overlay logos, play buttons, or other UI elements.
Text Readability
Font sizes:
- Headline: 48-72px
- Subheading: 28-36px
- Body text: 20-28px
Contrast:
- Ensure sufficient contrast (WCAG AA minimum: 4.5:1)
- Text should be readable when scaled down to thumbnail size
Font weights:
- Use bold (700+) for headlines
- Regular (400) or medium (500) for body text
Visual Hierarchy
Focus on 1-2 key elements:
- Primary: Page title or main message
- Secondary: Supporting text or visual
Avoid:
- Too much text (max 2-3 lines for headline)
- Cluttered layouts
- Small text or icons
Color
Background:
- Solid color: Simple, brand-aligned
- Gradient: Subtle, not distracting
- Image: Low opacity or darkened for text contrast
Text:
- High contrast with background
- Use brand colors for accents, not all text
Testing
Validation Tools
1. OpenGraph.xyz: https://www.opengraph.xyz/
- Real-time preview across platforms
- Validates meta tags
- Shows how image appears in feeds
2. LinkedIn Post Inspector: https://www.linkedin.com/post-inspector/
- LinkedIn-specific preview
- Shows cache status
- Force refresh cache
3. Facebook Sharing Debugger: https://developers.facebook.com/tools/debug/
- Facebook preview
- Cache clearing
- Shows detected meta tags
4. Twitter Card Validator: https://cards-dev.twitter.com/validator
- Twitter-specific preview
- Requires login
Testing Checklist
- [ ] Image appears correctly on all platforms
- [ ] Text is readable when scaled down
- [ ] No important content is cut off
- [ ] File size is reasonable (<200KB)
- [ ] All meta tags are present
- [ ] Absolute URLs are used
- [ ] Image serves over HTTPS
Platform-Specific Quirks
- Caches aggressively (use debugger to force refresh)
- Minimum 200x200px
- Prefers 1200x630px
- Max 8MB
Twitter/X
- Requires
twitter:cardset tosummary_large_image - Falls back to OG tags if Twitter tags missing
- Minimum 300x157px
- Prefers 1200x675px (but 1200x630 works fine)
- Max 5MB
- Prefers 1200x627px (but 1200x630 works fine)
- Minimum 1200x628px (strictly enforced)
- Max 5MB
- Caches for ~7 days
Slack
- Uses OG tags
- Shows preview automatically for links
- Respects 1200x630px standard
Discord
- Uses OG tags
- Embeds automatically
- Supports 1200x630px
Common Issues
Image Not Showing
Causes: 1. Using relative URL instead of absolute 2. Image not accessible (404, auth required) 3. No HTTPS (some platforms require it) 4. File too large 5. Platform cache (image updated but old version shows)
Solutions:
- Use absolute URLs:
https://example.com/og-image.png - Verify image is publicly accessible
- Ensure HTTPS
- Compress image (<200KB)
- Clear platform cache using debug tools
Wrong Image Showing
Cause: Platform cached old image
Solution: Use platform-specific cache clearing tools
Image Cut Off
Cause: Content too close to edges
Solution: Use safe zone padding (60px top/bottom, 80px left/right)
Dynamic vs Static
Static Images (Recommended for Brand Consistency)
- Pre-generated for each route
- Full design control
- Faster loading
- Consistent brand appearance
- Use this approach for most sites
Dynamic Images (Next.js OG, Vercel OG)
- Generated on-demand
- Good for user-generated content
- Less design control
- Can look generic if not carefully designed
- Use for dynamic content (user profiles, articles, etc.)
For brand consistency, static images generated from your actual components and brand assets are preferred.
#!/usr/bin/env python3
"""
Analyze a codebase to discover routes, brand identity, and prepare for OG image generation.
Usage:
python analyze_codebase.py [project_path]
Output:
analysis.json with routes, brand colors, fonts, logo paths, and page categorization
"""
import os
import sys
import json
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple
def detect_framework(project_path: Path) -> str:
"""Detect the framework used in the project."""
package_json = project_path / "package.json"
if not package_json.exists():
return "unknown"
with open(package_json, 'r') as f:
try:
pkg = json.load(f)
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
if 'next' in deps:
return 'nextjs'
elif 'astro' in deps:
return 'astro'
elif 'react-router-dom' in deps or 'react-router' in deps:
return 'react-router'
elif 'react' in deps:
return 'react'
except json.JSONDecodeError:
pass
return "unknown"
def find_nextjs_routes(project_path: Path) -> List[Dict]:
"""Find routes in a Next.js project (app router and pages router)."""
routes = []
# App router
app_dir = project_path / "app"
if app_dir.exists():
for page_file in app_dir.rglob("page.{tsx,ts,jsx,js}"):
route_path = page_file.parent.relative_to(app_dir)
route_str = "/" + str(route_path).replace("\\", "/")
# Clean up route groups and handle root
route_str = re.sub(r'/\([^)]+\)', '', route_str)
route_str = re.sub(r'/+', '/', route_str)
if route_str == "/.":
route_str = "/"
metadata = extract_metadata_from_file(page_file)
page_type = categorize_page(route_str, metadata)
routes.append({
"path": route_str,
"file": str(page_file.relative_to(project_path)),
"type": page_type,
"metadata": metadata,
"router": "app"
})
# Pages router
pages_dir = project_path / "pages"
if pages_dir.exists():
for page_file in pages_dir.rglob("*.{tsx,ts,jsx,js}"):
# Skip _app, _document, api routes
if page_file.name.startswith('_') or 'api' in page_file.parts:
continue
route_path = page_file.relative_to(pages_dir)
route_str = "/" + str(route_path.with_suffix('')).replace("\\", "/")
if route_str.endswith("/index"):
route_str = route_str[:-6] or "/"
metadata = extract_metadata_from_file(page_file)
page_type = categorize_page(route_str, metadata)
routes.append({
"path": route_str,
"file": str(page_file.relative_to(project_path)),
"type": page_type,
"metadata": metadata,
"router": "pages"
})
return routes
def find_astro_routes(project_path: Path) -> List[Dict]:
"""Find routes in an Astro project."""
routes = []
pages_dir = project_path / "src" / "pages"
if not pages_dir.exists():
return routes
for page_file in pages_dir.rglob("*.{astro,md,mdx}"):
route_path = page_file.relative_to(pages_dir)
route_str = "/" + str(route_path.with_suffix('')).replace("\\", "/")
if route_str.endswith("/index"):
route_str = route_str[:-6] or "/"
metadata = extract_metadata_from_file(page_file)
page_type = categorize_page(route_str, metadata)
routes.append({
"path": route_str,
"file": str(page_file.relative_to(project_path)),
"type": page_type,
"metadata": metadata,
"framework": "astro"
})
return routes
def find_react_routes(project_path: Path) -> List[Dict]:
"""Find routes in a React SPA (best effort)."""
routes = []
# Look for common route definition patterns
src_dir = project_path / "src"
if not src_dir.exists():
return routes
for file_path in src_dir.rglob("*.{tsx,ts,jsx,js}"):
content = file_path.read_text(errors='ignore')
# Look for react-router Route definitions
route_matches = re.findall(r'<Route\s+path=["\']([^"\']+)["\']', content)
for route_path in route_matches:
if route_path and not route_path.startswith(':'):
routes.append({
"path": route_path,
"file": str(file_path.relative_to(project_path)),
"type": categorize_page(route_path, {}),
"metadata": {},
"framework": "react-router"
})
# Deduplicate
seen = set()
unique_routes = []
for route in routes:
if route['path'] not in seen:
seen.add(route['path'])
unique_routes.append(route)
return unique_routes
def extract_metadata_from_file(file_path: Path) -> Dict:
"""Extract title and description from a file."""
metadata = {}
try:
content = file_path.read_text(errors='ignore')
# Look for title
title_patterns = [
r'<title>([^<]+)</title>',
r'title:\s*["\']([^"\']+)["\']',
r'<h1[^>]*>([^<]+)</h1>',
r'title:\s*[\'"]([^\'"]+)[\'"]',
]
for pattern in title_patterns:
match = re.search(pattern, content, re.IGNORECASE)
if match:
metadata['title'] = match.group(1).strip()
break
# Look for description
desc_patterns = [
r'<meta\s+name=["\']description["\']\s+content=["\']([^"\']+)["\']',
r'description:\s*["\']([^"\']+)["\']',
]
for pattern in desc_patterns:
match = re.search(pattern, content, re.IGNORECASE)
if match:
metadata['description'] = match.group(1).strip()
break
except Exception:
pass
return metadata
def categorize_page(route: str, metadata: Dict) -> str:
"""Categorize a page based on its route and metadata."""
route_lower = route.lower()
title_lower = metadata.get('title', '').lower()
# Homepage
if route == '/' or route == '/index':
return 'landing'
# Common page types
if any(x in route_lower for x in ['/blog', '/post', '/article', '/news']):
return 'article'
if any(x in route_lower for x in ['/product', '/shop', '/store', '/item']):
return 'product'
if any(x in route_lower for x in ['/doc', '/guide', '/api', '/reference']):
return 'documentation'
if any(x in route_lower for x in ['/about', '/team', '/contact', '/company']):
return 'about'
# Check title for hints
if any(x in title_lower for x in ['blog', 'article', 'post']):
return 'article'
return 'general'
def extract_brand_colors(project_path: Path) -> List[str]:
"""Extract brand colors from CSS, Tailwind config, or other sources."""
colors = []
# Check Tailwind config
tailwind_configs = [
project_path / "tailwind.config.js",
project_path / "tailwind.config.ts",
]
for config_file in tailwind_configs:
if config_file.exists():
content = config_file.read_text(errors='ignore')
# Extract hex colors
hex_colors = re.findall(r'#[0-9a-fA-F]{6}', content)
colors.extend(hex_colors)
# Extract color names and their values
color_matches = re.findall(r'(\w+):\s*["\']?(#[0-9a-fA-F]{6})["\']?', content)
if color_matches:
colors.extend([color[1] for color in color_matches])
# Check CSS files for custom properties
for css_file in project_path.rglob("*.css"):
if 'node_modules' in str(css_file):
continue
content = css_file.read_text(errors='ignore')
hex_colors = re.findall(r'#[0-9a-fA-F]{6}', content)
colors.extend(hex_colors)
# Deduplicate and return top colors
unique_colors = list(dict.fromkeys(colors))
return unique_colors[:10] # Return top 10 unique colors
def extract_fonts(project_path: Path) -> List[str]:
"""Extract font families used in the project."""
fonts = []
# Check CSS files
for css_file in project_path.rglob("*.css"):
if 'node_modules' in str(css_file):
continue
content = css_file.read_text(errors='ignore')
# Look for font-family declarations
font_matches = re.findall(r'font-family:\s*([^;]+);', content)
fonts.extend(font_matches)
# Check Tailwind config
tailwind_configs = [
project_path / "tailwind.config.js",
project_path / "tailwind.config.ts",
]
for config_file in tailwind_configs:
if config_file.exists():
content = config_file.read_text(errors='ignore')
font_matches = re.findall(r'fontFamily:\s*{([^}]+)}', content, re.DOTALL)
if font_matches:
fonts.extend(font_matches)
# Clean and deduplicate
cleaned_fonts = []
for font in fonts:
# Remove quotes, brackets, and clean up
cleaned = re.sub(r'[\[\]"\']', '', font)
cleaned = cleaned.split(',')[0].strip()
if cleaned and cleaned not in cleaned_fonts:
cleaned_fonts.append(cleaned)
return cleaned_fonts[:5] # Return top 5 fonts
def find_logo(project_path: Path) -> Optional[str]:
"""Find the logo file in the project."""
common_logo_paths = [
"public/logo.svg",
"public/logo.png",
"public/images/logo.svg",
"public/images/logo.png",
"public/assets/logo.svg",
"public/assets/logo.png",
"src/assets/logo.svg",
"src/assets/logo.png",
"assets/logo.svg",
"assets/logo.png",
]
for logo_path in common_logo_paths:
full_path = project_path / logo_path
if full_path.exists():
return logo_path
# Search for logo files
for ext in ['.svg', '.png', '.jpg']:
for logo_file in project_path.rglob(f"*logo*{ext}"):
if 'node_modules' not in str(logo_file):
return str(logo_file.relative_to(project_path))
return None
def analyze_codebase(project_path: Path) -> Dict:
"""Perform full codebase analysis."""
framework = detect_framework(project_path)
print(f"Detected framework: {framework}")
# Find routes based on framework
routes = []
if framework == 'nextjs':
routes = find_nextjs_routes(project_path)
elif framework == 'astro':
routes = find_astro_routes(project_path)
elif framework in ['react-router', 'react']:
routes = find_react_routes(project_path)
print(f"Found {len(routes)} routes")
# Extract brand
colors = extract_brand_colors(project_path)
fonts = extract_fonts(project_path)
logo = find_logo(project_path)
print(f"Extracted {len(colors)} colors, {len(fonts)} fonts")
if logo:
print(f"Found logo: {logo}")
return {
"framework": framework,
"routes": routes,
"brand": {
"colors": colors,
"fonts": fonts,
"logo": logo
}
}
def main():
project_path = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
if not project_path.exists():
print(f"Error: Path {project_path} does not exist")
sys.exit(1)
print(f"Analyzing codebase at: {project_path.resolve()}")
print("-" * 50)
analysis = analyze_codebase(project_path)
# Save to JSON
output_file = project_path / "og-analysis.json"
with open(output_file, 'w') as f:
json.dump(analysis, f, indent=2)
print("-" * 50)
print(f"Analysis saved to: {output_file}")
# Summary
print("\nSummary:")
print(f" Framework: {analysis['framework']}")
print(f" Routes: {len(analysis['routes'])}")
print(f" Page types:")
type_counts = {}
for route in analysis['routes']:
page_type = route['type']
type_counts[page_type] = type_counts.get(page_type, 0) + 1
for page_type, count in sorted(type_counts.items()):
print(f" - {page_type}: {count}")
print(f"\n Brand colors: {len(analysis['brand']['colors'])}")
print(f" Fonts: {len(analysis['brand']['fonts'])}")
print(f" Logo: {analysis['brand']['logo'] or 'Not found'}")
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Generate OG images for routes based on codebase analysis.
Usage:
python generate_og_images.py [project_path]
Requirements:
pip install playwright
playwright install chromium
Input:
og-analysis.json (from analyze_codebase.py)
Output:
OG images in public/og/ directory
"""
import os
import sys
import json
import asyncio
from pathlib import Path
from typing import Dict, List, Optional
from playwright.async_api import async_playwright
# OG image standard dimensions
OG_WIDTH = 1200
OG_HEIGHT = 630
def get_layout_template(page_type: str) -> str:
"""Get HTML template based on page type."""
# Different templates for different page types
templates = {
'landing': '''
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 80px;">
<h1 style="font-size: 72px; font-weight: 800; margin: 0 0 30px 0; line-height: 1.1; max-width: 900px;">{{title}}</h1>
{{#description}}
<p style="font-size: 32px; opacity: 0.8; margin: 0; max-width: 800px;">{{description}}</p>
{{/description}}
</div>
''',
'article': '''
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%; padding: 60px 80px;">
<div>
<div style="font-size: 20px; text-transform: uppercase; letter-spacing: 2px; opacity: 0.6; margin-bottom: 30px;">Article</div>
<h1 style="font-size: 56px; font-weight: 700; margin: 0 0 20px 0; line-height: 1.2; max-width: 1000px;">{{title}}</h1>
{{#description}}
<p style="font-size: 28px; opacity: 0.7; margin: 0; max-width: 900px; line-height: 1.4;">{{description}}</p>
{{/description}}
</div>
{{#logo}}
<div style="display: flex; align-items: center;">
<img src="{{logo}}" style="height: 40px;" />
</div>
{{/logo}}
</div>
''',
'product': '''
<div style="display: flex; align-items: center; height: 100%; padding: 80px;">
<div style="flex: 1;">
<h1 style="font-size: 64px; font-weight: 800; margin: 0 0 30px 0; line-height: 1.1;">{{title}}</h1>
{{#description}}
<p style="font-size: 28px; opacity: 0.8; margin: 0; line-height: 1.4;">{{description}}</p>
{{/description}}
</div>
</div>
''',
'documentation': '''
<div style="display: flex; flex-direction: column; justify-content: center; height: 100%; padding: 60px 80px;">
<div style="font-size: 20px; text-transform: uppercase; letter-spacing: 2px; opacity: 0.6; margin-bottom: 20px;">Documentation</div>
<h1 style="font-size: 60px; font-weight: 700; margin: 0 0 25px 0; line-height: 1.2; max-width: 1000px;">{{title}}</h1>
{{#description}}
<p style="font-size: 26px; opacity: 0.75; margin: 0; max-width: 900px; line-height: 1.4;">{{description}}</p>
{{/description}}
</div>
''',
'about': '''
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 80px;">
{{#logo}}
<img src="{{logo}}" style="height: 100px; margin-bottom: 50px;" />
{{/logo}}
<h1 style="font-size: 64px; font-weight: 700; margin: 0 0 30px 0; line-height: 1.1; max-width: 900px;">{{title}}</h1>
{{#description}}
<p style="font-size: 30px; opacity: 0.8; margin: 0; max-width: 800px;">{{description}}</p>
{{/description}}
</div>
''',
'general': '''
<div style="display: flex; flex-direction: column; justify-content: center; height: 100%; padding: 80px;">
<h1 style="font-size: 64px; font-weight: 700; margin: 0 0 30px 0; line-height: 1.2; max-width: 1000px;">{{title}}</h1>
{{#description}}
<p style="font-size: 28px; opacity: 0.8; margin: 0; max-width: 900px; line-height: 1.4;">{{description}}</p>
{{/description}}
</div>
'''
}
return templates.get(page_type, templates['general'])
def simple_mustache_render(template: str, data: Dict) -> str:
"""Simple mustache-like template rendering."""
result = template
# Replace {{variable}}
for key, value in data.items():
if value:
result = result.replace(f'{{{{{key}}}}}', str(value))
# Handle {{#variable}}...{{/variable}} blocks
import re
for key in data.keys():
pattern = f'{{{{#{key}}}}}(.*?){{{{/{key}}}}}'
if data.get(key):
# Keep the content, remove the tags
result = re.sub(pattern, r'\1', result, flags=re.DOTALL)
else:
# Remove the entire block
result = re.sub(pattern, '', result, flags=re.DOTALL)
return result
def generate_html(route: Dict, brand: Dict) -> str:
"""Generate complete HTML for a route's OG image."""
# Get template for page type
template = get_layout_template(route['type'])
# Prepare data
title = route['metadata'].get('title') or route['path'].strip('/').replace('-', ' ').title() or 'Home'
description = route['metadata'].get('description', '')
logo = brand.get('logo', '')
# If logo is relative path, we need to handle it
logo_url = ''
if logo:
# For local files, we'll use absolute path or data URI later
logo_url = logo
template_data = {
'title': title,
'description': description,
'logo': logo_url
}
content_html = simple_mustache_render(template, template_data)
# Determine colors
primary_color = brand['colors'][0] if brand['colors'] else '#000000'
background_color = '#ffffff'
text_color = '#000000'
# If primary color is dark, use it as background
if is_dark_color(primary_color):
background_color = primary_color
text_color = '#ffffff'
else:
# Light primary color - use as accent or gradient
background_color = f'linear-gradient(135deg, {primary_color}10 0%, {primary_color}30 100%)'
# Determine font
font_family = brand['fonts'][0] if brand['fonts'] else 'system-ui, -apple-system, sans-serif'
# Build complete HTML
html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
width: {OG_WIDTH}px;
height: {OG_HEIGHT}px;
background: {background_color};
color: {text_color};
font-family: {font_family};
overflow: hidden;
}}
.container {{
width: 100%;
height: 100%;
position: relative;
}}
</style>
</head>
<body>
<div class="container">
{content_html}
</div>
</body>
</html>
'''
return html
def is_dark_color(hex_color: str) -> bool:
"""Determine if a hex color is dark."""
hex_color = hex_color.lstrip('#')
try:
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
# Calculate luminance
luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
return luminance < 0.5
except:
return False
async def generate_og_image(html: str, output_path: Path):
"""Generate OG image from HTML using Playwright."""
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(viewport={'width': OG_WIDTH, 'height': OG_HEIGHT})
await page.set_content(html)
# Wait for any fonts or images to load
await page.wait_for_timeout(500)
# Take screenshot
await page.screenshot(path=output_path, type='png')
await browser.close()
async def generate_all_images(project_path: Path, analysis: Dict):
"""Generate OG images for all routes."""
routes = analysis['routes']
brand = analysis['brand']
# Create output directory
og_dir = project_path / "public" / "og"
og_dir.mkdir(parents=True, exist_ok=True)
print(f"Generating {len(routes)} OG images...")
print(f"Output directory: {og_dir}")
print("-" * 50)
for i, route in enumerate(routes, 1):
# Generate filename from route path
filename = route['path'].strip('/').replace('/', '-') or 'home'
filename = f"{filename}.png"
output_path = og_dir / filename
print(f"[{i}/{len(routes)}] {route['path']} ({route['type']}) -> {filename}")
# Generate HTML
html = generate_html(route, brand)
# Generate image
try:
await generate_og_image(html, output_path)
print(f" ✓ Generated: {output_path.relative_to(project_path)}")
except Exception as e:
print(f" ✗ Error: {e}")
print("-" * 50)
print(f"Generated {len(routes)} OG images in {og_dir.relative_to(project_path)}")
def main():
project_path = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
if not project_path.exists():
print(f"Error: Path {project_path} does not exist")
sys.exit(1)
# Load analysis
analysis_file = project_path / "og-analysis.json"
if not analysis_file.exists():
print(f"Error: {analysis_file} not found. Run analyze_codebase.py first.")
sys.exit(1)
with open(analysis_file, 'r') as f:
analysis = json.load(f)
print(f"Loaded analysis from: {analysis_file}")
print(f"Framework: {analysis['framework']}")
print(f"Routes: {len(analysis['routes'])}")
print()
# Generate images
asyncio.run(generate_all_images(project_path, analysis))
# Print next steps
print("\nNext steps:")
print("1. Review generated images in public/og/")
print("2. Add OG image meta tags to your pages:")
print(' <meta property="og:image" content="/og/[route].png" />')
print("3. Test with opengraph.xyz or LinkedIn Post Inspector")
if __name__ == '__main__':
main()