Skip to content

🌠 Using Astro or Vue file to generate cover, card, Open Graph ... image SVG, powered by Satori and Vite

License

Notifications You must be signed in to change notification settings

Zhengqbbb/x-satori

Repository files navigation

x-satori

version

Use Vue and Astro file to generate SVG image by Satori.
The image can be generated by running ESM script or CLI.



Vue

Open in StackBlitz

Astro

Open in StackBlitz
youtube guide demo

View on Twitter post
youtube guide demo

View on Twitter post

Local running example demo

npx degit Zhengqbbb/x-satori/playground/vue   <file_name>     # Vue
npx degit Zhengqbbb/x-satori/playground/astro <file_name>     # Astro

cd <file_name>
pnpm install

# Development Model
pnpm dev:og

# [Generate] SVG
pnpm gen:svg

# [Generate] PNG
pnpm gen:png

Usage

npm install -D x-satori

⭐ Vue

Using Vitepress buildEnd hook
Using CLI

Example: playground/vue

  • Dependency: Vue | Vite
$ npx x-satori --help

SYNOPSIS:
    x-satori --template <template_file_path> --config <satori_config_path> [--props <JSON>]
    x-satori --template <template_file_path> --config <satori_config_path> [--props <JSON>] --output <svg_path>
    x-satori --template <template_file_path> --config <satori_config_path> [--props <JSON>] --dev [--host --port <num>]

OPTIONS:
    -d|--dev                   Turn on Dev mode
       --host                  Expose  host in Dev mode
       --port     <num>        Specify port in Dev mode
    -t|--template <path>       The Vue or Astro template file path
    -c|--config   <path>       The export satori configure file path
    -o|--output   <path>       Target output SVG path
    --props       <JSON_str>   Overwrite and use props in config

EXAMPLES:
    x-satori --config "./satori.ts" --template "./Template.vue" --dev --host
    x-satori --config "./satori.ts" --template "./Template.vue"
    x-satori --config "./satori.js" --template "./Template.vue" --props '{"title": "Hello World"}'
    x-satori --config "./satori.js" --template "./Template.vue" -o image.svg

Configure

  • Extends Satori options and add Vue file props option
import { defineSatoriConfig } from 'x-satori/vue'

export default defineSatoriConfig({
    // ... Satori options
    props: {
        // ...Vue SFC props options
        // title: "Hello world"
    },
})

Vue template file

<script setup lang="ts">
const props = defineProps({
  title: String,
})
</script>
<template>
  <div class="w-full h-full flex text-white bg-blue-500 items-center justify-center">
    <h1 :style="{ fontSize: '70px' }">
      {{ title }} πŸ‘‹
    </h1>
  </div>
</template>
Using ESM script
  • Dependency: Vue
import { defineSatoriConfig, satoriVue } from 'x-satori/vue'

function main() {
    const _DIRNAME = typeof __dirname !== 'undefined'
        ? __dirname
        : dirname(fileURLToPath(import.meta.url))
    const _OUTPUT = resolve(_DIRNAME, './image/og.png')

    const templateStr = await readFile(resolve(_DIRNAME, './Template.vue'), 'utf8')
    const opt = defineSatoriConfig({
    // ... Satori options
        props: {
        // ...Vue SFC props options
        // title: "Hello world"
        },
    })
    const strSVG = await satoriVue(opt, templateStr)
    console.log(strSVG)
}
main()

⭐ Astro

Using Astro file-endpoints

1. Install Dependencies

npm install -D x-satori @resvg/resvg-js # Convert SVG to PNG

2. Create Astro file-endpoints

If target is generate dist/og/*.png.
So that touch a file src/pages/og/[slug].png.ts

import { readFile } from 'node:fs/promises'
import { type SatoriOptions, satoriAstro } from 'x-satori/astro'
import { Resvg } from '@resvg/resvg-js'
import type { APIRoute } from 'astro'
import { type CollectionEntry, getCollection } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');

    return posts
        .map(post => ({
            params: { slug: post.slug },
            props: { ...post },
        }));
}

async function getPostImageBuffer(props) {
        const template = await readFile(/** .astro template file */, 'utf-8')
        const config: SatoriOptions = {
            //... satori options,
            props: {
                //...astro template file props
                ...props.data,
            }
        }
        const svg = await satoriAstro(config, template)
        const resvg = new Resvg(svg)
        const pngData = resvg.render()
        return pngData.asPng()
}

export const GET: APIRoute = async ({ props }) =>
    new Response(
        await getPostImageBuffer(props as CollectionEntry<'blog'>),
        {
            headers: { 'Content-Type': 'image/png' },
        },
    )
Using ESM script
  • Dependency: Astro
import { defineSatoriConfig, satoriAstro } from 'x-satori/astro'

