npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bearstudio/astro-assets-generation

v0.1.0

Published

Astro integration to generate assets with Takumi

Downloads

73

Readme

@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