Appearance
Astro Integration
Generate OG images for Astro websites.
Setup
1. Environment Variable
bash
# .env
OG_IMAGE_API_KEY=og_your_api_key_here2. API Endpoint
javascript
// src/pages/api/og.png.js
export async function GET({ request }) {
const url = new URL(request.url);
const title = url.searchParams.get('title') || 'My Astro Site';
const subtitle = url.searchParams.get('subtitle') || '';
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': import.meta.env.OG_IMAGE_API_KEY
},
body: JSON.stringify({
title,
subtitle,
template: 'default',
theme: 'dark'
})
});
const imageBuffer = await response.arrayBuffer();
return new Response(imageBuffer, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400'
}
});
}SEO Component
astro
---
// src/components/SEO.astro
interface Props {
title: string;
description?: string;
image?: string;
type?: string;
}
const {
title,
description = '',
image = null,
type = 'website'
} = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const ogImage = image || `/api/og.png?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;
---
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:image" content={ogImage} />
<meta property="og:type" content={type} />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />Usage in Pages
astro
---
// src/pages/blog/[slug].astro
import SEO from '../../components/SEO.astro';
import { getPost } from '../../lib/posts';
export async function getStaticPaths() {
const posts = await getAllPosts();
return posts.map((post) => ({
params: { slug: post.slug },
props: { post }
}));
}
const { post } = Astro.props;
---
<html>
<head>
<SEO
title={post.title}
description={post.excerpt}
type="article"
/>
</head>
<body>
<article>
<h1>{post.title}</h1>
<Fragment set:html={post.content} />
</article>
</body>
</html>Build-Time Generation
Generate all OG images during build:
Integration
javascript
// integrations/og-images.js
import fs from 'fs';
import path from 'path';
export default function ogImages() {
return {
name: 'og-images',
hooks: {
'astro:build:done': async ({ pages }) => {
console.log('Generating OG images...');
const outputDir = './dist/og';
fs.mkdirSync(outputDir, { recursive: true });
for (const page of pages) {
if (page.pathname.startsWith('blog/')) {
const slug = page.pathname.replace('blog/', '').replace('/', '');
const post = await getPost(slug);
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.OG_IMAGE_API_KEY
},
body: JSON.stringify({
title: post.title,
subtitle: post.excerpt,
author_name: post.author,
template: 'blog',
theme: 'dark'
})
});
const buffer = await response.arrayBuffer();
fs.writeFileSync(
path.join(outputDir, `${slug}.png`),
Buffer.from(buffer)
);
console.log(`✓ ${slug}.png`);
}
}
}
}
};
}Astro Config
javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import ogImages from './integrations/og-images.js';
export default defineConfig({
integrations: [ogImages()]
});Content Collections
astro
---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
import SEO from '../../components/SEO.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.slug },
props: { post }
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
const ogImage = `/og/${post.slug}.png`;
---
<html>
<head>
<SEO
title={post.data.title}
description={post.data.description}
image={ogImage}
type="article"
/>
</head>
<body>
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
</body>
</html>Script for Batch Generation
javascript
// scripts/generate-og.mjs
import { getCollection } from 'astro:content';
import fs from 'fs';
import path from 'path';
async function generateOGImages() {
const posts = await getCollection('blog');
const outputDir = './public/og';
fs.mkdirSync(outputDir, { recursive: true });
for (const post of posts) {
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.OG_IMAGE_API_KEY
},
body: JSON.stringify({
title: post.data.title,
subtitle: post.data.description,
author_name: post.data.author,
template: 'blog',
theme: 'dark'
})
});
const buffer = await response.arrayBuffer();
fs.writeFileSync(
path.join(outputDir, `${post.slug}.png`),
Buffer.from(buffer)
);
console.log(`Generated: ${post.slug}.png`);
}
}
generateOGImages();SSR Mode
For server-rendered Astro:
javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel()
});astro
---
// src/pages/og/[slug].png.js
export async function GET({ params }) {
const post = await getPost(params.slug);
const response = await fetch('https://ogimageapi.io/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': import.meta.env.OG_IMAGE_API_KEY
},
body: JSON.stringify({
title: post.title,
subtitle: post.excerpt,
template: 'blog',
theme: 'dark'
})
});
return new Response(await response.arrayBuffer(), {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=86400'
}
});
}
---Best Practices
- Generate at build time for static sites
- Use integrations for automatic generation
- Cache responses when using SSR
- Use content collections for typed content
- Test locally before deploying