function main() {
    const _DIRNAME = typeof __dirname !== 'undefined'
        ? __dirname
        : dirname(fileURLToPath(import.meta.url))
    const _OUTPUT = resolve(_DIRNAME, './image/og.png')

    const templateStr = await readFile(resolve(_DIRNAME, './Template.vue'), 'utf8')
    const opt = defineSatoriConfig({
    // ... Satori options
        props: {
        // ...Vue SFC props options
        // title: "Hello world"
        },
    })
    const strSVG = await satoriAstro(opt, templateStr)
    console.log(strSVG)
}
main()
Using CLI
  • Dependency: Astro | Vite (for dev mode)
$ npx x-satori --help

SYNOPSIS:
    x-satori --template <template_file_path> --config <satori_config_path> [--props <JSON>]
    x-satori --template <template_file_path> --config <satori_config_path> [--props <JSON>] --output <svg_path>
    x-satori --template <template_file_path> --config <satori_config_path> [--props <JSON>] --dev [--host --port <num>]

OPTIONS:
    -d|--dev                   Turn on Dev mode
       --host                  Expose  host in Dev mode
       --port     <num>        Specify port in Dev mode
    -t|--template <path>       The Vue or Astro template file path
    -c|--config   <path>       The export satori configure file path
    -o|--output   <path>       Target output SVG path
    --props       <JSON_str>   Overwrite and use props in config

EXAMPLES:
    x-satori --config "./satori.ts" --template "./Template.astro" --dev --host
    x-satori --config "./satori.ts" --template "./Template.astro"
    x-satori --config "./satori.js" --template "./Template.astro" --props '{"title": "Hello World"}'
    x-satori --config "./satori.js" --template "./Template.astro" -o image.svg

Configure

  • Extends Satori options and add Vue file props option
import { defineSatoriConfig } from 'x-satori/astro'

export default defineSatoriConfig({
    // ... Satori options
    props: {
        // ...astro file props options
        // title: "Hello world"
    },
})

Astro template file

---
interface Props {
    title: string
};

const { title = Hello world } = Astro.props;
---
<div class="w-full h-full text-1.4rem text-white flex flex-col items-center justify-between">
    <h2 >
        {title}
    </h2>
</div>

🎼 Command-line Advanced Usage

pipeline generate png with resvg-cli

TIP: You can install it globally or use bunx for replacement startup

npx x-satori --config "./satori.ts" --template "./Template.vue" --props '{"title": "Hello World"}' | \
    npx resvg-cli - image.png
pipeline generate webp or more edit with resvg-cli and imagemagick

TIP: You can install it globally or use bunx for replacement startup

npx x-satori --config "./satori.ts" --template "./Template.vue" --props '{"title": "Hello World"}' | \
    npx resvg-cli - | \
    magick - webp:image.webp

How it works

  1. β–² Satori is an amazing library for generating SVG strings from pure HTML and CSS.
  2. Unfortunately, it is built on top of React's JSX and expects "React-elements-like objects".
  3. Thanks an library natemoo-re/satori-html can to generate the necessary VDOM object from a string of HTML.
  4. So the key is to convert the Vue SFC file to an HTML string, and here I used transform so that I could generate it via script (Only the template syntax is used)
    • @vue/compiler-sfc: to parse Vue SFC file
    • vue - createSSRApp and vue/server-renderer: transform HTML string
  5. Astro: a similar method:
    • @astrojs/compiler: to transform .astro to ts
    • AstroContainer: renderToString to obtain HTML string

Why developed

My Weekend Pilot Project

  1. This processing logic, initially used in my Vite-SSG person website qbb.sh, I prefer to run the script to generate e.g tsx gen-og.mts at my building time rather than the edge Fn
  2. And personally, I think Vue SFC File would be better in expressing this SVG structure, but I only use the template syntax and props, and the css would use tailwindcss.
  3. I did a experiment this weekend, using Vite HRM to improve DX, and developed a CLI so that I could run command and generated the SVG directly.

I'm happy that I finally finished this series of experiments and results this weekend.

demo-img-1 demo-img-2
demo-img-3 demo-img-4
Vue
Vitepress buildEnd example:
examples/vue-vitepress/
ESM script code example:
qbb.sh@vitesse/node/og/main.mts
Astro
File endpoints example:
https://satori-astro.vercel.app
ESM script code example:
qbb.sh@astro/.x-cmd/og/main.ts

Related Links

FAQ

CJS support ?

Not supported, waiting for upstream library natemoo-re/ultrahtml

Contributing

I did it step by step according to the documentation of Astro, Vue and Vite, if you are interested, PR welcome πŸ€—

pnpm install
pnpm dev        # dev mode
pnpm x --help   # start up the CLI and development

LICENSE

MIT Copyright (c) 2023-2024 Q.Ben Zheng


I just try my best to make thing well.
Could you give a star ⭐ to encourage me πŸ€—
If possible, can to be my πŸ’– Sponsor πŸ’– to support my work