Diseño SVG: Satori + SVGO
Verificado 2026-10: satori 0.63, @resvg/resvg-js 2.6, svgo 4.1.
Satori — marcado → SVG
npm i satori @resvg/resvg-js
import satori from 'satori'
import { Resvg } from '@resvg/resvg-js'
import { readFile, writeFile } from 'node:fs/promises'
const font = await readFile('./Inter-Bold.ttf') // TTF, OTF, WOFF o WOFF2
const svg = await satori(
<div style={{ display: 'flex', width: '100%', height: '100%', alignItems: 'center',
justifyContent: 'center', background: '#0b1020', color: 'white', fontSize: 96 }}>
Đủ 20/20
</div>,
{ width: 1920, height: 1080, fonts: [{ name: 'Inter', data: font, weight: 700, style: 'normal' }] },
)
const png = new Resvg(svg, { fitTo: { mode: 'width', value: 1920 } }).render().asPng()
await writeFile('title.png', png)
- Sin JSX/React, pasa objetos planos:
{ type: 'div', props: { style: {...}, children: '...' } }. - Las fuentes son obligatorias cuando hay texto; el vietnamita necesita una fuente que tenga los glifos (Inter, Be Vietnam Pro, Noto Sans).
- Diseño: Flexbox, Grid (sin subgrid/masonry) y block. Es un subconjunto de CSS — sin JS, sin animaciones, sin hojas de estilo externas; estilo en línea.
- Imágenes:
<img src>con una URL absoluta o data URI, máswidth/heightexplícito. - El texto se convierte en
<path>por defecto (portátil, no seleccionable);embedFont: falsemantiene<text>. - Emoji:
graphemeImagesoloadAdditionalAssetpara proporcionar imágenes/fuentes para segmentos faltantes. - Alternativa PNG:
sharp(Buffer.from(svg)).png().
SVGO v4 — optimizar
npx svgo in.svg -o out.svg
npx svgo -f ./icons -o ./icons-min
// svgo.config.mjs
export default {
multipass: true,
plugins: [
{ name: 'preset-default', params: { overrides: {
// mantener estos cuando el SVG será animado o estilizado por id/clase:
cleanupIds: false, // renombraría/eliminaría ids a los que apunta tu código
collapseGroups: false, // aplanaría <g> que animas como unidad
mergePaths: false, // fusionaría paths que animas por separado
convertShapeToPath: false, // convertiría <circle>/<rect> en <path> (sin r, cx, width)
} } },
'prefixIds', // ids únicos cuando varios SVGs están incrustados en una página
],
}
- Cambios v4:
removeViewBoxyremoveTitleya no están en preset-default (agrégalos por nombre si se desea; conserva viewBox para escalado).removeScriptElement→removeScripts. Solo exportaciones nombradas:import { optimize } from 'svgo'; la compilación para navegador essvgo/browser. - API Node:
const { data } = optimize(svgString, { multipass: true, plugins: [...] }). - Para íconos estáticos, el
preset-defaultsimple está bien; las anulaciones anteriores solo importan para SVGs animados o con scripts. - Después de optimizar un SVG para animación, verifica que cada selector que usa el código de animación todavía exista en la salida.