Skip to content

BearStudio/astro-assets-generation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

24 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

@bearstudio/astro-assets-generation

Generate dynamic images (OG images, social media cards, etc.) using React components in your Astro projects. Powered by Takumi.

Features

  • ๐ŸŽจ 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

Installation

pnpm add @bearstudio/astro-assets-generation

Quick Start

1. Configure Astro

Update 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()],
});

2. Configure the Library

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",
    },
  ],
});

3. Create a Template

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>
  );
}

4. Create API Route

// 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);

5. Access Your Images

  • PNG: /blog/my-post/assets/og-image.png
  • JPEG: /blog/my-post/assets/og-image.jpg
  • Debug: /blog/my-post/assets/og-image.debug

Using Dynamic Routes (with an Adapter)

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.

Install an adapter

pnpm astro add vercel
# or
pnpm astro add node

Update astro.config.mjs

import { 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(),
});

Update API Route

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 })
);

Font Management

Built-in Fonts

The library automatically includes fonts for Thai, Japanese, Korean, and Arabic. These are used as fallbacks.

Custom Fonts

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",
  },
];

FontWrapper Component

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>;

Emoji Support

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>
);

Styling

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>

Working with Images

Astro Images

import { getAstroImageBase64 } from "@bearstudio/astro-assets-generation";

const avatarBase64 = await getAstroImageBase64(author.data.avatar);

<img
  src={avatarBase64}
  style={{ width: 128, height: 128, borderRadius: 9999 }}
/>;

External Images

<img
  src="https://example.com/image.jpg"
  style={{ width: 256, height: 256 }}
/>

JSX to Base64

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" />;

API Reference

Functions

configure(config)

configure({
  debugBackground: "#0a0a0a",
  siteUrl: "https://example.com",
  isDev: import.meta.env.DEV,
  customFonts: [
    /* ... */
  ],
});

apiImageEndpoint(modules)

Creates an Astro API route handler:

export const GET = apiImageEndpoint(
  import.meta.glob("./_*.tsx", { eager: true })
);

getStaticPathsForAssets(modules, parentParams, imageTypes?)

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
  );
};

getAstroImageBase64(image)

Converts Astro image to base64 data URI.

jsxToBase64(component, config)

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 });

Components

<Emoji emoji="๐Ÿš€" size={64} />

Renders emojis using Twemoji SVGs.

<TextWithEmoji>

Renders a text string containing emojis, automatically splitting and rendering each emoji with Twemoji SVGs at the correct size.

<FontWrapper fontFamily="Geist">

Wraps content with automatic font fallback support.

Types

AssetImageConfig

interface AssetImageConfig {
  width: number;
  height: number;
  debugScale?: number; // Default: 0.5
}

FontConfig

interface FontConfig {
  name: string;
  url: string;
  weight: number;
  style: "normal" | "italic";
}

Troubleshooting

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 siteUrl is correct in production
  • Ensure fonts are accessible from production URL

Styling issues?

  • Use the style prop with inline CSS objects
  • Test in debug mode (.debug extension)
  • Stick to well-supported CSS features

Example: Blog OG Image

// 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>
  );
}

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors