x-satori
Version:
use Vue or Astro file to generate SVG image using Satori
508 lines (400 loc) • 15.8 kB
Markdown
<h1 align="center">x-satori</h1>
<p align="center">
<a href="https://www.npmjs.com/package/x-satori">
<img alt="version" src="https://img.shields.io/npm/v/x-satori?color=212121&label=">
</a><br><br>
Use <b>Vue and Astro file to generate SVG image</b> by <a href="https://github.com/vercel/satori">Satori</a>.<br>
The image can be generated by running ESM script or CLI.
</p>
<br>
<table>
<tr>
<td align="center" width="50%">
<br>
<p><b><a href="#-vue">Vue</a></b></p>
<a href="https://stackblitz.com/edit/x-satori?file=package.json">
<img alt="Open in StackBlitz" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg">
</a>
</td>
<td align="center" width="50%">
<br>
<p><b><a href="#-astro">Astro</a></b></p>
<a href="https://stackblitz.com/edit/x-satori-astro?file=package.json">
<img alt="Open in StackBlitz" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg">
</a>
</td>
</tr>
<tr>
<td align="center" width="50%">
<a href="https://youtu.be/8HkJg1a_Zew">
<img alt="youtube guide demo" src="./demo-vue.gif">
</a>
<br>
<br>
<a href="https://x.com/zhengqbbb/status/1637849646075908096">
<img alt="View on Twitter post" src="https://cdn.jsdelivr.net/gh/Zhengqbbb/Zhengqbbb@v1.1.1/icons/view-twitter-post.svg">
</a>
</td>
<td align="center" width="50%">
<a href="https://youtu.be/LG8PDLZHEts">
<img alt="youtube guide demo" src="./demo-astro.gif">
</a>
<br>
<br>
<a href="https://x.com/zhengqbbb/status/1817191553217241130">
<img alt="View on Twitter post" src="https://cdn.jsdelivr.net/gh/Zhengqbbb/Zhengqbbb@v1.1.1/icons/view-twitter-post.svg">
</a>
</td>
</tr>
</table>
---
> Local running example demo
```sh
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
```sh
npm install -D x-satori
```
### ⭐ Vue
<details>
<summary>Using Vitepress <a href="https://vitepress.dev/reference/site-config#buildend">buildEnd hook</a></summary><br>
- **Example**: [examples/vue-vitepress](./examples/vue-vitepress/) ⭐⭐⭐
</details>
<details>
<summary>Using CLI</summary><br>
**Example**: [playground/vue](./playground/vue/)
- Dependency: **Vue | Vite**
```sh
$ 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
```mjs
import { defineSatoriConfig } from 'x-satori/vue'
export default defineSatoriConfig({
// ... Satori options
props: {
// ...Vue SFC props options
// title: "Hello world"
},
})
```
#### Vue template file
- **Only the template syntax is used**, and props are only used for hint completion
- [→ Satori supports common CSS features](https://github.com/vercel/satori#css)
- [→ Tailwindcss documentation](https://tailwindcss.com/docs/customizing-colors)
```html
<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>
```
</details>
<details>
<summary>Using ESM script</summary><br>
- Dependency: **Vue**
```mjs
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()
```
- **Example**: [examples/vue-run-esm-script](./examples/vue-run-esm-script/)
```sh
npm run gen:svg
npm run gen:png
```
</details>
### ⭐ Astro
<details>
<summary>Using Astro <a href="https://docs.astro.build/en/guides/endpoints/">file-endpoints</a></summary><br>
- **Example**: [examples/astro-file-endpoint](./examples/astro-file-endpoint/) ⭐⭐⭐
- **Example**: [Repo - Zhengqbbb/qbb.sh](https://github.com/Zhengqbbb/qbb.sh/blob/astro/src/pages/og/%5Bslug%5D.png.ts)
---
#### 1. Install Dependencies
```sh
npm install -D x-satori @resvg/resvg-js # Convert SVG to PNG
```
#### 2. Create Astro [file-endpoints](https://docs.astro.build/en/guides/endpoints/)
> If target is generate `dist/og/*.png`.<br>
> So that touch a file `src/pages/og/[slug].png.ts`
```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' },
},
)
```
</details>
<details>
<summary>Using ESM script</summary><br>
- Dependency: **Astro**
```mjs
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()
```
- **Example**: [examples/astro-run-esm-script](./examples/astro-run-esm-script/) ⭐⭐⭐
```sh
npm run gen:svg
npm run gen:png
```
- **Example**: [Repo - Zhengqbbb/qbb.sh@v2.1.1/.x-cmd/og/main.ts](https://github.com/Zhengqbbb/qbb.sh/blob/v2.1.1/.x-cmd/og/main.ts)
</details>
<details>
<summary>Using CLI</summary><br>
- Dependency: **Astro** | **Vite** (for dev mode)
```sh
$ 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
```mjs
import { defineSatoriConfig } from 'x-satori/astro'
export default defineSatoriConfig({
// ... Satori options
props: {
// ...astro file props options
// title: "Hello world"
},
})
```
#### Astro template file
- **Only the template syntax is used**, and props are only used for hint completion
- [→ Satori supports common CSS features](https://github.com/vercel/satori#css)
- [→ Tailwindcss documentation](https://tailwindcss.com/docs/customizing-colors)
```astro
---
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>
```
- **Example**: [playground/astro](./playground/astro/) ⭐⭐⭐
- **Example (Advanced)** - Using `Shell Scripts` to batch image generation with `resvg-cli`:<br>
[Repo - Zhengqbbb/qbb.sh/.x-cmd/og](https://github.com/Zhengqbbb/qbb.sh/blob/6fe03b051c1468417682da6e329be0e0b77c6468/.x-cmd/og#L37-L45)
</details>
### 🎼 Command-line Advanced Usage
<details>
<summary>pipeline generate <b>png</b> with <code>resvg-cli</code></summary><br>
> TIP: You can install it globally or use `bunx` for replacement startup
```sh
npx x-satori --config "./satori.ts" --template "./Template.vue" --props '{"title": "Hello World"}' | \
npx resvg-cli - image.png
```
</details>
<details>
<summary>pipeline generate <b>webp</b> or more edit with <code>resvg-cli</code> and <code>imagemagick</code> </summary><br>
> TIP: You can install it globally or use `bunx` for replacement startup
```sh
npx x-satori --config "./satori.ts" --template "./Template.vue" --props '{"title": "Hello World"}' | \
npx resvg-cli - | \
magick - webp:image.webp
```
</details>
## How it works
1. ▲ [Satori](https://github.com/vercel/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"](https://github.com/vercel/satori#use-without-jsx).
3. Thanks an library [natemoo-re/satori-html](https://github.com/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](https://github.com/Zhengqbbb/qbb.sh/blob/790c47026cb1baac34dee8642150ec1729fb0f39/package.json#L18), 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. <br>
<table>
<tr>
<td align="center" width="50%">
<img alt="demo-img-1"src="https://user-images.githubusercontent.com/40693636/226387222-e2de688d-bbb6-41a2-9454-d10d8fd7784d.png">
</td>
<td align="center" width="50%">
<img alt="demo-img-2"src="https://cdn.jsdelivr.net/gh/Zhengqbbb/qbb.sh@v2.1.1/public/og/posts.png">
</td>
</tr>
<tr>
<td align="center" width="50%">
<img alt="demo-img-3"src="https://user-images.githubusercontent.com/40693636/226387925-57b58c6a-6677-44d4-a7a0-6939193704b3.png">
</td>
<td align="center" width="50%">
<img alt="demo-img-4"src="https://cdn.jsdelivr.net/gh/Zhengqbbb/qbb.sh@v2.1.1/public/og/2022-12-17-new-homepage.png">
</td>
</tr>
<tr>
<td align="center" width="50%">
<b>Vue</b>
<br>
Vitepress buildEnd example:
<br>
<a href="./examples/vue-vitepress/">examples/vue-vitepress/</a>
<br>
ESM script code example:
<br>
<a href="https://github.com/Zhengqbbb/qbb.sh/tree/vitesse/build/node/og/main.mts">qbb.sh@vitesse/node/og/main.mts</a>
</td>
<td align="center" width="50%">
<b>Astro</b>
<br>
File endpoints example:
<br>
<a href="https://satori-astro.vercel.app/blog/">https://satori-astro.vercel.app</a>
<br>
ESM script code example:
<br>
<a href="https://github.com/Zhengqbbb/qbb.sh/blob/v2.1.1/.x-cmd/og/main.ts">qbb.sh@astro/.x-cmd/og/main.ts</a>
</td>
</tr>
</table>
## Related Links
- [nuxt-modules/og-image](https://github.com/nuxt-modules/og-image) - Nuxt or want to use edge Fn
- [Vercel / Open Graph (OG) Image Generation](https://vercel.com/docs/functions/og-image-generation)
## FAQ
<details>
<summary>CJS support ?</summary><br>
**Not supported**, waiting for upstream library [natemoo-re/ultrahtml](https://github.com/natemoo-re/ultrahtml/tree/main)
</details>
## Contributing
> I did it step by step according to the documentation of Astro, Vue and Vite, if you are interested, PR welcome 🤗
```sh
pnpm install
pnpm dev # dev mode
pnpm x --help # start up the CLI and development
```
## LICENSE
MIT
Copyright (c) 2023-2024 [Q.Ben Zheng](https://github.com/Zhengqbbb)
<p align="center">
<table>
<tbody>
<td align="center">
<br>
I just try my best to make thing well.<br>
Could you give a star ⭐ to encourage me 🤗
<br>
If possible, can to be my <a href="https://github.com/sponsors/Zhengqbbb">💖 Sponsor 💖</a> to support my work
<img width="800" height="0" />
</td>
</tbody>
</table>
</p>