@ox-content/vite-plugin 0.3.0-alpha.13 → 0.3.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +586 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +180 -19
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +180 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +584 -59
- package/dist/index.js.map +1 -1
- package/package.json +28 -3
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { dirname, join } from "node:path";
|
|
|
14
14
|
import * as fs$1 from "fs";
|
|
15
15
|
import * as fs from "fs/promises";
|
|
16
16
|
import { glob } from "glob";
|
|
17
|
+
import * as crypto from "crypto";
|
|
17
18
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
18
19
|
|
|
19
20
|
//#region rolldown:runtime
|
|
@@ -7123,33 +7124,6 @@ if (import.meta.hot) {
|
|
|
7123
7124
|
}
|
|
7124
7125
|
`;
|
|
7125
7126
|
}
|
|
7126
|
-
/**
|
|
7127
|
-
* Generates an OG image SVG using the Rust-based generator.
|
|
7128
|
-
*
|
|
7129
|
-
* This function uses the Rust NAPI bindings to generate SVG-based
|
|
7130
|
-
* OG images for social media previews. The SVG can be served directly
|
|
7131
|
-
* or converted to PNG/JPEG for broader compatibility.
|
|
7132
|
-
*
|
|
7133
|
-
* In the future, custom JS templates can be provided to override
|
|
7134
|
-
* the default Rust-based template.
|
|
7135
|
-
*
|
|
7136
|
-
* @param data - OG image data (title, description, etc.)
|
|
7137
|
-
* @param config - Optional OG image configuration
|
|
7138
|
-
* @returns SVG string or null if NAPI bindings are unavailable
|
|
7139
|
-
*/
|
|
7140
|
-
async function generateOgImageSvg(data, config) {
|
|
7141
|
-
const napi = await loadNapiBindings();
|
|
7142
|
-
if (!napi) return null;
|
|
7143
|
-
const napiConfig = config ? {
|
|
7144
|
-
width: config.width,
|
|
7145
|
-
height: config.height,
|
|
7146
|
-
backgroundColor: config.backgroundColor,
|
|
7147
|
-
textColor: config.textColor,
|
|
7148
|
-
titleFontSize: config.titleFontSize,
|
|
7149
|
-
descriptionFontSize: config.descriptionFontSize
|
|
7150
|
-
} : void 0;
|
|
7151
|
-
return napi.generateOgImageSvg(data, napiConfig);
|
|
7152
|
-
}
|
|
7153
7127
|
|
|
7154
7128
|
//#endregion
|
|
7155
7129
|
//#region src/nav-generator.ts
|
|
@@ -8004,6 +7978,528 @@ function resolveDocsOptions(options) {
|
|
|
8004
7978
|
};
|
|
8005
7979
|
}
|
|
8006
7980
|
|
|
7981
|
+
//#endregion
|
|
7982
|
+
//#region src/og-image/renderer.ts
|
|
7983
|
+
/**
|
|
7984
|
+
* Wraps template HTML in a minimal document with viewport locked to given dimensions.
|
|
7985
|
+
*/
|
|
7986
|
+
function wrapHtml(bodyHtml, width, height) {
|
|
7987
|
+
return `<!DOCTYPE html>
|
|
7988
|
+
<html>
|
|
7989
|
+
<head>
|
|
7990
|
+
<meta charset="UTF-8">
|
|
7991
|
+
<style>
|
|
7992
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7993
|
+
html, body { width: ${width}px; height: ${height}px; overflow: hidden; }
|
|
7994
|
+
</style>
|
|
7995
|
+
</head>
|
|
7996
|
+
<body>${bodyHtml}</body>
|
|
7997
|
+
</html>`;
|
|
7998
|
+
}
|
|
7999
|
+
/**
|
|
8000
|
+
* Renders an HTML string to a PNG buffer using Chromium.
|
|
8001
|
+
*
|
|
8002
|
+
* @param page - Playwright page instance
|
|
8003
|
+
* @param html - HTML string from template function
|
|
8004
|
+
* @param width - Image width
|
|
8005
|
+
* @param height - Image height
|
|
8006
|
+
* @returns PNG buffer
|
|
8007
|
+
*/
|
|
8008
|
+
async function renderHtmlToPng(page, html, width, height) {
|
|
8009
|
+
await page.setViewportSize({
|
|
8010
|
+
width,
|
|
8011
|
+
height
|
|
8012
|
+
});
|
|
8013
|
+
const fullHtml = wrapHtml(html, width, height);
|
|
8014
|
+
await page.setContent(fullHtml, { waitUntil: "networkidle" });
|
|
8015
|
+
const screenshot = await page.screenshot({
|
|
8016
|
+
type: "png",
|
|
8017
|
+
clip: {
|
|
8018
|
+
x: 0,
|
|
8019
|
+
y: 0,
|
|
8020
|
+
width,
|
|
8021
|
+
height
|
|
8022
|
+
}
|
|
8023
|
+
});
|
|
8024
|
+
return Buffer.from(screenshot);
|
|
8025
|
+
}
|
|
8026
|
+
|
|
8027
|
+
//#endregion
|
|
8028
|
+
//#region src/og-image/browser.ts
|
|
8029
|
+
/**
|
|
8030
|
+
* Opens a Chromium browser and returns a session for rendering OG images.
|
|
8031
|
+
* Returns null if Playwright/Chromium is not available.
|
|
8032
|
+
*
|
|
8033
|
+
* The session implements AsyncDisposable — use `await using` for automatic cleanup:
|
|
8034
|
+
* ```ts
|
|
8035
|
+
* await using session = await openBrowser();
|
|
8036
|
+
* if (!session) return;
|
|
8037
|
+
* const png = await session.renderPage(html, 1200, 630);
|
|
8038
|
+
* ```
|
|
8039
|
+
*/
|
|
8040
|
+
async function openBrowser() {
|
|
8041
|
+
try {
|
|
8042
|
+
const { chromium } = await import("playwright");
|
|
8043
|
+
const browser = await chromium.launch({
|
|
8044
|
+
headless: true,
|
|
8045
|
+
args: [
|
|
8046
|
+
"--no-sandbox",
|
|
8047
|
+
"--disable-setuid-sandbox",
|
|
8048
|
+
"--disable-dev-shm-usage",
|
|
8049
|
+
"--disable-gpu"
|
|
8050
|
+
]
|
|
8051
|
+
});
|
|
8052
|
+
return {
|
|
8053
|
+
async renderPage(html, width, height) {
|
|
8054
|
+
const page = await browser.newPage();
|
|
8055
|
+
try {
|
|
8056
|
+
return await renderHtmlToPng(page, html, width, height);
|
|
8057
|
+
} finally {
|
|
8058
|
+
await page.close();
|
|
8059
|
+
}
|
|
8060
|
+
},
|
|
8061
|
+
async [Symbol.asyncDispose]() {
|
|
8062
|
+
try {
|
|
8063
|
+
await browser.close();
|
|
8064
|
+
} catch {}
|
|
8065
|
+
}
|
|
8066
|
+
};
|
|
8067
|
+
} catch (err) {
|
|
8068
|
+
console.warn("[ox-content:og-image] Chromium not available, skipping OG image generation.", err instanceof Error ? err.message : err);
|
|
8069
|
+
return null;
|
|
8070
|
+
}
|
|
8071
|
+
}
|
|
8072
|
+
|
|
8073
|
+
//#endregion
|
|
8074
|
+
//#region src/og-image/template.ts
|
|
8075
|
+
/**
|
|
8076
|
+
* Escapes HTML special characters.
|
|
8077
|
+
*/
|
|
8078
|
+
function escapeHtml$2(str) {
|
|
8079
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8080
|
+
}
|
|
8081
|
+
/**
|
|
8082
|
+
* Returns the built-in default template function.
|
|
8083
|
+
*/
|
|
8084
|
+
function getDefaultTemplate() {
|
|
8085
|
+
return function defaultTemplate(props) {
|
|
8086
|
+
const { title, description, siteName, tags } = props;
|
|
8087
|
+
const tagsHtml = tags?.length ? `<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:auto;">
|
|
8088
|
+
${tags.map((tag) => `<span style="background:rgba(255,255,255,0.15);color:#e2e8f0;padding:4px 12px;border-radius:16px;font-size:14px;">${escapeHtml$2(tag)}</span>`).join("")}
|
|
8089
|
+
</div>` : "";
|
|
8090
|
+
return `<div style="width:100%;height:100%;display:flex;flex-direction:column;justify-content:center;padding:60px 80px;background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%);font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
|
8091
|
+
<div style="display:flex;flex-direction:column;gap:16px;flex:1;justify-content:center;">
|
|
8092
|
+
<h1 style="font-size:56px;font-weight:700;color:#ffffff;line-height:1.2;margin:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;">${escapeHtml$2(title)}</h1>
|
|
8093
|
+
${description ? `<p style="font-size:24px;color:#94a3b8;line-height:1.5;margin:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;">${escapeHtml$2(description)}</p>` : ""}
|
|
8094
|
+
</div>
|
|
8095
|
+
<div style="display:flex;align-items:flex-end;justify-content:space-between;margin-top:auto;">
|
|
8096
|
+
${siteName ? `<span style="font-size:20px;color:#64748b;font-weight:500;">${escapeHtml$2(siteName)}</span>` : ""}
|
|
8097
|
+
${tagsHtml}
|
|
8098
|
+
</div>
|
|
8099
|
+
</div>`;
|
|
8100
|
+
};
|
|
8101
|
+
}
|
|
8102
|
+
|
|
8103
|
+
//#endregion
|
|
8104
|
+
//#region src/og-image/cache.ts
|
|
8105
|
+
/**
|
|
8106
|
+
* Content-hash based caching for OG images.
|
|
8107
|
+
*
|
|
8108
|
+
* Uses SHA256 of (template source + props + options) to determine
|
|
8109
|
+
* if a re-render is needed. Cache dir: .cache/og-images
|
|
8110
|
+
*/
|
|
8111
|
+
/**
|
|
8112
|
+
* Computes a cache key from template + props + options.
|
|
8113
|
+
*/
|
|
8114
|
+
function computeCacheKey(templateSource, props, width, height) {
|
|
8115
|
+
const data = JSON.stringify({
|
|
8116
|
+
templateSource,
|
|
8117
|
+
props,
|
|
8118
|
+
width,
|
|
8119
|
+
height
|
|
8120
|
+
});
|
|
8121
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
8122
|
+
}
|
|
8123
|
+
/**
|
|
8124
|
+
* Checks if a cached PNG exists for the given key.
|
|
8125
|
+
* Returns the cached file path if found, null otherwise.
|
|
8126
|
+
*/
|
|
8127
|
+
async function getCached(cacheDir, key) {
|
|
8128
|
+
const filePath = path.join(cacheDir, `${key}.png`);
|
|
8129
|
+
try {
|
|
8130
|
+
return await fs.readFile(filePath);
|
|
8131
|
+
} catch {
|
|
8132
|
+
return null;
|
|
8133
|
+
}
|
|
8134
|
+
}
|
|
8135
|
+
/**
|
|
8136
|
+
* Writes a PNG buffer to the cache.
|
|
8137
|
+
*/
|
|
8138
|
+
async function writeCache(cacheDir, key, png) {
|
|
8139
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
8140
|
+
const filePath = path.join(cacheDir, `${key}.png`);
|
|
8141
|
+
await fs.writeFile(filePath, png);
|
|
8142
|
+
}
|
|
8143
|
+
|
|
8144
|
+
//#endregion
|
|
8145
|
+
//#region src/og-image/index.ts
|
|
8146
|
+
/**
|
|
8147
|
+
* Public API for Chromium-based OG image generation.
|
|
8148
|
+
*
|
|
8149
|
+
* Orchestrates browser lifecycle, template resolution, caching,
|
|
8150
|
+
* and batch rendering with concurrency control.
|
|
8151
|
+
*/
|
|
8152
|
+
/**
|
|
8153
|
+
* Resolves user-provided OG image options with defaults.
|
|
8154
|
+
*/
|
|
8155
|
+
function resolveOgImageOptions(options) {
|
|
8156
|
+
return {
|
|
8157
|
+
template: options?.template,
|
|
8158
|
+
vuePlugin: options?.vuePlugin ?? "vitejs",
|
|
8159
|
+
width: options?.width ?? 1200,
|
|
8160
|
+
height: options?.height ?? 630,
|
|
8161
|
+
cache: options?.cache ?? true,
|
|
8162
|
+
concurrency: options?.concurrency ?? 1
|
|
8163
|
+
};
|
|
8164
|
+
}
|
|
8165
|
+
/**
|
|
8166
|
+
* Resolves the template function from options.
|
|
8167
|
+
*
|
|
8168
|
+
* Dispatches by file extension:
|
|
8169
|
+
* - `.vue` → Vue SFC (SSR via vue/server-renderer)
|
|
8170
|
+
* - `.svelte` → Svelte SFC (SSR via svelte/server)
|
|
8171
|
+
* - `.tsx`/`.jsx` → React Server Component (SSR via react-dom/server)
|
|
8172
|
+
* - others → TypeScript template (direct function export)
|
|
8173
|
+
*/
|
|
8174
|
+
async function resolveTemplate(options, root) {
|
|
8175
|
+
if (!options.template) return getDefaultTemplate();
|
|
8176
|
+
const templatePath = path.resolve(root, options.template);
|
|
8177
|
+
const fs = await import("fs/promises");
|
|
8178
|
+
try {
|
|
8179
|
+
await fs.access(templatePath);
|
|
8180
|
+
} catch {
|
|
8181
|
+
throw new Error(`[ox-content:og-image] Template file not found: ${templatePath}`);
|
|
8182
|
+
}
|
|
8183
|
+
switch (path.extname(templatePath).toLowerCase()) {
|
|
8184
|
+
case ".vue": return resolveVueTemplate(templatePath, options, root);
|
|
8185
|
+
case ".svelte": return resolveSvelteTemplate(templatePath, root);
|
|
8186
|
+
case ".tsx":
|
|
8187
|
+
case ".jsx": return resolveReactTemplate(templatePath, root);
|
|
8188
|
+
default: return resolveTsTemplate(templatePath, options, root);
|
|
8189
|
+
}
|
|
8190
|
+
}
|
|
8191
|
+
/**
|
|
8192
|
+
* Resolves a plain TypeScript template (existing behavior).
|
|
8193
|
+
*/
|
|
8194
|
+
async function resolveTsTemplate(templatePath, options, root) {
|
|
8195
|
+
const fs = await import("fs/promises");
|
|
8196
|
+
const { rolldown } = await import("rolldown");
|
|
8197
|
+
const cacheDir = path.join(root, ".cache", "og-images");
|
|
8198
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
8199
|
+
const outfile = path.join(cacheDir, "_template.mjs");
|
|
8200
|
+
const bundle = await rolldown({
|
|
8201
|
+
input: templatePath,
|
|
8202
|
+
platform: "node"
|
|
8203
|
+
});
|
|
8204
|
+
await bundle.write({
|
|
8205
|
+
file: outfile,
|
|
8206
|
+
format: "esm"
|
|
8207
|
+
});
|
|
8208
|
+
await bundle.close();
|
|
8209
|
+
const templateFn = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8210
|
+
if (typeof templateFn !== "function") throw new Error(`[ox-content:og-image] Template must default-export a function: ${options.template}`);
|
|
8211
|
+
return templateFn;
|
|
8212
|
+
}
|
|
8213
|
+
/**
|
|
8214
|
+
* Resolves a Vue SFC template via SSR.
|
|
8215
|
+
*
|
|
8216
|
+
* Compiles the SFC with @vue/compiler-sfc (or @vizejs/vite-plugin),
|
|
8217
|
+
* bundles with rolldown, then wraps with createSSRApp + renderToString.
|
|
8218
|
+
*/
|
|
8219
|
+
async function resolveVueTemplate(templatePath, options, root) {
|
|
8220
|
+
const fs = await import("fs/promises");
|
|
8221
|
+
const { rolldown } = await import("rolldown");
|
|
8222
|
+
const cacheDir = path.join(root, ".cache", "og-images");
|
|
8223
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
8224
|
+
const outfile = path.join(cacheDir, "_template_vue.mjs");
|
|
8225
|
+
const bundle = await rolldown({
|
|
8226
|
+
input: templatePath,
|
|
8227
|
+
platform: "node",
|
|
8228
|
+
external: ["vue", "vue/server-renderer"],
|
|
8229
|
+
plugins: options.vuePlugin === "vizejs" ? await getVizejsPlugin() : [createVueCompilerPlugin()]
|
|
8230
|
+
});
|
|
8231
|
+
await bundle.write({
|
|
8232
|
+
file: outfile,
|
|
8233
|
+
format: "esm"
|
|
8234
|
+
});
|
|
8235
|
+
await bundle.close();
|
|
8236
|
+
const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8237
|
+
if (!Component) throw new Error(`[ox-content:og-image] Vue template must have a default export: ${templatePath}`);
|
|
8238
|
+
const { createSSRApp } = await import("vue");
|
|
8239
|
+
const { renderToString } = await import("vue/server-renderer");
|
|
8240
|
+
return async (props) => {
|
|
8241
|
+
return renderToString(createSSRApp(Component, props));
|
|
8242
|
+
};
|
|
8243
|
+
}
|
|
8244
|
+
/**
|
|
8245
|
+
* Creates a rolldown plugin that compiles Vue SFCs using @vue/compiler-sfc.
|
|
8246
|
+
*/
|
|
8247
|
+
function createVueCompilerPlugin() {
|
|
8248
|
+
return {
|
|
8249
|
+
name: "ox-content-vue-sfc",
|
|
8250
|
+
async transform(code, id) {
|
|
8251
|
+
if (!id.endsWith(".vue")) return null;
|
|
8252
|
+
let compilerSfc;
|
|
8253
|
+
try {
|
|
8254
|
+
compilerSfc = await import("@vue/compiler-sfc");
|
|
8255
|
+
} catch {
|
|
8256
|
+
throw new Error("[ox-content:og-image] @vue/compiler-sfc is required for .vue templates. Install it with: pnpm add -D @vue/compiler-sfc");
|
|
8257
|
+
}
|
|
8258
|
+
const { descriptor } = compilerSfc.parse(code, { filename: id });
|
|
8259
|
+
let scriptCode;
|
|
8260
|
+
if (descriptor.scriptSetup || descriptor.script) scriptCode = compilerSfc.compileScript(descriptor, {
|
|
8261
|
+
id,
|
|
8262
|
+
inlineTemplate: true
|
|
8263
|
+
}).content;
|
|
8264
|
+
else {
|
|
8265
|
+
if (!descriptor.template) throw new Error(`[ox-content:og-image] Vue SFC must have a <template> or <script>: ${id}`);
|
|
8266
|
+
const templateResult = compilerSfc.compileTemplate({
|
|
8267
|
+
source: descriptor.template.content,
|
|
8268
|
+
filename: id,
|
|
8269
|
+
id
|
|
8270
|
+
});
|
|
8271
|
+
if (templateResult.errors.length > 0) throw new Error(`[ox-content:og-image] Vue template compilation errors in ${id}: ${templateResult.errors.join(", ")}`);
|
|
8272
|
+
scriptCode = `${templateResult.code}\nexport default { render }`;
|
|
8273
|
+
}
|
|
8274
|
+
const isTs = !!(descriptor.scriptSetup?.lang === "ts" || descriptor.script?.lang === "ts");
|
|
8275
|
+
return {
|
|
8276
|
+
code: scriptCode,
|
|
8277
|
+
moduleType: isTs ? "ts" : "js"
|
|
8278
|
+
};
|
|
8279
|
+
}
|
|
8280
|
+
};
|
|
8281
|
+
}
|
|
8282
|
+
/**
|
|
8283
|
+
* Loads @vizejs/vite-plugin as a rolldown plugin for Vue SFC compilation.
|
|
8284
|
+
*/
|
|
8285
|
+
async function getVizejsPlugin() {
|
|
8286
|
+
try {
|
|
8287
|
+
const vizejs = await import("@vizejs/vite-plugin");
|
|
8288
|
+
const plugin = vizejs.default?.() ?? vizejs;
|
|
8289
|
+
return Array.isArray(plugin) ? plugin : [plugin];
|
|
8290
|
+
} catch {
|
|
8291
|
+
throw new Error("[ox-content:og-image] @vizejs/vite-plugin is required when vuePlugin is 'vizejs'. Install it with: pnpm add -D @vizejs/vite-plugin");
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
/**
|
|
8295
|
+
* Resolves a Svelte SFC template via SSR.
|
|
8296
|
+
*
|
|
8297
|
+
* Compiles the SFC with svelte/compiler (server mode + runes),
|
|
8298
|
+
* bundles with rolldown, then wraps with svelte/server render().
|
|
8299
|
+
*/
|
|
8300
|
+
async function resolveSvelteTemplate(templatePath, root) {
|
|
8301
|
+
const fs = await import("fs/promises");
|
|
8302
|
+
const { rolldown } = await import("rolldown");
|
|
8303
|
+
const cacheDir = path.join(root, ".cache", "og-images");
|
|
8304
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
8305
|
+
const outfile = path.join(cacheDir, "_template_svelte.mjs");
|
|
8306
|
+
const bundle = await rolldown({
|
|
8307
|
+
input: templatePath,
|
|
8308
|
+
platform: "node",
|
|
8309
|
+
external: [
|
|
8310
|
+
"svelte",
|
|
8311
|
+
"svelte/server",
|
|
8312
|
+
"svelte/internal",
|
|
8313
|
+
"svelte/internal/server"
|
|
8314
|
+
],
|
|
8315
|
+
plugins: [createSvelteCompilerPlugin()]
|
|
8316
|
+
});
|
|
8317
|
+
await bundle.write({
|
|
8318
|
+
file: outfile,
|
|
8319
|
+
format: "esm"
|
|
8320
|
+
});
|
|
8321
|
+
await bundle.close();
|
|
8322
|
+
const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8323
|
+
if (!Component) throw new Error(`[ox-content:og-image] Svelte template must have a default export: ${templatePath}`);
|
|
8324
|
+
const { render } = await import("svelte/server");
|
|
8325
|
+
return async (props) => {
|
|
8326
|
+
const { body } = render(Component, { props });
|
|
8327
|
+
return body;
|
|
8328
|
+
};
|
|
8329
|
+
}
|
|
8330
|
+
/**
|
|
8331
|
+
* Creates a rolldown plugin that compiles Svelte SFCs using svelte/compiler.
|
|
8332
|
+
*/
|
|
8333
|
+
function createSvelteCompilerPlugin() {
|
|
8334
|
+
return {
|
|
8335
|
+
name: "ox-content-svelte-sfc",
|
|
8336
|
+
async transform(code, id) {
|
|
8337
|
+
if (!id.endsWith(".svelte")) return null;
|
|
8338
|
+
let svelteCompiler;
|
|
8339
|
+
try {
|
|
8340
|
+
svelteCompiler = await import("svelte/compiler");
|
|
8341
|
+
} catch {
|
|
8342
|
+
throw new Error("[ox-content:og-image] svelte is required for .svelte templates. Install it with: pnpm add -D svelte");
|
|
8343
|
+
}
|
|
8344
|
+
return { code: svelteCompiler.compile(code, {
|
|
8345
|
+
generate: "server",
|
|
8346
|
+
runes: true,
|
|
8347
|
+
filename: id
|
|
8348
|
+
}).js.code };
|
|
8349
|
+
}
|
|
8350
|
+
};
|
|
8351
|
+
}
|
|
8352
|
+
/**
|
|
8353
|
+
* Resolves a React (.tsx/.jsx) template via SSR.
|
|
8354
|
+
*
|
|
8355
|
+
* Bundles with rolldown (JSX transform), then wraps with
|
|
8356
|
+
* react-dom/server renderToReadableStream for async Server Component support.
|
|
8357
|
+
*/
|
|
8358
|
+
async function resolveReactTemplate(templatePath, root) {
|
|
8359
|
+
const fs = await import("fs/promises");
|
|
8360
|
+
const { rolldown } = await import("rolldown");
|
|
8361
|
+
const cacheDir = path.join(root, ".cache", "og-images");
|
|
8362
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
8363
|
+
const outfile = path.join(cacheDir, "_template_react.mjs");
|
|
8364
|
+
const bundle = await rolldown({
|
|
8365
|
+
input: templatePath,
|
|
8366
|
+
platform: "node",
|
|
8367
|
+
external: [
|
|
8368
|
+
"react",
|
|
8369
|
+
"react/jsx-runtime",
|
|
8370
|
+
"react/jsx-dev-runtime",
|
|
8371
|
+
"react-dom",
|
|
8372
|
+
"react-dom/server"
|
|
8373
|
+
],
|
|
8374
|
+
transform: { jsx: "react-jsx" }
|
|
8375
|
+
});
|
|
8376
|
+
await bundle.write({
|
|
8377
|
+
file: outfile,
|
|
8378
|
+
format: "esm"
|
|
8379
|
+
});
|
|
8380
|
+
await bundle.close();
|
|
8381
|
+
const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8382
|
+
if (!Component) throw new Error(`[ox-content:og-image] React template must have a default export: ${templatePath}`);
|
|
8383
|
+
let React;
|
|
8384
|
+
let ReactDOMServer;
|
|
8385
|
+
try {
|
|
8386
|
+
React = await import("react");
|
|
8387
|
+
ReactDOMServer = await import("react-dom/server");
|
|
8388
|
+
} catch {
|
|
8389
|
+
throw new Error("[ox-content:og-image] react and react-dom are required for .tsx/.jsx templates. Install them with: pnpm add -D react react-dom");
|
|
8390
|
+
}
|
|
8391
|
+
return async (props) => {
|
|
8392
|
+
const element = React.createElement(Component, props);
|
|
8393
|
+
const reader = (await ReactDOMServer.renderToReadableStream(element)).getReader();
|
|
8394
|
+
const chunks = [];
|
|
8395
|
+
while (true) {
|
|
8396
|
+
const { done, value } = await reader.read();
|
|
8397
|
+
if (done) break;
|
|
8398
|
+
chunks.push(value);
|
|
8399
|
+
}
|
|
8400
|
+
const decoder = new TextDecoder();
|
|
8401
|
+
return chunks.map((chunk) => decoder.decode(chunk, { stream: true })).join("") + decoder.decode();
|
|
8402
|
+
};
|
|
8403
|
+
}
|
|
8404
|
+
/**
|
|
8405
|
+
* Computes a stable template source identifier for cache keys.
|
|
8406
|
+
*
|
|
8407
|
+
* For custom templates, hashes the file content so cache invalidates
|
|
8408
|
+
* when the template changes. For the default template, returns a fixed string.
|
|
8409
|
+
*/
|
|
8410
|
+
async function computeTemplateSource(options, root) {
|
|
8411
|
+
if (!options.template) return "__default__";
|
|
8412
|
+
const fs = await import("fs/promises");
|
|
8413
|
+
const templatePath = path.resolve(root, options.template);
|
|
8414
|
+
const content = await fs.readFile(templatePath, "utf-8");
|
|
8415
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
8416
|
+
}
|
|
8417
|
+
/**
|
|
8418
|
+
* Generates OG images for a batch of pages.
|
|
8419
|
+
*
|
|
8420
|
+
* Manages the full lifecycle: resolve template → launch browser (with `using`) →
|
|
8421
|
+
* render each page (with caching and concurrency).
|
|
8422
|
+
*
|
|
8423
|
+
* All errors are non-fatal: failures are reported in results but never throw.
|
|
8424
|
+
*/
|
|
8425
|
+
async function generateOgImages(pages, options, root) {
|
|
8426
|
+
if (pages.length === 0) return [];
|
|
8427
|
+
const templateFn = await resolveTemplate(options, root);
|
|
8428
|
+
const templateSource = await computeTemplateSource(options, root);
|
|
8429
|
+
const cacheDir = path.join(root, ".cache", "og-images");
|
|
8430
|
+
if (options.cache) {
|
|
8431
|
+
const allCached = await tryServeAllFromCache(pages, templateSource, options, cacheDir);
|
|
8432
|
+
if (allCached) return allCached;
|
|
8433
|
+
}
|
|
8434
|
+
await using session = await openBrowser();
|
|
8435
|
+
if (!session) return pages.map((p) => ({
|
|
8436
|
+
outputPath: p.outputPath,
|
|
8437
|
+
cached: false,
|
|
8438
|
+
error: "Chromium not available"
|
|
8439
|
+
}));
|
|
8440
|
+
const results = [];
|
|
8441
|
+
const concurrency = Math.max(1, options.concurrency);
|
|
8442
|
+
for (let i = 0; i < pages.length; i += concurrency) {
|
|
8443
|
+
const batch = pages.slice(i, i + concurrency);
|
|
8444
|
+
const batchResults = await Promise.all(batch.map((entry) => renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session)));
|
|
8445
|
+
results.push(...batchResults);
|
|
8446
|
+
}
|
|
8447
|
+
return results;
|
|
8448
|
+
}
|
|
8449
|
+
/**
|
|
8450
|
+
* Tries to serve all pages from cache.
|
|
8451
|
+
* Returns results if ALL pages are cached, null otherwise.
|
|
8452
|
+
*/
|
|
8453
|
+
async function tryServeAllFromCache(pages, templateSource, options, cacheDir) {
|
|
8454
|
+
const fs = await import("fs/promises");
|
|
8455
|
+
const results = [];
|
|
8456
|
+
for (const entry of pages) {
|
|
8457
|
+
const cached = await getCached(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height));
|
|
8458
|
+
if (!cached) return null;
|
|
8459
|
+
await fs.mkdir(path.dirname(entry.outputPath), { recursive: true });
|
|
8460
|
+
await fs.writeFile(entry.outputPath, cached);
|
|
8461
|
+
results.push({
|
|
8462
|
+
outputPath: entry.outputPath,
|
|
8463
|
+
cached: true
|
|
8464
|
+
});
|
|
8465
|
+
}
|
|
8466
|
+
return results;
|
|
8467
|
+
}
|
|
8468
|
+
/**
|
|
8469
|
+
* Renders a single page to PNG, with cache support.
|
|
8470
|
+
*/
|
|
8471
|
+
async function renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session) {
|
|
8472
|
+
const fs = await import("fs/promises");
|
|
8473
|
+
try {
|
|
8474
|
+
if (options.cache) {
|
|
8475
|
+
const cached = await getCached(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height));
|
|
8476
|
+
if (cached) {
|
|
8477
|
+
await fs.mkdir(path.dirname(entry.outputPath), { recursive: true });
|
|
8478
|
+
await fs.writeFile(entry.outputPath, cached);
|
|
8479
|
+
return {
|
|
8480
|
+
outputPath: entry.outputPath,
|
|
8481
|
+
cached: true
|
|
8482
|
+
};
|
|
8483
|
+
}
|
|
8484
|
+
}
|
|
8485
|
+
const html = await templateFn(entry.props);
|
|
8486
|
+
const png = await session.renderPage(html, options.width, options.height);
|
|
8487
|
+
await fs.mkdir(path.dirname(entry.outputPath), { recursive: true });
|
|
8488
|
+
await fs.writeFile(entry.outputPath, png);
|
|
8489
|
+
if (options.cache) await writeCache(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height), png);
|
|
8490
|
+
return {
|
|
8491
|
+
outputPath: entry.outputPath,
|
|
8492
|
+
cached: false
|
|
8493
|
+
};
|
|
8494
|
+
} catch (err) {
|
|
8495
|
+
return {
|
|
8496
|
+
outputPath: entry.outputPath,
|
|
8497
|
+
cached: false,
|
|
8498
|
+
error: err instanceof Error ? err.message : String(err)
|
|
8499
|
+
};
|
|
8500
|
+
}
|
|
8501
|
+
}
|
|
8502
|
+
|
|
8007
8503
|
//#endregion
|
|
8008
8504
|
//#region src/plugins/index.ts
|
|
8009
8505
|
/**
|
|
@@ -9307,9 +9803,9 @@ function getOgImagePath(inputPath, srcDir, outDir) {
|
|
|
9307
9803
|
const baseName = path.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
|
|
9308
9804
|
if (baseName === "index" || baseName.endsWith("/index")) {
|
|
9309
9805
|
const dirPath = baseName.replace(/\/?index$/, "") || "";
|
|
9310
|
-
return path.join(outDir, dirPath, "og-image.
|
|
9806
|
+
return path.join(outDir, dirPath, "og-image.png");
|
|
9311
9807
|
}
|
|
9312
|
-
return path.join(outDir, baseName, "og-image.
|
|
9808
|
+
return path.join(outDir, baseName, "og-image.png");
|
|
9313
9809
|
}
|
|
9314
9810
|
/**
|
|
9315
9811
|
* Gets the OG image URL for use in meta tags.
|
|
@@ -9318,8 +9814,8 @@ function getOgImagePath(inputPath, srcDir, outDir) {
|
|
|
9318
9814
|
function getOgImageUrl(inputPath, srcDir, base, siteUrl) {
|
|
9319
9815
|
const urlPath = getUrlPath(inputPath, srcDir);
|
|
9320
9816
|
let relativePath;
|
|
9321
|
-
if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.
|
|
9322
|
-
else relativePath = `${base}${urlPath}/og-image.
|
|
9817
|
+
if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.png`;
|
|
9818
|
+
else relativePath = `${base}${urlPath}/og-image.png`;
|
|
9323
9819
|
if (siteUrl) return `${siteUrl.replace(/\/$/, "")}${relativePath}`;
|
|
9324
9820
|
return relativePath;
|
|
9325
9821
|
}
|
|
@@ -9434,6 +9930,10 @@ async function buildSsg(options, root) {
|
|
|
9434
9930
|
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
9435
9931
|
if (pkg.name) siteName = formatTitle(pkg.name);
|
|
9436
9932
|
} catch {}
|
|
9933
|
+
const ogImageEntries = [];
|
|
9934
|
+
const ogImageUrlMap = /* @__PURE__ */ new Map();
|
|
9935
|
+
const shouldGenerateOgImages = (options.ogImage || ssgOptions.generateOgImage) && !ssgOptions.bare;
|
|
9936
|
+
const pageResults = [];
|
|
9437
9937
|
for (const inputPath of markdownFiles) try {
|
|
9438
9938
|
const result = await transformMarkdown(await fs.readFile(inputPath, "utf-8"), inputPath, options, {
|
|
9439
9939
|
convertMdLinks: true,
|
|
@@ -9456,31 +9956,56 @@ async function buildSsg(options, root) {
|
|
|
9456
9956
|
transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
|
|
9457
9957
|
const title = extractTitle(transformedHtml, result.frontmatter);
|
|
9458
9958
|
const description = result.frontmatter.description;
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
}
|
|
9959
|
+
pageResults.push({
|
|
9960
|
+
inputPath,
|
|
9961
|
+
transformedHtml,
|
|
9962
|
+
title,
|
|
9963
|
+
description,
|
|
9964
|
+
frontmatter: result.frontmatter,
|
|
9965
|
+
toc: result.toc
|
|
9966
|
+
});
|
|
9967
|
+
if (shouldGenerateOgImages) {
|
|
9968
|
+
const ogImageOutputPath = getOgImagePath(inputPath, srcDir, outDir);
|
|
9969
|
+
const { layout: _layout, ...frontmatterRest } = result.frontmatter;
|
|
9970
|
+
ogImageEntries.push({
|
|
9971
|
+
props: {
|
|
9972
|
+
...frontmatterRest,
|
|
9973
|
+
title,
|
|
9974
|
+
description,
|
|
9975
|
+
siteName
|
|
9976
|
+
},
|
|
9977
|
+
outputPath: ogImageOutputPath
|
|
9978
|
+
});
|
|
9979
|
+
ogImageUrlMap.set(inputPath, getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl));
|
|
9479
9980
|
}
|
|
9981
|
+
} catch (err) {
|
|
9982
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
9983
|
+
errors.push(`Failed to process ${inputPath}: ${errorMessage}`);
|
|
9984
|
+
}
|
|
9985
|
+
if (shouldGenerateOgImages && ogImageEntries.length > 0) try {
|
|
9986
|
+
const ogResults = await generateOgImages(ogImageEntries, options.ogImageOptions, root);
|
|
9987
|
+
let ogSuccessCount = 0;
|
|
9988
|
+
for (const result of ogResults) if (result.error) errors.push(`OG image failed for ${result.outputPath}: ${result.error}`);
|
|
9989
|
+
else {
|
|
9990
|
+
generatedFiles.push(result.outputPath);
|
|
9991
|
+
ogSuccessCount++;
|
|
9992
|
+
}
|
|
9993
|
+
if (ogSuccessCount > 0) {
|
|
9994
|
+
const cachedCount = ogResults.filter((r) => r.cached && !r.error).length;
|
|
9995
|
+
console.log(`[ox-content:og-image] Generated ${ogSuccessCount} OG images` + (cachedCount > 0 ? ` (${cachedCount} from cache)` : ""));
|
|
9996
|
+
}
|
|
9997
|
+
} catch (err) {
|
|
9998
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
9999
|
+
console.warn(`[ox-content:og-image] Batch generation failed: ${errorMessage}`);
|
|
10000
|
+
}
|
|
10001
|
+
for (const pageResult of pageResults) try {
|
|
10002
|
+
const { inputPath, transformedHtml, title, description, frontmatter, toc } = pageResult;
|
|
10003
|
+
let pageOgImage = ssgOptions.ogImage;
|
|
10004
|
+
if (shouldGenerateOgImages && ogImageUrlMap.has(inputPath)) pageOgImage = ogImageUrlMap.get(inputPath);
|
|
9480
10005
|
let entryPage;
|
|
9481
|
-
if (
|
|
9482
|
-
hero:
|
|
9483
|
-
features:
|
|
10006
|
+
if (frontmatter.layout === "entry") entryPage = {
|
|
10007
|
+
hero: frontmatter.hero,
|
|
10008
|
+
features: frontmatter.features
|
|
9484
10009
|
};
|
|
9485
10010
|
let html;
|
|
9486
10011
|
if (ssgOptions.bare) html = generateBareHtmlPage(transformedHtml, title);
|
|
@@ -9488,8 +10013,8 @@ async function buildSsg(options, root) {
|
|
|
9488
10013
|
title,
|
|
9489
10014
|
description,
|
|
9490
10015
|
content: transformedHtml,
|
|
9491
|
-
toc
|
|
9492
|
-
frontmatter
|
|
10016
|
+
toc,
|
|
10017
|
+
frontmatter,
|
|
9493
10018
|
path: getUrlPath(inputPath, srcDir),
|
|
9494
10019
|
href: getHref(inputPath, srcDir, base, ssgOptions.extension),
|
|
9495
10020
|
entryPage
|
|
@@ -9500,7 +10025,7 @@ async function buildSsg(options, root) {
|
|
|
9500
10025
|
generatedFiles.push(outputPath);
|
|
9501
10026
|
} catch (err) {
|
|
9502
10027
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
9503
|
-
errors.push(`Failed to
|
|
10028
|
+
errors.push(`Failed to generate HTML for ${pageResult.inputPath}: ${errorMessage}`);
|
|
9504
10029
|
}
|
|
9505
10030
|
return {
|
|
9506
10031
|
files: generatedFiles,
|
|
@@ -10521,7 +11046,7 @@ function resolveOptions(options) {
|
|
|
10521
11046
|
toc: options.toc ?? true,
|
|
10522
11047
|
tocMaxDepth: options.tocMaxDepth ?? 3,
|
|
10523
11048
|
ogImage: options.ogImage ?? false,
|
|
10524
|
-
ogImageOptions: options.ogImageOptions
|
|
11049
|
+
ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
|
|
10525
11050
|
transformers: options.transformers ?? [],
|
|
10526
11051
|
docs: resolveDocsOptions(options.docs),
|
|
10527
11052
|
search: resolveSearchOptions(options.search)
|
|
@@ -10546,5 +11071,5 @@ function generateVirtualModule(path, options) {
|
|
|
10546
11071
|
}
|
|
10547
11072
|
|
|
10548
11073
|
//#endregion
|
|
10549
|
-
export { DEFAULT_HTML_TEMPLATE, DefaultTheme, Fragment, buildSearchIndex, buildSsg, clearRenderContext, collectGitHubRepos, collectOgpUrls, createMarkdownEnvironment, createTheme, defaultTheme, defineTheme, each, extractDocs, extractIslandInfo, extractVideoId, fetchOgpData, fetchRepoData, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateTabsCSS, generateTypes, hasIslands, inferType, jsx, jsxs, mergeThemes, mermaidClientScript, oxContent, prefetchGitHubRepos, prefetchOgpData, raw, renderAllPages, renderPage, renderToString, resolveDocsOptions, resolveSearchOptions, resolveSsgOptions, resolveTheme, setRenderContext, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeSearchIndex };
|
|
11074
|
+
export { DEFAULT_HTML_TEMPLATE, DefaultTheme, Fragment, buildSearchIndex, buildSsg, clearRenderContext, collectGitHubRepos, collectOgpUrls, createMarkdownEnvironment, createTheme, defaultTheme, defineTheme, each, extractDocs, extractIslandInfo, extractVideoId, fetchOgpData, fetchRepoData, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateOgImages, generateTabsCSS, generateTypes, hasIslands, inferType, jsx, jsxs, mergeThemes, mermaidClientScript, oxContent, prefetchGitHubRepos, prefetchOgpData, raw, renderAllPages, renderPage, renderToString, resolveDocsOptions, resolveOgImageOptions, resolveSearchOptions, resolveSsgOptions, resolveTheme, setRenderContext, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeSearchIndex };
|
|
10550
11075
|
//# sourceMappingURL=index.js.map
|