Generate dynamic images (OG images, social media cards, etc.) using React components in your Astro projects. Powered by Takumi.
- ๐จ Design images with React components and JSX
- ๐ผ๏ธ Generate PNG, JPEG, or JPG images
- ๐ Debug mode to preview templates in browser
- ๐ Built-in emoji support with Twemoji
- ๐ Automatic multilingual support (Thai, Japanese, Korean, Arabic)
- ๐ค Custom fonts with automatic fallbacks
- โก Fast Rust-based rendering
pnpm add @bearstudio/astro-assets-generationUpdate your astro.config.mjs:
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
vite: {
optimizeDeps: {
exclude: [
"@takumi-rs/image-response",
"@takumi-rs/core",
"@takumi-rs/helpers",
],
},
ssr: {
noExternal: [
"@takumi-rs/image-response",
"@takumi-rs/core",
"@takumi-rs/helpers",
"@bearstudio/astro-assets-generation",
],
},
},
integrations: [react()],
});Create a configuration file (e.g., src/lib/assets.ts):
import { configure } from "@bearstudio/astro-assets-generation";
configure({
debugBackground: "#0a0a0a",
siteUrl: import.meta.env.SITE ?? "http://localhost:4321",
isDev: import.meta.env.DEV,
customFonts: [
// Optional
{
name: "Geist",
url: "/fonts/Geist.ttf",
weight: 400,
style: "normal",
},
],
});Create a React component prefixed with _:
// src/pages/blog/[slug]/assets/_og-image.tsx
import { FontWrapper } from "@bearstudio/astro-assets-generation";
import type { AssetImageConfig } from "@bearstudio/astro-assets-generation";
export const config: AssetImageConfig = {
width: 1200,
height: 630,
debugScale: 0.5, // Optional: for debug view
};
export default function OgImage({ params }: { params: { slug: string } }) {
return (
<FontWrapper fontFamily="Geist">
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "100%",
padding: 64,
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
}}
>
<h1 style={{ color: "white", fontSize: 72, fontWeight: "bold" }}>
My Blog Post
</h1>
<p style={{ color: "white", fontSize: 24 }}>Post: {params.slug}</p>
</div>
</FontWrapper>
);
}// src/pages/blog/[slug]/assets/[__image].[__type].ts
import {
apiImageEndpoint,
getStaticPathsForAssets,
} from "@bearstudio/astro-assets-generation";
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
import "@/lib/assets"; // Import your config
const modules = import.meta.glob("./_*.tsx", { eager: true });
export const getStaticPaths = async () => {
const posts = await getCollection("blog");
return getStaticPathsForAssets(
modules,
posts.map((post) => ({ slug: post.id }))
);
};
export const GET: APIRoute = apiImageEndpoint(modules);- PNG:
/blog/my-post/assets/og-image.png - JPEG:
/blog/my-post/assets/og-image.jpg - Debug:
/blog/my-post/assets/og-image.debug
By default, the examples above use getStaticPaths to pre-generate images at build time โ no server adapter required.
If you need to generate images on-demand (e.g., for a very large number of routes or frequently changing content), you can use prerender = false instead. This requires an Astro server adapter.
pnpm astro add vercel
# or
pnpm astro add nodeimport { defineConfig } from "astro/config";
import react from "@astrojs/react";
import vercel from "@astrojs/vercel";
export default defineConfig({
vite: {
optimizeDeps: {
exclude: [
"@takumi-rs/image-response",
"@takumi-rs/core",
"@takumi-rs/helpers",
],
},
ssr: {
noExternal: [
"@takumi-rs/image-response",
"@takumi-rs/core",
"@takumi-rs/helpers",
"@bearstudio/astro-assets-generation",
],
},
},
integrations: [react()],
adapter: vercel(),
});Replace getStaticPaths with prerender = false:
// src/pages/blog/[slug]/assets/[__image].[__type].ts
import { apiImageEndpoint } from "@bearstudio/astro-assets-generation";
import type { APIRoute } from "astro";
import "@/lib/assets";
export const prerender = false;
export const GET: APIRoute = apiImageEndpoint(
import.meta.glob("./_*.tsx", { eager: true })
);The library automatically includes fonts for Thai, Japanese, Korean, and Arabic. These are used as fallbacks.
Add fonts in your configuration:
const customFonts: FontConfig[] = [
{
name: "Geist", // Must match font's internal name
url: "/fonts/Geist.ttf", // Path or URL
weight: 400,
style: "normal",
},
];Automatically creates a font stack with fallbacks:
import { FontWrapper } from "@bearstudio/astro-assets-generation";
<FontWrapper fontFamily="Geist">
<div style={{ padding: 64 }}>
<p>English, ๆฅๆฌ่ช, ํ๊ตญ์ด, ุงูุนุฑุจูุฉ, เนเธเธข - all supported!</p>
</div>
</FontWrapper>;Use the Emoji component for crisp emoji rendering at any size:
import { Emoji } from "@bearstudio/astro-assets-generation";
return (
<h1>
<span>Hello world</span> <Emoji emoji={๐} size={64} />
</h1>
);Use the TextWithEmoji component to correctly display a text containing nested emojis
import { TextWithEmoji } from "@bearstudio/astro-assets-generation";
return (
<h1>
<TextWithEmoji>Hello ๐ World ๐</TextWithEmoji>
</h1>
);Use inline styles via the style prop:
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
backgroundColor: "#3b82f6",
}}
>
<h1 style={{ color: "white", fontSize: 96, fontWeight: "bold" }}>Hello</h1>
</div>import { getAstroImageBase64 } from "@bearstudio/astro-assets-generation";
const avatarBase64 = await getAstroImageBase64(author.data.avatar);
<img
src={avatarBase64}
style={{ width: 128, height: 128, borderRadius: 9999 }}
/>;<img
src="https://example.com/image.jpg"
style={{ width: 256, height: 256 }}
/>Use jsxToBase64 to render any JSX component as a PNG and get a base64 data URI. This is useful for embedding generated images inside other templates.
import { jsxToBase64 } from "@bearstudio/astro-assets-generation";
const base64 = await jsxToBase64(
<div tw="flex items-center justify-center w-full h-full bg-blue-500">
<h1 tw="text-white text-4xl">Hello</h1>
</div>,
{ width: 600, height: 300 }
);
<img src={base64} tw="w-64 h-32" />;configure({
debugBackground: "#0a0a0a",
siteUrl: "https://example.com",
isDev: import.meta.env.DEV,
customFonts: [
/* ... */
],
});Creates an Astro API route handler:
export const GET = apiImageEndpoint(
import.meta.glob("./_*.tsx", { eager: true })
);Generates static paths for all combinations of templates and image types. Automatically derives template names from the glob modules.
const modules = import.meta.glob("./_*.tsx", { eager: true });
export const getStaticPaths = async () => {
const posts = await getCollection("blog");
return getStaticPathsForAssets(
modules,
posts.map((post) => ({ slug: post.id }))
// optional 3rd arg: ["png", "jpg"] by default
);
};Converts Astro image to base64 data URI.
Renders a JSX element as a PNG and returns a base64 data URI. Useful for embedding a generated image inside another template.
const base64 = await jsxToBase64(<MyComponent />, { width: 600, height: 300 });Renders emojis using Twemoji SVGs.
Renders a text string containing emojis, automatically splitting and rendering each emoji with Twemoji SVGs at the correct size.
Wraps content with automatic font fallback support.
interface AssetImageConfig {
width: number;
height: number;
debugScale?: number; // Default: 0.5
}interface FontConfig {
name: string;
url: string;
weight: number;
style: "normal" | "italic";
}Images not generating?
- Verify template starts with
_(e.g.,_og-image.tsx) - Check API route uses
[__image].[__type].ts(double underscores) - Import config file in API route
Fonts not loading?
- Verify font name matches internal font name
- Check
siteUrlis correct in production - Ensure fonts are accessible from production URL
Styling issues?
- Use the
styleprop with inline CSS objects - Test in debug mode (
.debugextension) - Stick to well-supported CSS features
// src/pages/blog/[slug]/assets/_og-image.tsx
import {
FontWrapper,
TextWithEmoji,
} from "@bearstudio/astro-assets-generation";
import { getEntry } from "astro:content";
export const config = { width: 1200, height: 630 };
export default async function BlogOgImage({
params,
}: {
params: { slug: string };
}) {
const post = await getEntry("blog", params.slug);
return (
<FontWrapper fontFamily="Geist" style={{ width: "100%", height: "100%" }}>
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "100%",
padding: 64,
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: "auto",
}}
>
<h1 style={{ color: "white", fontSize: 72, fontWeight: "bold" }}>
<TextWithEmoji>{post.data.title}</TextWithEmoji>
</h1>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<p style={{ color: "white", fontSize: 24 }}>{post.data.author}</p>
<p style={{ color: "white", fontSize: 24 }}>
{new Date(post.data.date).toLocaleDateString()}
</p>
</div>
</div>
</FontWrapper>
);
}MIT