Takumi
Integrate Takumi for framework agnostic and fast metadata image generation.
Installation
npm install @takumi-rs/image-responsepnpm add @takumi-rs/image-responseyarn add @takumi-rs/image-responsebun add @takumi-rs/image-responseMake sure to add @takumi-rs/image-response to serverExternalPackages (Next.js) or external depends on what bundler you are using.
import type { NextConfig } from 'next';
const config: NextConfig = {
serverExternalPackages: ['@takumi-rs/image-response'],
};import { defineConfig } from 'vite';
import takumiPackageJson from '@takumi-rs/core/package.json' with { type: 'json' };
export default defineConfig({
nitro: {
externals: {
external: ['@takumi-rs/core'],
traceInclude: Object.keys(takumiPackageJson.optionalDependencies),
},
},
});import { defineConfig } from 'vite';
export default defineConfig({
ssr: {
external: ['@takumi-rs/image-response'],
},
});Metadata Image
You can generate metadata images dynamically using @takumi-rs/image-response.
Add the following under your loader, and define image metadata for pages:
import { type InferPageType } from '@hanzo/docs-core/source';
export function getPageImage(page: InferPageType<typeof source>) {
const segments = [...page.slugs, 'image.webp'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
}import { notFound } from 'next/navigation';
import { source, getPageImage } from '@/lib/source';
import type { Metadata } from 'next';
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
};
}import { getPageImage } from '@/lib/source';
// Somewhere in your component or client loader
return (
<>
<meta name="og:image" content={getPageImage(page).url} />
</>
);We append image.webp to the end of slugs so that we can access it via /og/docs/my-page/image.webp, which results in smaller image sizes. You could use image.png if you prefer.
Finally, create a route handler to generate images at build time:
import { getPageImage, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from '@takumi-rs/image-response';
import { generate as DefaultImage } from '@hanzo/docs-ui/og';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse(
<DefaultImage title={page.data.title} description={page.data.description} site="My App" />,
{
width: 1200,
height: 630,
format: 'webp',
},
);
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
}));
}import { ImageResponse } from '@takumi-rs/image-response';
import { source } from '@/lib/source';
import { generate as DefaultImage } from '@hanzo/docs-ui/og';
import type { Route } from './+types/og';
export function loader({ params }: Route.LoaderArgs) {
const slugs = params['*']
.split('/')
.filter((v) => v.length > 0)
.slice(0, -1);
const page = source.getPage(slugs);
if (!page) throw new Response(undefined, { status: 404 });
return new ImageResponse(
<DefaultImage title={page.data.title} description={page.data.description} site="My App" />,
{
width: 1200,
height: 630,
format: 'webp',
},
);
}import { type RouteConfig, route } from '@react-router/dev/routes';
export default [
route('/og/docs/*', 'docs/og.tsx'),
] satisfies RouteConfig;import type { Config } from '@react-router/dev/config';
function getDocsPaths() {
const paths = Object.keys(
import.meta.glob('./**/*.{mdx,md}', {
base: './content',
}),
);
return paths.map(relativeFileNameToPath);
}
function relativeFileNameToPath(name: string) {
const path = name.slice(1).replace(/(index)?\.(md|mdx)$/, '');
if (!path.endsWith('/')) return `${path}/`;
return path;
}
export default {
prerender({ getStaticPaths }) {
const docsPaths = getDocsPaths();
const docsOgPaths = docsPaths.map((path) => `/og${path}image.webp`);
return [...getStaticPaths(), ...docsPaths, ...docsOgPaths];
},
routeDiscovery: {
mode: 'initial',
},
} satisfies Config;Takumi comes with pre-bundled full-axis (100-900) Geist and Geist Mono fonts, so you don't need to worry about it.
See Takumi's Documentation for more details or advanced usage.
Other Templates
There's other available templates, see Takumi Templates for a full list.
