From 9ed28839db7d62f69126a8fa6d442a80eef011d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:34:56 +0000 Subject: [PATCH 1/5] Initial plan From 91f31ca25fb2d9c806f1f799f9076929bad013ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:43:42 +0000 Subject: [PATCH 2/5] Add plugin-og-image package with core functionality Co-authored-by: SoonIter <79413249+SoonIter@users.noreply.github.com> --- packages/plugin-og-image/LICENSE | 22 ++ packages/plugin-og-image/README.md | 106 ++++++ packages/plugin-og-image/package.json | 51 +++ packages/plugin-og-image/rslib.config.ts | 9 + packages/plugin-og-image/src/generator.ts | 62 ++++ packages/plugin-og-image/src/index.ts | 6 + packages/plugin-og-image/src/plugin.ts | 109 ++++++ packages/plugin-og-image/src/template.ts | 95 +++++ packages/plugin-og-image/src/types.ts | 70 ++++ packages/plugin-og-image/tsconfig.json | 8 + pnpm-lock.yaml | 412 ++++++++++++++++++++++ 11 files changed, 950 insertions(+) create mode 100644 packages/plugin-og-image/LICENSE create mode 100644 packages/plugin-og-image/README.md create mode 100644 packages/plugin-og-image/package.json create mode 100644 packages/plugin-og-image/rslib.config.ts create mode 100644 packages/plugin-og-image/src/generator.ts create mode 100644 packages/plugin-og-image/src/index.ts create mode 100644 packages/plugin-og-image/src/plugin.ts create mode 100644 packages/plugin-og-image/src/template.ts create mode 100644 packages/plugin-og-image/src/types.ts create mode 100644 packages/plugin-og-image/tsconfig.json diff --git a/packages/plugin-og-image/LICENSE b/packages/plugin-og-image/LICENSE new file mode 100644 index 000000000..b6bac59cb --- /dev/null +++ b/packages/plugin-og-image/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 jl917 +Copyright (c) 2025-present Bytedance, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/plugin-og-image/README.md b/packages/plugin-og-image/README.md new file mode 100644 index 000000000..3570ca5bb --- /dev/null +++ b/packages/plugin-og-image/README.md @@ -0,0 +1,106 @@ +# @rspress/plugin-og-image + +A plugin for Rspress to dynamically generate Open Graph (OG) images for each page. + +## Installation + +```bash +npm install @rspress/plugin-og-image +# or +pnpm add @rspress/plugin-og-image +# or +yarn add @rspress/plugin-og-image +``` + +## Usage + +Add the plugin to your `rspress.config.ts`: + +```ts +import { defineConfig } from '@rspress/core'; +import { pluginOgImage } from '@rspress/plugin-og-image'; + +export default defineConfig({ + plugins: [ + pluginOgImage({ + siteUrl: 'https://your-site.com', + }), + ], +}); +``` + +## Options + +### `siteUrl` + +- Type: `string` +- Required: `true` + +The base URL of your site. Used to generate absolute URLs for OG images. + +### `ogImage` + +- Type: `OgImageOptions` +- Required: `false` + +Options for OG image generation. + +#### `ogImage.width` + +- Type: `number` +- Default: `1200` + +Width of the generated OG image in pixels. + +#### `ogImage.height` + +- Type: `number` +- Default: `630` + +Height of the generated OG image in pixels (630px is the recommended size for OG images). + +#### `ogImage.template` + +- Type: `(data: OgImageTemplateData) => string | Promise` +- Required: `false` + +Custom template function to generate the image. Receives page data and should return a React-like JSX structure compatible with [Satori](https://github.com/vercel/satori). + +#### `ogImage.filter` + +- Type: `(pageData: PageIndexInfo) => boolean` +- Default: `() => true` + +Filter function to determine which pages should have OG images generated. By default, all pages get OG images. + +## Frontmatter Options + +You can customize OG images per page using frontmatter: + +```md +--- +title: My Page +description: A description of my page +ogBackgroundColor: '#1a1a1a' +ogTextColor: '#ffffff' +siteName: My Site +--- +``` + +## Generated URLs + +OG images are generated with URLs matching your page routes: + +- Page: `https://your-site.com/guide/getting-started` +- OG Image: `https://your-site.com/og/guide/getting-started.png` + +## How It Works + +1. During the build process, the plugin generates OG images for each page +2. Images are created using [Satori](https://github.com/vercel/satori) (SVG generation) and [Sharp](https://sharp.pixelplumbing.com/) (PNG conversion) +3. OG image meta tags are automatically added to each page's frontmatter +4. Images are saved to the output directory in the `/og` folder + +## License + +MIT diff --git a/packages/plugin-og-image/package.json b/packages/plugin-og-image/package.json new file mode 100644 index 000000000..a95cb98e7 --- /dev/null +++ b/packages/plugin-og-image/package.json @@ -0,0 +1,51 @@ +{ + "name": "@rspress/plugin-og-image", + "version": "2.0.0-rc.1", + "description": "A plugin for rspress to generate dynamic OG (Open Graph) images", + "bugs": "https://github.com/web-infra-dev/rspress/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/web-infra-dev/rspress.git", + "directory": "packages/plugin-og-image" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "static" + ], + "scripts": { + "build": "rslib build", + "dev": "rslib build -w", + "reset": "rimraf ./**/node_modules" + }, + "dependencies": { + "satori": "^0.11.2", + "sharp": "^0.33.5" + }, + "devDependencies": { + "@microsoft/api-extractor": "^7.55.0", + "@rslib/core": "0.17.2", + "@types/node": "^22.8.1", + "rsbuild-plugin-publint": "^0.3.3" + }, + "peerDependencies": { + "@rspress/core": "workspace:^2.0.0-rc.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/plugin-og-image/rslib.config.ts b/packages/plugin-og-image/rslib.config.ts new file mode 100644 index 000000000..3f49351d1 --- /dev/null +++ b/packages/plugin-og-image/rslib.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from '@rslib/core'; +import { pluginPublint } from 'rsbuild-plugin-publint'; + +export default defineConfig({ + plugins: [pluginPublint()], + lib: [ + { bundle: true, syntax: 'es2022', format: 'esm', dts: { bundle: true } }, + ], +}); diff --git a/packages/plugin-og-image/src/generator.ts b/packages/plugin-og-image/src/generator.ts new file mode 100644 index 000000000..d888bdc8c --- /dev/null +++ b/packages/plugin-og-image/src/generator.ts @@ -0,0 +1,62 @@ +import { readFile } from 'node:fs/promises'; +import satori from 'satori'; +import sharp from 'sharp'; +import { defaultTemplate } from './template'; +import type { OgImageOptions, OgImageTemplateData } from './types'; + +let cachedFont: Buffer | null = null; + +/** + * Get font data for Satori + */ +async function getFontData(): Promise { + if (cachedFont) { + return cachedFont; + } + + try { + // Try to use a system font or bundled font + // For now, we'll use a simple approach - in production this might need bundled fonts + const fontPath = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'; + const fontData = await readFile(fontPath); + cachedFont = Buffer.from(fontData); + return cachedFont; + } catch (error) { + // Fallback: return empty buffer and Satori will use default + console.warn('Could not load font, using Satori defaults'); + return Buffer.from([]); + } +} + +/** + * Generate OG image PNG from template data + */ +export async function generateOgImage( + data: OgImageTemplateData, + options: OgImageOptions = {}, +): Promise { + const { width = 1200, height = 630, template = defaultTemplate } = options; + + // Get the template (either custom or default) + const templateResult = + typeof template === 'function' ? await template(data) : template; + + // Convert template to SVG using Satori + const svg = await satori(templateResult, { + width, + height, + fonts: [ + { + name: 'sans-serif', + data: await getFontData(), + weight: 400, + style: 'normal', + }, + ], + }); + + // Convert SVG to PNG using Sharp + const png = await sharp(Buffer.from(svg)).png().toBuffer(); + + return png; +} diff --git a/packages/plugin-og-image/src/index.ts b/packages/plugin-og-image/src/index.ts new file mode 100644 index 000000000..24c3b6335 --- /dev/null +++ b/packages/plugin-og-image/src/index.ts @@ -0,0 +1,6 @@ +export { pluginOgImage } from './plugin'; +export type { + OgImageOptions, + OgImageTemplateData, + PluginOgImageOptions, +} from './types'; diff --git a/packages/plugin-og-image/src/plugin.ts b/packages/plugin-og-image/src/plugin.ts new file mode 100644 index 000000000..efacdb03c --- /dev/null +++ b/packages/plugin-og-image/src/plugin.ts @@ -0,0 +1,109 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname, isAbsolute, join } from 'node:path'; +import type { PageIndexInfo, RspressPlugin } from '@rspress/core'; +import { generateOgImage } from './generator'; +import type { OgImageTemplateData, PluginOgImageOptions } from './types'; + +interface PageOgImageInfo { + routePath: string; + imagePath: string; + imageUrl: string; + templateData: OgImageTemplateData; +} + +export function pluginOgImage(options: PluginOgImageOptions): RspressPlugin { + const { siteUrl, ogImage = {} } = options; + const { filter = () => true } = ogImage; + + // Store page info for image generation + const pageInfoMap = new Map(); + + return { + name: '@rspress/plugin-og-image', + + async extendPageData(pageData: PageIndexInfo, isProd: boolean) { + if (!isProd || !filter(pageData)) { + return; + } + + const { routePath, title, frontmatter } = pageData; + + // Get description from frontmatter or default + const description = frontmatter?.description as string | undefined; + + // Generate OG image path based on route + // Example: /guide/getting-started -> /og/guide/getting-started.png + const cleanPath = routePath.replace(/^\//, '').replace(/\/$/, ''); + const imagePath = cleanPath ? `og/${cleanPath}.png` : 'og/index.png'; + const imageUrl = `${siteUrl.replace(/\/$/, '')}/${imagePath}`; + + // Store template data + const templateData: OgImageTemplateData = { + title: title || 'Untitled', + description, + siteName: frontmatter?.siteName as string | undefined, + logo: frontmatter?.logo as string | undefined, + backgroundColor: frontmatter?.ogBackgroundColor as string | undefined, + textColor: frontmatter?.ogTextColor as string | undefined, + }; + + pageInfoMap.set(routePath, { + routePath, + imagePath, + imageUrl, + templateData, + }); + + // Add OG meta tags to page data + if (!pageData.frontmatter) { + pageData.frontmatter = {}; + } + + // Set OG image meta tags + pageData.frontmatter.ogImage = imageUrl; + }, + + async afterBuild(config, isProd) { + if (!isProd || pageInfoMap.size === 0) { + return; + } + + // Get output directory + const distPathRoot = + typeof config.builderConfig?.output?.distPath === 'string' + ? config.builderConfig?.output?.distPath + : config.builderConfig?.output?.distPath?.root; + const configPath = config.outDir || distPathRoot; + const outputDir = isAbsolute(configPath || '') + ? configPath + : `./${configPath || 'doc_build'}`; + + // Generate all OG images + console.log(`Generating ${pageInfoMap.size} OG images...`); + + for (const pageInfo of pageInfoMap.values()) { + try { + // Generate the image + const imageBuffer = await generateOgImage( + pageInfo.templateData, + ogImage, + ); + + // Write to disk + const fullPath = join(outputDir!, pageInfo.imagePath); + await mkdir(dirname(fullPath), { recursive: true }); + await writeFile(fullPath, imageBuffer); + + console.log(`Generated OG image: ${pageInfo.imagePath}`); + } catch (error) { + console.error( + `Failed to generate OG image for ${pageInfo.routePath}:`, + error, + ); + } + } + + console.log('OG image generation complete!'); + }, + }; +} diff --git a/packages/plugin-og-image/src/template.ts b/packages/plugin-og-image/src/template.ts new file mode 100644 index 000000000..c08865306 --- /dev/null +++ b/packages/plugin-og-image/src/template.ts @@ -0,0 +1,95 @@ +import type { OgImageTemplateData } from './types'; + +/** + * Default OG image template + * Returns a React-like JSX structure that Satori can render + */ +export function defaultTemplate(data: OgImageTemplateData): any { + const { + title, + description, + siteName = 'Rspress', + backgroundColor = '#1a1a1a', + textColor = '#ffffff', + } = data; + + return { + type: 'div', + props: { + style: { + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + backgroundColor, + padding: '80px', + fontFamily: 'system-ui, -apple-system, sans-serif', + position: 'relative', + }, + children: [ + // Main content + { + type: 'div', + props: { + style: { + display: 'flex', + flexDirection: 'column', + flex: 1, + justifyContent: 'center', + }, + children: [ + // Title + { + type: 'div', + props: { + style: { + fontSize: '72px', + fontWeight: 'bold', + color: textColor, + lineHeight: 1.2, + marginBottom: '24px', + maxWidth: '900px', + display: 'flex', + flexWrap: 'wrap', + }, + children: title, + }, + }, + // Description + description + ? { + type: 'div', + props: { + style: { + fontSize: '32px', + color: textColor, + opacity: 0.8, + lineHeight: 1.4, + maxWidth: '900px', + }, + children: description, + }, + } + : null, + ].filter(Boolean), + }, + }, + // Footer with site name + { + type: 'div', + props: { + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + fontSize: '28px', + color: textColor, + opacity: 0.7, + }, + children: siteName, + }, + }, + ], + }, + }; +} diff --git a/packages/plugin-og-image/src/types.ts b/packages/plugin-og-image/src/types.ts new file mode 100644 index 000000000..98e46b3eb --- /dev/null +++ b/packages/plugin-og-image/src/types.ts @@ -0,0 +1,70 @@ +import type { PageIndexInfo } from '@rspress/core'; + +/** + * OG image template data + */ +export interface OgImageTemplateData { + /** + * Page title + */ + title: string; + /** + * Page description + */ + description?: string; + /** + * Site name + */ + siteName?: string; + /** + * Site logo URL or path + */ + logo?: string; + /** + * Custom background color + */ + backgroundColor?: string; + /** + * Custom text color + */ + textColor?: string; +} + +/** + * OG image generation options + */ +export interface OgImageOptions { + /** + * Width of the generated image in pixels + * @default 1200 + */ + width?: number; + /** + * Height of the generated image in pixels + * @default 630 + */ + height?: number; + /** + * Custom template function to generate the SVG/JSX template + */ + template?: (data: OgImageTemplateData) => string | Promise; + /** + * Filter function to determine which pages should have OG images generated + * @default () => true (all pages) + */ + filter?: (pageData: PageIndexInfo) => boolean; +} + +/** + * Plugin options for pluginOgImage + */ +export interface PluginOgImageOptions { + /** + * Site URL (required for generating absolute URLs) + */ + siteUrl: string; + /** + * OG image generation options + */ + ogImage?: OgImageOptions; +} diff --git a/packages/plugin-og-image/tsconfig.json b/packages/plugin-og-image/tsconfig.json new file mode 100644 index 000000000..6435d4337 --- /dev/null +++ b/packages/plugin-og-image/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e1a584d4..b32d0552d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1299,6 +1299,31 @@ importers: specifier: ^6.0.3 version: 6.0.3 + packages/plugin-og-image: + dependencies: + '@rspress/core': + specifier: workspace:^2.0.0-rc.1 + version: link:../core + satori: + specifier: ^0.11.2 + version: 0.11.3 + sharp: + specifier: ^0.33.5 + version: 0.33.5 + devDependencies: + '@microsoft/api-extractor': + specifier: ^7.55.0 + version: 7.55.0(@types/node@22.10.2) + '@rslib/core': + specifier: 0.17.2 + version: 0.17.2(@microsoft/api-extractor@7.55.0(@types/node@22.10.2))(typescript@5.8.2) + '@types/node': + specifier: ^22.8.1 + version: 22.10.2 + rsbuild-plugin-publint: + specifier: ^0.3.3 + version: 0.3.3(@rsbuild/core@1.6.6) + packages/plugin-playground: dependencies: '@mdx-js/mdx': @@ -2600,6 +2625,123 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/external-editor@1.0.1': resolution: {integrity: sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==} engines: {node: '>=18'} @@ -3370,6 +3512,11 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@shuding/opentype.js@1.4.0-beta.0': + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + '@sinclair/typebox@0.34.41': resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} @@ -3919,6 +4066,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -4003,6 +4154,9 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + caniuse-lite@1.0.30001751: resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} @@ -4103,6 +4257,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -4231,9 +4392,26 @@ packages: engines: {node: '>=20'} hasBin: true + css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + + css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + css-gradient-parser@0.0.16: + resolution: {integrity: sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==} + engines: {node: '>=16'} + css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -4322,6 +4500,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@4.0.1: resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4501,6 +4683,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -4617,6 +4802,9 @@ packages: resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} engines: {node: '>=0.4.0'} + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -4920,6 +5108,10 @@ packages: resolution: {integrity: sha512-kHDUzStHy3OIysI4sxJsfQ7KNrRKEudT7YgWRwFMFVKN7/0CINF/l3prqN8eLgo7k18Vvfz+xRcEhR/mUVlCaQ==} hasBin: true + hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + highlight.js@11.8.0: resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==} engines: {node: '>=12.0.0'} @@ -5033,6 +5225,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -5226,6 +5421,9 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -5865,6 +6063,9 @@ packages: package-manager-detector@1.2.0: resolution: {integrity: sha512-PutJepsOtsqVfUsxCzgTTpyXmiAgvKptIgY4th5eq5UXXFhj5PxfQ9hnGkypMeovpAvVshFRItoFHYO18TCOqA==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -5873,6 +6074,9 @@ packages: resolution: {integrity: sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==} engines: {node: '>=8'} + parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} @@ -6522,6 +6726,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + satori@0.11.3: + resolution: {integrity: sha512-Wg7sls0iYAEETzi9YYcY16QVIqXjZT06XjkwondC5CGhw1mhmgKBCub8cCmkxdl/naXXQD+m29CFgn8pwtYCnA==} + engines: {node: '>=16'} + sax@1.3.0: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} @@ -6576,6 +6784,10 @@ packages: resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==} engines: {node: '>=11.0'} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6600,6 +6812,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -6718,6 +6933,9 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -6871,6 +7089,9 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -7004,6 +7225,9 @@ packages: unhead@2.0.19: resolution: {integrity: sha512-gEEjkV11Aj+rBnY6wnRfsFtF2RxKOLaPN4i+Gx3UhBxnszvV6ApSNZbGk7WKyy/lErQ6ekPN63qdFL7sa1leow==} + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -7326,6 +7550,9 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + yoga-wasm-web@0.3.3: + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + zod@4.1.11: resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} @@ -8261,6 +8488,81 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@inquirer/external-editor@1.0.1(@types/node@22.10.2)': dependencies: chardet: 2.1.0 @@ -9098,6 +9400,11 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@shuding/opentype.js@1.4.0-beta.0': + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + '@sinclair/typebox@0.34.41': {} '@sindresorhus/merge-streams@2.3.0': {} @@ -9711,6 +10018,8 @@ snapshots: balanced-match@1.0.2: {} + base64-js@0.0.8: {} + base64-js@1.5.1: {} base64id@2.0.0: {} @@ -9788,6 +10097,8 @@ snapshots: camelcase@6.3.0: {} + camelize@1.0.1: {} + caniuse-lite@1.0.30001751: {} ccount@2.0.1: {} @@ -9889,6 +10200,16 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + colorette@2.0.20: {} colorjs.io@0.5.2: {} @@ -10050,6 +10371,14 @@ snapshots: semver: 7.7.3 tinyglobby: 0.2.15 + css-background-parser@0.1.0: {} + + css-box-shadow@1.0.0-3: {} + + css-color-keywords@1.0.0: {} + + css-gradient-parser@0.0.16: {} + css-select@5.1.0: dependencies: boolbase: 1.0.0 @@ -10058,6 +10387,12 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + css-tree@2.2.1: dependencies: mdn-data: 2.0.28 @@ -10118,6 +10453,8 @@ snapshots: detect-libc@1.0.3: optional: true + detect-libc@2.1.2: {} + detect-newline@4.0.1: {} devlop@1.1.0: @@ -10363,6 +10700,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@5.0.0: {} @@ -10473,6 +10812,8 @@ snapshots: dependencies: xml-js: 1.6.11 + fflate@0.7.4: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -10908,6 +11249,8 @@ snapshots: heading-case@1.0.3: {} + hex-rgb@4.3.0: {} + highlight.js@11.8.0: {} hookable@5.5.3: {} @@ -11004,6 +11347,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.4: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.2.0 @@ -11159,6 +11504,11 @@ snapshots: lilconfig@3.1.3: {} + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + lines-and-columns@1.2.4: {} lines-and-columns@2.0.3: {} @@ -12285,6 +12635,8 @@ snapshots: package-manager-detector@1.2.0: {} + pako@0.2.9: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -12293,6 +12645,11 @@ snapshots: dependencies: callsites: 3.1.0 + parse-css-color@0.2.1: + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + parse-entities@4.0.1: dependencies: '@types/unist': 2.0.7 @@ -12984,6 +13341,20 @@ snapshots: '@parcel/watcher': 2.5.1 optional: true + satori@0.11.3: + dependencies: + '@shuding/opentype.js': 1.4.0-beta.0 + css-background-parser: 0.1.0 + css-box-shadow: 1.0.0-3 + css-gradient-parser: 0.0.16 + css-to-react-native: 3.2.0 + emoji-regex: 10.4.0 + escape-html: 1.0.3 + linebreak: 1.1.0 + parse-css-color: 0.2.1 + postcss-value-parser: 4.2.0 + yoga-wasm-web: 0.3.3 + sax@1.3.0: {} scheduler@0.27.0: {} @@ -13033,6 +13404,32 @@ snapshots: is-plain-object: 2.0.4 is-primitive: 3.0.1 + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -13063,6 +13460,10 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + slash@3.0.0: {} slash@5.1.0: {} @@ -13209,6 +13610,8 @@ snapshots: get-east-asian-width: 1.2.0 strip-ansi: 7.1.0 + string.prototype.codepointat@0.2.1: {} + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -13377,6 +13780,8 @@ snapshots: throttleit@2.1.0: {} + tiny-inflate@1.0.3: {} + tinybench@2.9.0: {} tinyexec@0.3.1: {} @@ -13496,6 +13901,11 @@ snapshots: dependencies: hookable: 5.5.3 + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + unicorn-magic@0.1.0: {} unified-lint-rule@3.0.1: @@ -13881,6 +14291,8 @@ snapshots: yocto-queue@1.0.0: {} + yoga-wasm-web@0.3.3: {} + zod@4.1.11: {} zwitch@2.0.4: {} From 50ac78673f99afb1ceba17cccbf69b982d637f32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:51:46 +0000 Subject: [PATCH 3/5] Fix og:image meta tag injection and update tests Co-authored-by: SoonIter <79413249+SoonIter@users.noreply.github.com> --- .../plugin-og-image/doc/blog/first-post.md | 8 +++ e2e/fixtures/plugin-og-image/doc/guide.md | 8 +++ e2e/fixtures/plugin-og-image/doc/index.md | 3 + e2e/fixtures/plugin-og-image/fixture.json | 5 ++ e2e/fixtures/plugin-og-image/index.test.ts | 63 +++++++++++++++++++ e2e/fixtures/plugin-og-image/package.json | 14 +++++ .../plugin-og-image/rspress.config.ts | 15 +++++ e2e/fixtures/plugin-og-image/tsconfig.json | 3 + packages/plugin-og-image/src/plugin.ts | 30 ++++++--- pnpm-lock.yaml | 9 +++ 10 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 e2e/fixtures/plugin-og-image/doc/blog/first-post.md create mode 100644 e2e/fixtures/plugin-og-image/doc/guide.md create mode 100644 e2e/fixtures/plugin-og-image/doc/index.md create mode 100644 e2e/fixtures/plugin-og-image/fixture.json create mode 100644 e2e/fixtures/plugin-og-image/index.test.ts create mode 100644 e2e/fixtures/plugin-og-image/package.json create mode 100644 e2e/fixtures/plugin-og-image/rspress.config.ts create mode 100644 e2e/fixtures/plugin-og-image/tsconfig.json diff --git a/e2e/fixtures/plugin-og-image/doc/blog/first-post.md b/e2e/fixtures/plugin-og-image/doc/blog/first-post.md new file mode 100644 index 000000000..030379be2 --- /dev/null +++ b/e2e/fixtures/plugin-og-image/doc/blog/first-post.md @@ -0,0 +1,8 @@ +--- +title: My First Post +description: This is my first blog post +--- + +# My First Post + +Welcome to my blog! diff --git a/e2e/fixtures/plugin-og-image/doc/guide.md b/e2e/fixtures/plugin-og-image/doc/guide.md new file mode 100644 index 000000000..370d58f7d --- /dev/null +++ b/e2e/fixtures/plugin-og-image/doc/guide.md @@ -0,0 +1,8 @@ +--- +title: Getting Started +description: Learn how to get started with our platform +--- + +# Getting Started + +This guide will help you get started. diff --git a/e2e/fixtures/plugin-og-image/doc/index.md b/e2e/fixtures/plugin-og-image/doc/index.md new file mode 100644 index 000000000..3bcaeca88 --- /dev/null +++ b/e2e/fixtures/plugin-og-image/doc/index.md @@ -0,0 +1,3 @@ +# Welcome + +This is the home page. diff --git a/e2e/fixtures/plugin-og-image/fixture.json b/e2e/fixtures/plugin-og-image/fixture.json new file mode 100644 index 000000000..95b9a35c6 --- /dev/null +++ b/e2e/fixtures/plugin-og-image/fixture.json @@ -0,0 +1,5 @@ +{ + "base": "/", + "siteUrl": "http://localhost:4173/", + "title": "OG Image Test Site" +} diff --git a/e2e/fixtures/plugin-og-image/index.test.ts b/e2e/fixtures/plugin-og-image/index.test.ts new file mode 100644 index 000000000..36f9d8a2a --- /dev/null +++ b/e2e/fixtures/plugin-og-image/index.test.ts @@ -0,0 +1,63 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import * as NodePath from 'node:path'; +import { expect, test } from '@playwright/test'; +import { runBuildCommand } from '../../utils/runCommands'; + +const appDir = __dirname; + +test.describe('plugin og-image test', async () => { + test.beforeAll(async () => { + await runBuildCommand(appDir); + }); + + test('should generate og image for home page', async () => { + const imagePath = NodePath.resolve(appDir, 'doc_build/og/index.png'); + expect(existsSync(imagePath)).toBe(true); + + // Verify it's a valid PNG + const buffer = await readFile(imagePath); + expect(buffer.subarray(0, 8).toString('hex')).toBe('89504e470d0a1a0a'); // PNG signature + }); + + test('should generate og image for guide page', async () => { + const imagePath = NodePath.resolve(appDir, 'doc_build/og/guide.png'); + expect(existsSync(imagePath)).toBe(true); + + const buffer = await readFile(imagePath); + expect(buffer.subarray(0, 8).toString('hex')).toBe('89504e470d0a1a0a'); // PNG signature + }); + + test('should generate og image for blog post', async () => { + const imagePath = NodePath.resolve( + appDir, + 'doc_build/og/blog/first-post.png', + ); + expect(existsSync(imagePath)).toBe(true); + + const buffer = await readFile(imagePath); + expect(buffer.subarray(0, 8).toString('hex')).toBe('89504e470d0a1a0a'); // PNG signature + }); + + test('should have correct image dimensions', async () => { + const imagePath = NodePath.resolve(appDir, 'doc_build/og/index.png'); + const buffer = await readFile(imagePath); + + // PNG files store width and height at specific byte positions + // Width is at bytes 16-19, height at bytes 20-23 (big-endian) + const width = buffer.readUInt32BE(16); + const height = buffer.readUInt32BE(20); + + expect(width).toBe(1200); // Default width + expect(height).toBe(630); // Default height + }); + + test('should include og:image meta tag in HTML', async () => { + const htmlPath = NodePath.resolve(appDir, 'doc_build/guide.html'); + const html = await readFile(htmlPath, 'utf-8'); + + // Check that og:image meta tag is in the HTML + expect(html).toContain('og:image'); + expect(html).toContain('og/guide.png'); + }); +}); diff --git a/e2e/fixtures/plugin-og-image/package.json b/e2e/fixtures/plugin-og-image/package.json new file mode 100644 index 000000000..f2d29964c --- /dev/null +++ b/e2e/fixtures/plugin-og-image/package.json @@ -0,0 +1,14 @@ +{ + "name": "@rspress-fixture/plugin-og-image", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "rspress build", + "dev": "rspress dev", + "preview": "rspress preview" + }, + "dependencies": { + "@rspress/core": "workspace:*", + "@rspress/plugin-og-image": "workspace:*" + } +} diff --git a/e2e/fixtures/plugin-og-image/rspress.config.ts b/e2e/fixtures/plugin-og-image/rspress.config.ts new file mode 100644 index 000000000..9466770d2 --- /dev/null +++ b/e2e/fixtures/plugin-og-image/rspress.config.ts @@ -0,0 +1,15 @@ +import * as NodePath from 'node:path'; +import { defineConfig } from '@rspress/core'; +import { pluginOgImage } from '@rspress/plugin-og-image'; +import fixture from './fixture.json'; + +export default defineConfig({ + root: NodePath.resolve(__dirname, 'doc'), + title: fixture.title, + base: fixture.base, + plugins: [ + pluginOgImage({ + siteUrl: fixture.siteUrl, + }), + ], +}); diff --git a/e2e/fixtures/plugin-og-image/tsconfig.json b/e2e/fixtures/plugin-og-image/tsconfig.json new file mode 100644 index 000000000..618c6c3e9 --- /dev/null +++ b/e2e/fixtures/plugin-og-image/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../tsconfig.json" +} diff --git a/packages/plugin-og-image/src/plugin.ts b/packages/plugin-og-image/src/plugin.ts index efacdb03c..9b7d1a865 100644 --- a/packages/plugin-og-image/src/plugin.ts +++ b/packages/plugin-og-image/src/plugin.ts @@ -21,6 +21,28 @@ export function pluginOgImage(options: PluginOgImageOptions): RspressPlugin { return { name: '@rspress/plugin-og-image', + config(config) { + // Extend config.head to add og:image meta tags + const originalHead = config.head || []; + config.head = [ + ...originalHead, + route => { + const pageInfo = pageInfoMap.get(route.routePath); + if (pageInfo) { + return [ + 'meta', + { + property: 'og:image', + content: pageInfo.imageUrl, + }, + ] as [string, Record]; + } + return undefined; + }, + ]; + return config; + }, + async extendPageData(pageData: PageIndexInfo, isProd: boolean) { if (!isProd || !filter(pageData)) { return; @@ -53,14 +75,6 @@ export function pluginOgImage(options: PluginOgImageOptions): RspressPlugin { imageUrl, templateData, }); - - // Add OG meta tags to page data - if (!pageData.frontmatter) { - pageData.frontmatter = {}; - } - - // Set OG image meta tags - pageData.frontmatter.ogImage = imageUrl; }, async afterBuild(config, isProd) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b32d0552d..a13a3319b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -509,6 +509,15 @@ importers: specifier: ^22.8.1 version: 22.10.2 + e2e/fixtures/plugin-og-image: + dependencies: + '@rspress/core': + specifier: workspace:* + version: link:../../../packages/core + '@rspress/plugin-og-image': + specifier: workspace:* + version: link:../../../packages/plugin-og-image + e2e/fixtures/plugin-playground: dependencies: '@rspress/core': From 1fa765eddb8ae483b3d26a4eb30f15bcb59ef992 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:54:18 +0000 Subject: [PATCH 4/5] Fix linting issues (spell check and heading case) Co-authored-by: SoonIter <79413249+SoonIter@users.noreply.github.com> --- .../plugin-og-image/doc/blog/first-post.md | 2 +- e2e/fixtures/plugin-og-image/doc/guide.md | 2 +- packages/plugin-og-image/README.md | 4 +- scripts/dictionary.txt | 40 ++++++++++--------- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/e2e/fixtures/plugin-og-image/doc/blog/first-post.md b/e2e/fixtures/plugin-og-image/doc/blog/first-post.md index 030379be2..9573c5cab 100644 --- a/e2e/fixtures/plugin-og-image/doc/blog/first-post.md +++ b/e2e/fixtures/plugin-og-image/doc/blog/first-post.md @@ -3,6 +3,6 @@ title: My First Post description: This is my first blog post --- -# My First Post +# My first post Welcome to my blog! diff --git a/e2e/fixtures/plugin-og-image/doc/guide.md b/e2e/fixtures/plugin-og-image/doc/guide.md index 370d58f7d..836d9e7b7 100644 --- a/e2e/fixtures/plugin-og-image/doc/guide.md +++ b/e2e/fixtures/plugin-og-image/doc/guide.md @@ -3,6 +3,6 @@ title: Getting Started description: Learn how to get started with our platform --- -# Getting Started +# Getting started This guide will help you get started. diff --git a/packages/plugin-og-image/README.md b/packages/plugin-og-image/README.md index 3570ca5bb..e8137954c 100644 --- a/packages/plugin-og-image/README.md +++ b/packages/plugin-og-image/README.md @@ -73,7 +73,7 @@ Custom template function to generate the image. Receives page data and should re Filter function to determine which pages should have OG images generated. By default, all pages get OG images. -## Frontmatter Options +## Frontmatter options You can customize OG images per page using frontmatter: @@ -94,7 +94,7 @@ OG images are generated with URLs matching your page routes: - Page: `https://your-site.com/guide/getting-started` - OG Image: `https://your-site.com/og/guide/getting-started.png` -## How It Works +## How it works 1. During the build process, the plugin generates OG images for each page 2. Images are created using [Satori](https://github.com/vercel/satori) (SVG generation) and [Sharp](https://sharp.pixelplumbing.com/) (PNG conversion) diff --git a/scripts/dictionary.txt b/scripts/dictionary.txt index f0d6ccd99..5fc217e44 100644 --- a/scripts/dictionary.txt +++ b/scripts/dictionary.txt @@ -1,4 +1,23 @@ # Custom Dictionary Words +Bluch +Bytedance +Chunktmp +Français +Geass +Kinsta +Lamperouge +Lelouch +MDSSG +Mdxjs +Pipeable +Println +Publint +Qrcode +Rustify +SSGMD +Sizefor +Twoslasher +Unhead algoliasearch alinks analyse @@ -12,15 +31,12 @@ autohide autoreleasepool bilibili biomejs -Bluch brotli browserslistrc bundleless bunx -Bytedance caniuse chunkhash -Chunktmp classname codesandbox compat @@ -34,6 +50,7 @@ craco crossorigin datauri deepmerge +dejavu directorytest docgen docsite @@ -45,11 +62,9 @@ flexbox flexbugs flexsearch fnames -Français frontmatter fullhash fumadocs -Geass htmr iconify icss @@ -66,18 +81,13 @@ jsesc jsxs juejin katex -Kinsta koppers -Lamperouge -Lelouch lightningcss llms longpaths manypkg mattcompiles mdast -MDSSG -Mdxjs mdxrs menlo metastring @@ -101,7 +111,6 @@ pathinfo perfetto picocolors pinkpurple -Pipeable pjpeg pluggable pmmmwh @@ -112,10 +121,7 @@ prebundle preflights prefresh preprocessors -Println -Publint pxtorem -Qrcode rebranded rehype rsbuild @@ -128,16 +134,14 @@ rspack rspress rstack rstest -Rustify +satori selfsign shiki shikijs sirv -Sizefor sokra speedscope srcset -SSGMD stacktracey styl subdir @@ -156,8 +160,6 @@ treeshaking tsbuildinfo tsdoc twoslash -Twoslasher -Unhead unocss unpatch unplugin From b457cf83784508cd681c42c2501ed596f36a88e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:57:57 +0000 Subject: [PATCH 5/5] Add changeset for plugin-og-image Co-authored-by: SoonIter <79413249+SoonIter@users.noreply.github.com> --- .changeset/add-plugin-og-image.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/add-plugin-og-image.md diff --git a/.changeset/add-plugin-og-image.md b/.changeset/add-plugin-og-image.md new file mode 100644 index 000000000..966967b3f --- /dev/null +++ b/.changeset/add-plugin-og-image.md @@ -0,0 +1,7 @@ +--- +'@rspress/plugin-og-image': minor +--- + +feat: add plugin-og-image for dynamic Open Graph image generation + +This plugin automatically generates OG images for each page during build. It uses Satori for SVG rendering and Sharp for PNG conversion. Images are placed at `/og/{route-path}.png` and og:image meta tags are automatically injected into the HTML.