Satori
Satori (悟り) is a Japanese Buddhist term for awakening, “comprehension; understanding”. In the context of web development, Satori is a powerful library for generating SVG images from HTML and CSS.
Overview
Satori is a library that converts HTML and CSS into SVG images. It’s particularly useful for generating social media images, thumbnails, and other visual content programmatically.
Key Features
1. HTML to SVG Conversion
- Converts HTML markup to SVG images
- Supports CSS styling and layout
- Generates high-quality vector graphics
2. Font Support
- Built-in font rendering
- Custom font loading
- Multiple font weight support
3. CSS Layout Engine
- Flexbox support
- CSS Grid support
- Responsive design capabilities
4. Performance
- Fast rendering
- Memory efficient
- Optimized for server-side generation
Installation
npm install satori
Basic Usage
Simple Example
import satori from 'satori'
import { join } from 'path'
import { readFileSync } from 'fs'
// Load a font
const fontPath = join(process.cwd(), 'fonts', 'Inter-Regular.ttf')
const fontData = readFileSync(fontPath)
// Define your HTML
const html = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<h1 style="color: white; font-size: 48px; margin: 0;">Hello Satori!</h1>
<p style="color: rgba(255, 255, 255, 0.8); font-size: 24px; margin: 16px 0 0 0;">Generate beautiful images from HTML</p>
</div>
`
// Generate SVG
const svg = await satori(html, {
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: fontData,
weight: 400,
style: 'normal',
},
],
})
console.log(svg)
Advanced Example with Custom Components
import satori from 'satori'
import { join } from 'path'
import { readFileSync } from 'fs'
// Load fonts
const interRegular = readFileSync(join(process.cwd(), 'fonts', 'Inter-Regular.ttf'))
const interBold = readFileSync(join(process.cwd(), 'fonts', 'Inter-Bold.ttf'))
// Create a blog post card
const createBlogCard = (title, excerpt, author, date) => `
<div style="display: flex; flex-direction: column; width: 800px; height: 400px; background: white; border-radius: 16px; padding: 40px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);">
<div style="display: flex; flex-direction: column; flex: 1;">
<h1 style="font-size: 32px; font-weight: bold; color: #1a1a1a; margin: 0 0 16px 0; line-height: 1.2;">
${title}
</h1>
<p style="font-size: 18px; color: #666; margin: 0; line-height: 1.5; flex: 1;">
${excerpt}
</p>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 24px; padding-top: 24px; border-top: 1px solid #eee;">
<div style="display: flex; align-items: center;">
<div style="width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); margin-right: 12px;"></div>
<span style="font-size: 16px; color: #1a1a1a; font-weight: 500;">${author}</span>
</div>
<span style="font-size: 14px; color: #999;">${date}</span>
</div>
</div>
`
// Generate the card
const svg = await satori(createBlogCard(
'Building Modern Web Applications',
'Learn how to build scalable and maintainable web applications using modern technologies and best practices.',
'John Doe',
'January 15, 2024'
), {
width: 800,
height: 400,
fonts: [
{
name: 'Inter',
data: interRegular,
weight: 400,
style: 'normal',
},
{
name: 'Inter',
data: interBold,
weight: 700,
style: 'normal',
},
],
})
Font Configuration
Loading Custom Fonts
import { readFileSync } from 'fs'
import { join } from 'path'
// Load multiple font weights
const fonts = [
{
name: 'Inter',
data: readFileSync(join(process.cwd(), 'fonts', 'Inter-Regular.ttf')),
weight: 400,
style: 'normal',
},
{
name: 'Inter',
data: readFileSync(join(process.cwd(), 'fonts', 'Inter-Bold.ttf')),
weight: 700,
style: 'normal',
},
{
name: 'Inter',
data: readFileSync(join(process.cwd(), 'fonts', 'Inter-Italic.ttf')),
weight: 400,
style: 'italic',
},
]
const svg = await satori(html, {
width: 1200,
height: 630,
fonts,
})
Using Google Fonts
// Fetch Google Fonts
const fetchGoogleFont = async (family, weight = 400) => {
const url = `https://fonts.googleapis.com/css2?family=${family}:wght@${weight}&display=swap`
const response = await fetch(url)
const css = await response.text()
// Extract font URL from CSS
const fontUrl = css.match(/src: url\((.+)\)/)?.[1]
if (!fontUrl) throw new Error('Font URL not found')
const fontResponse = await fetch(fontUrl)
return await fontResponse.arrayBuffer()
}
const interFont = await fetchGoogleFont('Inter', 400)
const fonts = [
{
name: 'Inter',
data: interFont,
weight: 400,
style: 'normal',
},
]
CSS Support
Supported Properties
Satori supports most common CSS properties:
const html = `
<div style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
">
<h1 style="
color: white;
font-size: 48px;
font-weight: bold;
margin: 0 0 16px 0;
text-align: center;
line-height: 1.2;
">Welcome to Satori</h1>
<p style="
color: rgba(255, 255, 255, 0.8);
font-size: 24px;
margin: 0;
text-align: center;
line-height: 1.5;
">Generate beautiful images from HTML and CSS</p>
</div>
`
Flexbox Layout
const html = `
<div style="display: flex; width: 100%; height: 100%;">
<div style="flex: 1; background: #f0f0f0; padding: 20px;">
<h2>Left Panel</h2>
<p>This is the left side content.</p>
</div>
<div style="flex: 2; background: #e0e0e0; padding: 20px;">
<h2>Main Content</h2>
<p>This is the main content area.</p>
</div>
<div style="flex: 1; background: #d0d0d0; padding: 20px;">
<h2>Right Panel</h2>
<p>This is the right side content.</p>
</div>
</div>
`
Integration Examples
Next.js API Route
// pages/api/og-image.js
import satori from 'satori'
import { readFileSync } from 'fs'
import { join } from 'path'
export default async function handler(req, res) {
const { title, description } = req.query
const fontPath = join(process.cwd(), 'fonts', 'Inter-Regular.ttf')
const fontData = readFileSync(fontPath)
const html = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<h1 style="color: white; font-size: 48px; margin: 0; text-align: center;">${title}</h1>
<p style="color: rgba(255, 255, 255, 0.8); font-size: 24px; margin: 16px 0 0 0; text-align: center;">${description}</p>
</div>
`
const svg = await satori(html, {
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: fontData,
weight: 400,
style: 'normal',
},
],
})
res.setHeader('Content-Type', 'image/svg+xml')
res.send(svg)
}
Vercel Edge Function
// api/og-image.js
import satori from 'satori'
import { readFileSync } from 'fs'
import { join } from 'path'
export default async function handler(req) {
const { searchParams } = new URL(req.url)
const title = searchParams.get('title') || 'Default Title'
const fontPath = join(process.cwd(), 'fonts', 'Inter-Regular.ttf')
const fontData = readFileSync(fontPath)
const html = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<h1 style="color: white; font-size: 48px; margin: 0; text-align: center;">${title}</h1>
</div>
`
const svg = await satori(html, {
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: fontData,
weight: 400,
style: 'normal',
},
],
})
return new Response(svg, {
headers: {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public, max-age=31536000, immutable',
},
})
}
Best Practices
1. Font Optimization
// Preload fonts for better performance
const fonts = [
{
name: 'Inter',
data: await fetchGoogleFont('Inter', 400),
weight: 400,
style: 'normal',
},
{
name: 'Inter',
data: await fetchGoogleFont('Inter', 700),
weight: 700,
style: 'normal',
},
]
// Cache fonts for reuse
const fontCache = new Map()
const getFont = async (name, weight) => {
const key = `${name}-${weight}`
if (!fontCache.has(key)) {
fontCache.set(key, await fetchGoogleFont(name, weight))
}
return fontCache.get(key)
}
2. Responsive Design
// Create responsive layouts
const createResponsiveCard = (width, height) => {
const fontSize = Math.min(width, height) * 0.1
const padding = Math.min(width, height) * 0.05
return `
<div style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: ${padding}px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
">
<h1 style="
color: white;
font-size: ${fontSize}px;
margin: 0;
text-align: center;
">Responsive Design</h1>
</div>
`
}
3. Error Handling
const generateImage = async (html, options) => {
try {
const svg = await satori(html, options)
return svg
} catch (error) {
console.error('Failed to generate image:', error)
// Fallback to a simple error image
const fallbackHtml = `
<div style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background: #f0f0f0;">
<p style="color: #666; font-size: 16px;">Failed to generate image</p>
</div>
`
return await satori(fallbackHtml, options)
}
}
Performance Tips
1. Font Loading
- Preload and cache fonts
- Use system fonts when possible
- Minimize font file sizes
2. HTML Optimization
- Keep HTML structure simple
- Avoid complex CSS animations
- Use efficient CSS selectors
3. Caching
- Cache generated SVGs
- Use appropriate cache headers
- Implement cache invalidation
Troubleshooting
Common Issues
-
Font not loading
- Ensure font file path is correct
- Check font file format (TTF/OTF)
- Verify font weight and style
-
Layout issues
- Check CSS property support
- Verify flexbox/grid syntax
- Test with simpler layouts first
-
Performance problems
- Optimize font loading
- Simplify HTML structure
- Use caching strategies