@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.cjs
CHANGED
|
@@ -18,6 +18,8 @@ fs = require_chunk.__toESM(fs);
|
|
|
18
18
|
let fs_promises = require("fs/promises");
|
|
19
19
|
fs_promises = require_chunk.__toESM(fs_promises);
|
|
20
20
|
let glob = require("glob");
|
|
21
|
+
let crypto = require("crypto");
|
|
22
|
+
crypto = require_chunk.__toESM(crypto);
|
|
21
23
|
let node_fs_promises = require("node:fs/promises");
|
|
22
24
|
|
|
23
25
|
//#region src/environment.ts
|
|
@@ -7083,33 +7085,6 @@ if (import.meta.hot) {
|
|
|
7083
7085
|
}
|
|
7084
7086
|
`;
|
|
7085
7087
|
}
|
|
7086
|
-
/**
|
|
7087
|
-
* Generates an OG image SVG using the Rust-based generator.
|
|
7088
|
-
*
|
|
7089
|
-
* This function uses the Rust NAPI bindings to generate SVG-based
|
|
7090
|
-
* OG images for social media previews. The SVG can be served directly
|
|
7091
|
-
* or converted to PNG/JPEG for broader compatibility.
|
|
7092
|
-
*
|
|
7093
|
-
* In the future, custom JS templates can be provided to override
|
|
7094
|
-
* the default Rust-based template.
|
|
7095
|
-
*
|
|
7096
|
-
* @param data - OG image data (title, description, etc.)
|
|
7097
|
-
* @param config - Optional OG image configuration
|
|
7098
|
-
* @returns SVG string or null if NAPI bindings are unavailable
|
|
7099
|
-
*/
|
|
7100
|
-
async function generateOgImageSvg(data, config) {
|
|
7101
|
-
const napi = await loadNapiBindings();
|
|
7102
|
-
if (!napi) return null;
|
|
7103
|
-
const napiConfig = config ? {
|
|
7104
|
-
width: config.width,
|
|
7105
|
-
height: config.height,
|
|
7106
|
-
backgroundColor: config.backgroundColor,
|
|
7107
|
-
textColor: config.textColor,
|
|
7108
|
-
titleFontSize: config.titleFontSize,
|
|
7109
|
-
descriptionFontSize: config.descriptionFontSize
|
|
7110
|
-
} : void 0;
|
|
7111
|
-
return napi.generateOgImageSvg(data, napiConfig);
|
|
7112
|
-
}
|
|
7113
7088
|
|
|
7114
7089
|
//#endregion
|
|
7115
7090
|
//#region src/nav-generator.ts
|
|
@@ -7964,6 +7939,528 @@ function resolveDocsOptions(options) {
|
|
|
7964
7939
|
};
|
|
7965
7940
|
}
|
|
7966
7941
|
|
|
7942
|
+
//#endregion
|
|
7943
|
+
//#region src/og-image/renderer.ts
|
|
7944
|
+
/**
|
|
7945
|
+
* Wraps template HTML in a minimal document with viewport locked to given dimensions.
|
|
7946
|
+
*/
|
|
7947
|
+
function wrapHtml(bodyHtml, width, height) {
|
|
7948
|
+
return `<!DOCTYPE html>
|
|
7949
|
+
<html>
|
|
7950
|
+
<head>
|
|
7951
|
+
<meta charset="UTF-8">
|
|
7952
|
+
<style>
|
|
7953
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7954
|
+
html, body { width: ${width}px; height: ${height}px; overflow: hidden; }
|
|
7955
|
+
</style>
|
|
7956
|
+
</head>
|
|
7957
|
+
<body>${bodyHtml}</body>
|
|
7958
|
+
</html>`;
|
|
7959
|
+
}
|
|
7960
|
+
/**
|
|
7961
|
+
* Renders an HTML string to a PNG buffer using Chromium.
|
|
7962
|
+
*
|
|
7963
|
+
* @param page - Playwright page instance
|
|
7964
|
+
* @param html - HTML string from template function
|
|
7965
|
+
* @param width - Image width
|
|
7966
|
+
* @param height - Image height
|
|
7967
|
+
* @returns PNG buffer
|
|
7968
|
+
*/
|
|
7969
|
+
async function renderHtmlToPng(page, html, width, height) {
|
|
7970
|
+
await page.setViewportSize({
|
|
7971
|
+
width,
|
|
7972
|
+
height
|
|
7973
|
+
});
|
|
7974
|
+
const fullHtml = wrapHtml(html, width, height);
|
|
7975
|
+
await page.setContent(fullHtml, { waitUntil: "networkidle" });
|
|
7976
|
+
const screenshot = await page.screenshot({
|
|
7977
|
+
type: "png",
|
|
7978
|
+
clip: {
|
|
7979
|
+
x: 0,
|
|
7980
|
+
y: 0,
|
|
7981
|
+
width,
|
|
7982
|
+
height
|
|
7983
|
+
}
|
|
7984
|
+
});
|
|
7985
|
+
return Buffer.from(screenshot);
|
|
7986
|
+
}
|
|
7987
|
+
|
|
7988
|
+
//#endregion
|
|
7989
|
+
//#region src/og-image/browser.ts
|
|
7990
|
+
/**
|
|
7991
|
+
* Opens a Chromium browser and returns a session for rendering OG images.
|
|
7992
|
+
* Returns null if Playwright/Chromium is not available.
|
|
7993
|
+
*
|
|
7994
|
+
* The session implements AsyncDisposable — use `await using` for automatic cleanup:
|
|
7995
|
+
* ```ts
|
|
7996
|
+
* await using session = await openBrowser();
|
|
7997
|
+
* if (!session) return;
|
|
7998
|
+
* const png = await session.renderPage(html, 1200, 630);
|
|
7999
|
+
* ```
|
|
8000
|
+
*/
|
|
8001
|
+
async function openBrowser() {
|
|
8002
|
+
try {
|
|
8003
|
+
const { chromium } = await import("playwright");
|
|
8004
|
+
const browser = await chromium.launch({
|
|
8005
|
+
headless: true,
|
|
8006
|
+
args: [
|
|
8007
|
+
"--no-sandbox",
|
|
8008
|
+
"--disable-setuid-sandbox",
|
|
8009
|
+
"--disable-dev-shm-usage",
|
|
8010
|
+
"--disable-gpu"
|
|
8011
|
+
]
|
|
8012
|
+
});
|
|
8013
|
+
return {
|
|
8014
|
+
async renderPage(html, width, height) {
|
|
8015
|
+
const page = await browser.newPage();
|
|
8016
|
+
try {
|
|
8017
|
+
return await renderHtmlToPng(page, html, width, height);
|
|
8018
|
+
} finally {
|
|
8019
|
+
await page.close();
|
|
8020
|
+
}
|
|
8021
|
+
},
|
|
8022
|
+
async [Symbol.asyncDispose]() {
|
|
8023
|
+
try {
|
|
8024
|
+
await browser.close();
|
|
8025
|
+
} catch {}
|
|
8026
|
+
}
|
|
8027
|
+
};
|
|
8028
|
+
} catch (err) {
|
|
8029
|
+
console.warn("[ox-content:og-image] Chromium not available, skipping OG image generation.", err instanceof Error ? err.message : err);
|
|
8030
|
+
return null;
|
|
8031
|
+
}
|
|
8032
|
+
}
|
|
8033
|
+
|
|
8034
|
+
//#endregion
|
|
8035
|
+
//#region src/og-image/template.ts
|
|
8036
|
+
/**
|
|
8037
|
+
* Escapes HTML special characters.
|
|
8038
|
+
*/
|
|
8039
|
+
function escapeHtml$2(str) {
|
|
8040
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8041
|
+
}
|
|
8042
|
+
/**
|
|
8043
|
+
* Returns the built-in default template function.
|
|
8044
|
+
*/
|
|
8045
|
+
function getDefaultTemplate() {
|
|
8046
|
+
return function defaultTemplate(props) {
|
|
8047
|
+
const { title, description, siteName, tags } = props;
|
|
8048
|
+
const tagsHtml = tags?.length ? `<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:auto;">
|
|
8049
|
+
${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("")}
|
|
8050
|
+
</div>` : "";
|
|
8051
|
+
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;">
|
|
8052
|
+
<div style="display:flex;flex-direction:column;gap:16px;flex:1;justify-content:center;">
|
|
8053
|
+
<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>
|
|
8054
|
+
${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>` : ""}
|
|
8055
|
+
</div>
|
|
8056
|
+
<div style="display:flex;align-items:flex-end;justify-content:space-between;margin-top:auto;">
|
|
8057
|
+
${siteName ? `<span style="font-size:20px;color:#64748b;font-weight:500;">${escapeHtml$2(siteName)}</span>` : ""}
|
|
8058
|
+
${tagsHtml}
|
|
8059
|
+
</div>
|
|
8060
|
+
</div>`;
|
|
8061
|
+
};
|
|
8062
|
+
}
|
|
8063
|
+
|
|
8064
|
+
//#endregion
|
|
8065
|
+
//#region src/og-image/cache.ts
|
|
8066
|
+
/**
|
|
8067
|
+
* Content-hash based caching for OG images.
|
|
8068
|
+
*
|
|
8069
|
+
* Uses SHA256 of (template source + props + options) to determine
|
|
8070
|
+
* if a re-render is needed. Cache dir: .cache/og-images
|
|
8071
|
+
*/
|
|
8072
|
+
/**
|
|
8073
|
+
* Computes a cache key from template + props + options.
|
|
8074
|
+
*/
|
|
8075
|
+
function computeCacheKey(templateSource, props, width, height) {
|
|
8076
|
+
const data = JSON.stringify({
|
|
8077
|
+
templateSource,
|
|
8078
|
+
props,
|
|
8079
|
+
width,
|
|
8080
|
+
height
|
|
8081
|
+
});
|
|
8082
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
8083
|
+
}
|
|
8084
|
+
/**
|
|
8085
|
+
* Checks if a cached PNG exists for the given key.
|
|
8086
|
+
* Returns the cached file path if found, null otherwise.
|
|
8087
|
+
*/
|
|
8088
|
+
async function getCached(cacheDir, key) {
|
|
8089
|
+
const filePath = path$1.join(cacheDir, `${key}.png`);
|
|
8090
|
+
try {
|
|
8091
|
+
return await fs_promises.readFile(filePath);
|
|
8092
|
+
} catch {
|
|
8093
|
+
return null;
|
|
8094
|
+
}
|
|
8095
|
+
}
|
|
8096
|
+
/**
|
|
8097
|
+
* Writes a PNG buffer to the cache.
|
|
8098
|
+
*/
|
|
8099
|
+
async function writeCache(cacheDir, key, png) {
|
|
8100
|
+
await fs_promises.mkdir(cacheDir, { recursive: true });
|
|
8101
|
+
const filePath = path$1.join(cacheDir, `${key}.png`);
|
|
8102
|
+
await fs_promises.writeFile(filePath, png);
|
|
8103
|
+
}
|
|
8104
|
+
|
|
8105
|
+
//#endregion
|
|
8106
|
+
//#region src/og-image/index.ts
|
|
8107
|
+
/**
|
|
8108
|
+
* Public API for Chromium-based OG image generation.
|
|
8109
|
+
*
|
|
8110
|
+
* Orchestrates browser lifecycle, template resolution, caching,
|
|
8111
|
+
* and batch rendering with concurrency control.
|
|
8112
|
+
*/
|
|
8113
|
+
/**
|
|
8114
|
+
* Resolves user-provided OG image options with defaults.
|
|
8115
|
+
*/
|
|
8116
|
+
function resolveOgImageOptions(options) {
|
|
8117
|
+
return {
|
|
8118
|
+
template: options?.template,
|
|
8119
|
+
vuePlugin: options?.vuePlugin ?? "vitejs",
|
|
8120
|
+
width: options?.width ?? 1200,
|
|
8121
|
+
height: options?.height ?? 630,
|
|
8122
|
+
cache: options?.cache ?? true,
|
|
8123
|
+
concurrency: options?.concurrency ?? 1
|
|
8124
|
+
};
|
|
8125
|
+
}
|
|
8126
|
+
/**
|
|
8127
|
+
* Resolves the template function from options.
|
|
8128
|
+
*
|
|
8129
|
+
* Dispatches by file extension:
|
|
8130
|
+
* - `.vue` → Vue SFC (SSR via vue/server-renderer)
|
|
8131
|
+
* - `.svelte` → Svelte SFC (SSR via svelte/server)
|
|
8132
|
+
* - `.tsx`/`.jsx` → React Server Component (SSR via react-dom/server)
|
|
8133
|
+
* - others → TypeScript template (direct function export)
|
|
8134
|
+
*/
|
|
8135
|
+
async function resolveTemplate(options, root) {
|
|
8136
|
+
if (!options.template) return getDefaultTemplate();
|
|
8137
|
+
const templatePath = path$1.resolve(root, options.template);
|
|
8138
|
+
const fs = await import("fs/promises");
|
|
8139
|
+
try {
|
|
8140
|
+
await fs.access(templatePath);
|
|
8141
|
+
} catch {
|
|
8142
|
+
throw new Error(`[ox-content:og-image] Template file not found: ${templatePath}`);
|
|
8143
|
+
}
|
|
8144
|
+
switch (path$1.extname(templatePath).toLowerCase()) {
|
|
8145
|
+
case ".vue": return resolveVueTemplate(templatePath, options, root);
|
|
8146
|
+
case ".svelte": return resolveSvelteTemplate(templatePath, root);
|
|
8147
|
+
case ".tsx":
|
|
8148
|
+
case ".jsx": return resolveReactTemplate(templatePath, root);
|
|
8149
|
+
default: return resolveTsTemplate(templatePath, options, root);
|
|
8150
|
+
}
|
|
8151
|
+
}
|
|
8152
|
+
/**
|
|
8153
|
+
* Resolves a plain TypeScript template (existing behavior).
|
|
8154
|
+
*/
|
|
8155
|
+
async function resolveTsTemplate(templatePath, options, root) {
|
|
8156
|
+
const fs = await import("fs/promises");
|
|
8157
|
+
const { rolldown } = await import("rolldown");
|
|
8158
|
+
const cacheDir = path$1.join(root, ".cache", "og-images");
|
|
8159
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
8160
|
+
const outfile = path$1.join(cacheDir, "_template.mjs");
|
|
8161
|
+
const bundle = await rolldown({
|
|
8162
|
+
input: templatePath,
|
|
8163
|
+
platform: "node"
|
|
8164
|
+
});
|
|
8165
|
+
await bundle.write({
|
|
8166
|
+
file: outfile,
|
|
8167
|
+
format: "esm"
|
|
8168
|
+
});
|
|
8169
|
+
await bundle.close();
|
|
8170
|
+
const templateFn = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8171
|
+
if (typeof templateFn !== "function") throw new Error(`[ox-content:og-image] Template must default-export a function: ${options.template}`);
|
|
8172
|
+
return templateFn;
|
|
8173
|
+
}
|
|
8174
|
+
/**
|
|
8175
|
+
* Resolves a Vue SFC template via SSR.
|
|
8176
|
+
*
|
|
8177
|
+
* Compiles the SFC with @vue/compiler-sfc (or @vizejs/vite-plugin),
|
|
8178
|
+
* bundles with rolldown, then wraps with createSSRApp + renderToString.
|
|
8179
|
+
*/
|
|
8180
|
+
async function resolveVueTemplate(templatePath, options, root) {
|
|
8181
|
+
const fs = await import("fs/promises");
|
|
8182
|
+
const { rolldown } = await import("rolldown");
|
|
8183
|
+
const cacheDir = path$1.join(root, ".cache", "og-images");
|
|
8184
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
8185
|
+
const outfile = path$1.join(cacheDir, "_template_vue.mjs");
|
|
8186
|
+
const bundle = await rolldown({
|
|
8187
|
+
input: templatePath,
|
|
8188
|
+
platform: "node",
|
|
8189
|
+
external: ["vue", "vue/server-renderer"],
|
|
8190
|
+
plugins: options.vuePlugin === "vizejs" ? await getVizejsPlugin() : [createVueCompilerPlugin()]
|
|
8191
|
+
});
|
|
8192
|
+
await bundle.write({
|
|
8193
|
+
file: outfile,
|
|
8194
|
+
format: "esm"
|
|
8195
|
+
});
|
|
8196
|
+
await bundle.close();
|
|
8197
|
+
const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8198
|
+
if (!Component) throw new Error(`[ox-content:og-image] Vue template must have a default export: ${templatePath}`);
|
|
8199
|
+
const { createSSRApp } = await import("vue");
|
|
8200
|
+
const { renderToString } = await import("vue/server-renderer");
|
|
8201
|
+
return async (props) => {
|
|
8202
|
+
return renderToString(createSSRApp(Component, props));
|
|
8203
|
+
};
|
|
8204
|
+
}
|
|
8205
|
+
/**
|
|
8206
|
+
* Creates a rolldown plugin that compiles Vue SFCs using @vue/compiler-sfc.
|
|
8207
|
+
*/
|
|
8208
|
+
function createVueCompilerPlugin() {
|
|
8209
|
+
return {
|
|
8210
|
+
name: "ox-content-vue-sfc",
|
|
8211
|
+
async transform(code, id) {
|
|
8212
|
+
if (!id.endsWith(".vue")) return null;
|
|
8213
|
+
let compilerSfc;
|
|
8214
|
+
try {
|
|
8215
|
+
compilerSfc = await import("@vue/compiler-sfc");
|
|
8216
|
+
} catch {
|
|
8217
|
+
throw new Error("[ox-content:og-image] @vue/compiler-sfc is required for .vue templates. Install it with: pnpm add -D @vue/compiler-sfc");
|
|
8218
|
+
}
|
|
8219
|
+
const { descriptor } = compilerSfc.parse(code, { filename: id });
|
|
8220
|
+
let scriptCode;
|
|
8221
|
+
if (descriptor.scriptSetup || descriptor.script) scriptCode = compilerSfc.compileScript(descriptor, {
|
|
8222
|
+
id,
|
|
8223
|
+
inlineTemplate: true
|
|
8224
|
+
}).content;
|
|
8225
|
+
else {
|
|
8226
|
+
if (!descriptor.template) throw new Error(`[ox-content:og-image] Vue SFC must have a <template> or <script>: ${id}`);
|
|
8227
|
+
const templateResult = compilerSfc.compileTemplate({
|
|
8228
|
+
source: descriptor.template.content,
|
|
8229
|
+
filename: id,
|
|
8230
|
+
id
|
|
8231
|
+
});
|
|
8232
|
+
if (templateResult.errors.length > 0) throw new Error(`[ox-content:og-image] Vue template compilation errors in ${id}: ${templateResult.errors.join(", ")}`);
|
|
8233
|
+
scriptCode = `${templateResult.code}\nexport default { render }`;
|
|
8234
|
+
}
|
|
8235
|
+
const isTs = !!(descriptor.scriptSetup?.lang === "ts" || descriptor.script?.lang === "ts");
|
|
8236
|
+
return {
|
|
8237
|
+
code: scriptCode,
|
|
8238
|
+
moduleType: isTs ? "ts" : "js"
|
|
8239
|
+
};
|
|
8240
|
+
}
|
|
8241
|
+
};
|
|
8242
|
+
}
|
|
8243
|
+
/**
|
|
8244
|
+
* Loads @vizejs/vite-plugin as a rolldown plugin for Vue SFC compilation.
|
|
8245
|
+
*/
|
|
8246
|
+
async function getVizejsPlugin() {
|
|
8247
|
+
try {
|
|
8248
|
+
const vizejs = await import("@vizejs/vite-plugin");
|
|
8249
|
+
const plugin = vizejs.default?.() ?? vizejs;
|
|
8250
|
+
return Array.isArray(plugin) ? plugin : [plugin];
|
|
8251
|
+
} catch {
|
|
8252
|
+
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");
|
|
8253
|
+
}
|
|
8254
|
+
}
|
|
8255
|
+
/**
|
|
8256
|
+
* Resolves a Svelte SFC template via SSR.
|
|
8257
|
+
*
|
|
8258
|
+
* Compiles the SFC with svelte/compiler (server mode + runes),
|
|
8259
|
+
* bundles with rolldown, then wraps with svelte/server render().
|
|
8260
|
+
*/
|
|
8261
|
+
async function resolveSvelteTemplate(templatePath, root) {
|
|
8262
|
+
const fs = await import("fs/promises");
|
|
8263
|
+
const { rolldown } = await import("rolldown");
|
|
8264
|
+
const cacheDir = path$1.join(root, ".cache", "og-images");
|
|
8265
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
8266
|
+
const outfile = path$1.join(cacheDir, "_template_svelte.mjs");
|
|
8267
|
+
const bundle = await rolldown({
|
|
8268
|
+
input: templatePath,
|
|
8269
|
+
platform: "node",
|
|
8270
|
+
external: [
|
|
8271
|
+
"svelte",
|
|
8272
|
+
"svelte/server",
|
|
8273
|
+
"svelte/internal",
|
|
8274
|
+
"svelte/internal/server"
|
|
8275
|
+
],
|
|
8276
|
+
plugins: [createSvelteCompilerPlugin()]
|
|
8277
|
+
});
|
|
8278
|
+
await bundle.write({
|
|
8279
|
+
file: outfile,
|
|
8280
|
+
format: "esm"
|
|
8281
|
+
});
|
|
8282
|
+
await bundle.close();
|
|
8283
|
+
const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8284
|
+
if (!Component) throw new Error(`[ox-content:og-image] Svelte template must have a default export: ${templatePath}`);
|
|
8285
|
+
const { render } = await import("svelte/server");
|
|
8286
|
+
return async (props) => {
|
|
8287
|
+
const { body } = render(Component, { props });
|
|
8288
|
+
return body;
|
|
8289
|
+
};
|
|
8290
|
+
}
|
|
8291
|
+
/**
|
|
8292
|
+
* Creates a rolldown plugin that compiles Svelte SFCs using svelte/compiler.
|
|
8293
|
+
*/
|
|
8294
|
+
function createSvelteCompilerPlugin() {
|
|
8295
|
+
return {
|
|
8296
|
+
name: "ox-content-svelte-sfc",
|
|
8297
|
+
async transform(code, id) {
|
|
8298
|
+
if (!id.endsWith(".svelte")) return null;
|
|
8299
|
+
let svelteCompiler;
|
|
8300
|
+
try {
|
|
8301
|
+
svelteCompiler = await import("svelte/compiler");
|
|
8302
|
+
} catch {
|
|
8303
|
+
throw new Error("[ox-content:og-image] svelte is required for .svelte templates. Install it with: pnpm add -D svelte");
|
|
8304
|
+
}
|
|
8305
|
+
return { code: svelteCompiler.compile(code, {
|
|
8306
|
+
generate: "server",
|
|
8307
|
+
runes: true,
|
|
8308
|
+
filename: id
|
|
8309
|
+
}).js.code };
|
|
8310
|
+
}
|
|
8311
|
+
};
|
|
8312
|
+
}
|
|
8313
|
+
/**
|
|
8314
|
+
* Resolves a React (.tsx/.jsx) template via SSR.
|
|
8315
|
+
*
|
|
8316
|
+
* Bundles with rolldown (JSX transform), then wraps with
|
|
8317
|
+
* react-dom/server renderToReadableStream for async Server Component support.
|
|
8318
|
+
*/
|
|
8319
|
+
async function resolveReactTemplate(templatePath, root) {
|
|
8320
|
+
const fs = await import("fs/promises");
|
|
8321
|
+
const { rolldown } = await import("rolldown");
|
|
8322
|
+
const cacheDir = path$1.join(root, ".cache", "og-images");
|
|
8323
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
8324
|
+
const outfile = path$1.join(cacheDir, "_template_react.mjs");
|
|
8325
|
+
const bundle = await rolldown({
|
|
8326
|
+
input: templatePath,
|
|
8327
|
+
platform: "node",
|
|
8328
|
+
external: [
|
|
8329
|
+
"react",
|
|
8330
|
+
"react/jsx-runtime",
|
|
8331
|
+
"react/jsx-dev-runtime",
|
|
8332
|
+
"react-dom",
|
|
8333
|
+
"react-dom/server"
|
|
8334
|
+
],
|
|
8335
|
+
transform: { jsx: "react-jsx" }
|
|
8336
|
+
});
|
|
8337
|
+
await bundle.write({
|
|
8338
|
+
file: outfile,
|
|
8339
|
+
format: "esm"
|
|
8340
|
+
});
|
|
8341
|
+
await bundle.close();
|
|
8342
|
+
const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8343
|
+
if (!Component) throw new Error(`[ox-content:og-image] React template must have a default export: ${templatePath}`);
|
|
8344
|
+
let React;
|
|
8345
|
+
let ReactDOMServer;
|
|
8346
|
+
try {
|
|
8347
|
+
React = await import("react");
|
|
8348
|
+
ReactDOMServer = await import("react-dom/server");
|
|
8349
|
+
} catch {
|
|
8350
|
+
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");
|
|
8351
|
+
}
|
|
8352
|
+
return async (props) => {
|
|
8353
|
+
const element = React.createElement(Component, props);
|
|
8354
|
+
const reader = (await ReactDOMServer.renderToReadableStream(element)).getReader();
|
|
8355
|
+
const chunks = [];
|
|
8356
|
+
while (true) {
|
|
8357
|
+
const { done, value } = await reader.read();
|
|
8358
|
+
if (done) break;
|
|
8359
|
+
chunks.push(value);
|
|
8360
|
+
}
|
|
8361
|
+
const decoder = new TextDecoder();
|
|
8362
|
+
return chunks.map((chunk) => decoder.decode(chunk, { stream: true })).join("") + decoder.decode();
|
|
8363
|
+
};
|
|
8364
|
+
}
|
|
8365
|
+
/**
|
|
8366
|
+
* Computes a stable template source identifier for cache keys.
|
|
8367
|
+
*
|
|
8368
|
+
* For custom templates, hashes the file content so cache invalidates
|
|
8369
|
+
* when the template changes. For the default template, returns a fixed string.
|
|
8370
|
+
*/
|
|
8371
|
+
async function computeTemplateSource(options, root) {
|
|
8372
|
+
if (!options.template) return "__default__";
|
|
8373
|
+
const fs = await import("fs/promises");
|
|
8374
|
+
const templatePath = path$1.resolve(root, options.template);
|
|
8375
|
+
const content = await fs.readFile(templatePath, "utf-8");
|
|
8376
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
8377
|
+
}
|
|
8378
|
+
/**
|
|
8379
|
+
* Generates OG images for a batch of pages.
|
|
8380
|
+
*
|
|
8381
|
+
* Manages the full lifecycle: resolve template → launch browser (with `using`) →
|
|
8382
|
+
* render each page (with caching and concurrency).
|
|
8383
|
+
*
|
|
8384
|
+
* All errors are non-fatal: failures are reported in results but never throw.
|
|
8385
|
+
*/
|
|
8386
|
+
async function generateOgImages(pages, options, root) {
|
|
8387
|
+
if (pages.length === 0) return [];
|
|
8388
|
+
const templateFn = await resolveTemplate(options, root);
|
|
8389
|
+
const templateSource = await computeTemplateSource(options, root);
|
|
8390
|
+
const cacheDir = path$1.join(root, ".cache", "og-images");
|
|
8391
|
+
if (options.cache) {
|
|
8392
|
+
const allCached = await tryServeAllFromCache(pages, templateSource, options, cacheDir);
|
|
8393
|
+
if (allCached) return allCached;
|
|
8394
|
+
}
|
|
8395
|
+
await using session = await openBrowser();
|
|
8396
|
+
if (!session) return pages.map((p) => ({
|
|
8397
|
+
outputPath: p.outputPath,
|
|
8398
|
+
cached: false,
|
|
8399
|
+
error: "Chromium not available"
|
|
8400
|
+
}));
|
|
8401
|
+
const results = [];
|
|
8402
|
+
const concurrency = Math.max(1, options.concurrency);
|
|
8403
|
+
for (let i = 0; i < pages.length; i += concurrency) {
|
|
8404
|
+
const batch = pages.slice(i, i + concurrency);
|
|
8405
|
+
const batchResults = await Promise.all(batch.map((entry) => renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session)));
|
|
8406
|
+
results.push(...batchResults);
|
|
8407
|
+
}
|
|
8408
|
+
return results;
|
|
8409
|
+
}
|
|
8410
|
+
/**
|
|
8411
|
+
* Tries to serve all pages from cache.
|
|
8412
|
+
* Returns results if ALL pages are cached, null otherwise.
|
|
8413
|
+
*/
|
|
8414
|
+
async function tryServeAllFromCache(pages, templateSource, options, cacheDir) {
|
|
8415
|
+
const fs = await import("fs/promises");
|
|
8416
|
+
const results = [];
|
|
8417
|
+
for (const entry of pages) {
|
|
8418
|
+
const cached = await getCached(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height));
|
|
8419
|
+
if (!cached) return null;
|
|
8420
|
+
await fs.mkdir(path$1.dirname(entry.outputPath), { recursive: true });
|
|
8421
|
+
await fs.writeFile(entry.outputPath, cached);
|
|
8422
|
+
results.push({
|
|
8423
|
+
outputPath: entry.outputPath,
|
|
8424
|
+
cached: true
|
|
8425
|
+
});
|
|
8426
|
+
}
|
|
8427
|
+
return results;
|
|
8428
|
+
}
|
|
8429
|
+
/**
|
|
8430
|
+
* Renders a single page to PNG, with cache support.
|
|
8431
|
+
*/
|
|
8432
|
+
async function renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session) {
|
|
8433
|
+
const fs = await import("fs/promises");
|
|
8434
|
+
try {
|
|
8435
|
+
if (options.cache) {
|
|
8436
|
+
const cached = await getCached(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height));
|
|
8437
|
+
if (cached) {
|
|
8438
|
+
await fs.mkdir(path$1.dirname(entry.outputPath), { recursive: true });
|
|
8439
|
+
await fs.writeFile(entry.outputPath, cached);
|
|
8440
|
+
return {
|
|
8441
|
+
outputPath: entry.outputPath,
|
|
8442
|
+
cached: true
|
|
8443
|
+
};
|
|
8444
|
+
}
|
|
8445
|
+
}
|
|
8446
|
+
const html = await templateFn(entry.props);
|
|
8447
|
+
const png = await session.renderPage(html, options.width, options.height);
|
|
8448
|
+
await fs.mkdir(path$1.dirname(entry.outputPath), { recursive: true });
|
|
8449
|
+
await fs.writeFile(entry.outputPath, png);
|
|
8450
|
+
if (options.cache) await writeCache(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height), png);
|
|
8451
|
+
return {
|
|
8452
|
+
outputPath: entry.outputPath,
|
|
8453
|
+
cached: false
|
|
8454
|
+
};
|
|
8455
|
+
} catch (err) {
|
|
8456
|
+
return {
|
|
8457
|
+
outputPath: entry.outputPath,
|
|
8458
|
+
cached: false,
|
|
8459
|
+
error: err instanceof Error ? err.message : String(err)
|
|
8460
|
+
};
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
8463
|
+
|
|
7967
8464
|
//#endregion
|
|
7968
8465
|
//#region src/plugins/index.ts
|
|
7969
8466
|
/**
|
|
@@ -9267,9 +9764,9 @@ function getOgImagePath(inputPath, srcDir, outDir) {
|
|
|
9267
9764
|
const baseName = path$1.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
|
|
9268
9765
|
if (baseName === "index" || baseName.endsWith("/index")) {
|
|
9269
9766
|
const dirPath = baseName.replace(/\/?index$/, "") || "";
|
|
9270
|
-
return path$1.join(outDir, dirPath, "og-image.
|
|
9767
|
+
return path$1.join(outDir, dirPath, "og-image.png");
|
|
9271
9768
|
}
|
|
9272
|
-
return path$1.join(outDir, baseName, "og-image.
|
|
9769
|
+
return path$1.join(outDir, baseName, "og-image.png");
|
|
9273
9770
|
}
|
|
9274
9771
|
/**
|
|
9275
9772
|
* Gets the OG image URL for use in meta tags.
|
|
@@ -9278,8 +9775,8 @@ function getOgImagePath(inputPath, srcDir, outDir) {
|
|
|
9278
9775
|
function getOgImageUrl(inputPath, srcDir, base, siteUrl) {
|
|
9279
9776
|
const urlPath = getUrlPath(inputPath, srcDir);
|
|
9280
9777
|
let relativePath;
|
|
9281
|
-
if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.
|
|
9282
|
-
else relativePath = `${base}${urlPath}/og-image.
|
|
9778
|
+
if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.png`;
|
|
9779
|
+
else relativePath = `${base}${urlPath}/og-image.png`;
|
|
9283
9780
|
if (siteUrl) return `${siteUrl.replace(/\/$/, "")}${relativePath}`;
|
|
9284
9781
|
return relativePath;
|
|
9285
9782
|
}
|
|
@@ -9394,6 +9891,10 @@ async function buildSsg(options, root) {
|
|
|
9394
9891
|
const pkg = JSON.parse(await fs_promises.readFile(pkgPath, "utf-8"));
|
|
9395
9892
|
if (pkg.name) siteName = formatTitle(pkg.name);
|
|
9396
9893
|
} catch {}
|
|
9894
|
+
const ogImageEntries = [];
|
|
9895
|
+
const ogImageUrlMap = /* @__PURE__ */ new Map();
|
|
9896
|
+
const shouldGenerateOgImages = (options.ogImage || ssgOptions.generateOgImage) && !ssgOptions.bare;
|
|
9897
|
+
const pageResults = [];
|
|
9397
9898
|
for (const inputPath of markdownFiles) try {
|
|
9398
9899
|
const result = await transformMarkdown(await fs_promises.readFile(inputPath, "utf-8"), inputPath, options, {
|
|
9399
9900
|
convertMdLinks: true,
|
|
@@ -9416,31 +9917,56 @@ async function buildSsg(options, root) {
|
|
|
9416
9917
|
transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
|
|
9417
9918
|
const title = extractTitle(transformedHtml, result.frontmatter);
|
|
9418
9919
|
const description = result.frontmatter.description;
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
}
|
|
9920
|
+
pageResults.push({
|
|
9921
|
+
inputPath,
|
|
9922
|
+
transformedHtml,
|
|
9923
|
+
title,
|
|
9924
|
+
description,
|
|
9925
|
+
frontmatter: result.frontmatter,
|
|
9926
|
+
toc: result.toc
|
|
9927
|
+
});
|
|
9928
|
+
if (shouldGenerateOgImages) {
|
|
9929
|
+
const ogImageOutputPath = getOgImagePath(inputPath, srcDir, outDir);
|
|
9930
|
+
const { layout: _layout, ...frontmatterRest } = result.frontmatter;
|
|
9931
|
+
ogImageEntries.push({
|
|
9932
|
+
props: {
|
|
9933
|
+
...frontmatterRest,
|
|
9934
|
+
title,
|
|
9935
|
+
description,
|
|
9936
|
+
siteName
|
|
9937
|
+
},
|
|
9938
|
+
outputPath: ogImageOutputPath
|
|
9939
|
+
});
|
|
9940
|
+
ogImageUrlMap.set(inputPath, getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl));
|
|
9439
9941
|
}
|
|
9942
|
+
} catch (err) {
|
|
9943
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
9944
|
+
errors.push(`Failed to process ${inputPath}: ${errorMessage}`);
|
|
9945
|
+
}
|
|
9946
|
+
if (shouldGenerateOgImages && ogImageEntries.length > 0) try {
|
|
9947
|
+
const ogResults = await generateOgImages(ogImageEntries, options.ogImageOptions, root);
|
|
9948
|
+
let ogSuccessCount = 0;
|
|
9949
|
+
for (const result of ogResults) if (result.error) errors.push(`OG image failed for ${result.outputPath}: ${result.error}`);
|
|
9950
|
+
else {
|
|
9951
|
+
generatedFiles.push(result.outputPath);
|
|
9952
|
+
ogSuccessCount++;
|
|
9953
|
+
}
|
|
9954
|
+
if (ogSuccessCount > 0) {
|
|
9955
|
+
const cachedCount = ogResults.filter((r) => r.cached && !r.error).length;
|
|
9956
|
+
console.log(`[ox-content:og-image] Generated ${ogSuccessCount} OG images` + (cachedCount > 0 ? ` (${cachedCount} from cache)` : ""));
|
|
9957
|
+
}
|
|
9958
|
+
} catch (err) {
|
|
9959
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
9960
|
+
console.warn(`[ox-content:og-image] Batch generation failed: ${errorMessage}`);
|
|
9961
|
+
}
|
|
9962
|
+
for (const pageResult of pageResults) try {
|
|
9963
|
+
const { inputPath, transformedHtml, title, description, frontmatter, toc } = pageResult;
|
|
9964
|
+
let pageOgImage = ssgOptions.ogImage;
|
|
9965
|
+
if (shouldGenerateOgImages && ogImageUrlMap.has(inputPath)) pageOgImage = ogImageUrlMap.get(inputPath);
|
|
9440
9966
|
let entryPage;
|
|
9441
|
-
if (
|
|
9442
|
-
hero:
|
|
9443
|
-
features:
|
|
9967
|
+
if (frontmatter.layout === "entry") entryPage = {
|
|
9968
|
+
hero: frontmatter.hero,
|
|
9969
|
+
features: frontmatter.features
|
|
9444
9970
|
};
|
|
9445
9971
|
let html;
|
|
9446
9972
|
if (ssgOptions.bare) html = generateBareHtmlPage(transformedHtml, title);
|
|
@@ -9448,8 +9974,8 @@ async function buildSsg(options, root) {
|
|
|
9448
9974
|
title,
|
|
9449
9975
|
description,
|
|
9450
9976
|
content: transformedHtml,
|
|
9451
|
-
toc
|
|
9452
|
-
frontmatter
|
|
9977
|
+
toc,
|
|
9978
|
+
frontmatter,
|
|
9453
9979
|
path: getUrlPath(inputPath, srcDir),
|
|
9454
9980
|
href: getHref(inputPath, srcDir, base, ssgOptions.extension),
|
|
9455
9981
|
entryPage
|
|
@@ -9460,7 +9986,7 @@ async function buildSsg(options, root) {
|
|
|
9460
9986
|
generatedFiles.push(outputPath);
|
|
9461
9987
|
} catch (err) {
|
|
9462
9988
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
9463
|
-
errors.push(`Failed to
|
|
9989
|
+
errors.push(`Failed to generate HTML for ${pageResult.inputPath}: ${errorMessage}`);
|
|
9464
9990
|
}
|
|
9465
9991
|
return {
|
|
9466
9992
|
files: generatedFiles,
|
|
@@ -10481,7 +11007,7 @@ function resolveOptions(options) {
|
|
|
10481
11007
|
toc: options.toc ?? true,
|
|
10482
11008
|
tocMaxDepth: options.tocMaxDepth ?? 3,
|
|
10483
11009
|
ogImage: options.ogImage ?? false,
|
|
10484
|
-
ogImageOptions: options.ogImageOptions
|
|
11010
|
+
ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
|
|
10485
11011
|
transformers: options.transformers ?? [],
|
|
10486
11012
|
docs: resolveDocsOptions(options.docs),
|
|
10487
11013
|
search: resolveSearchOptions(options.search)
|
|
@@ -10527,6 +11053,7 @@ exports.fetchRepoData = require_github.fetchRepoData;
|
|
|
10527
11053
|
exports.generateFrontmatterTypes = generateFrontmatterTypes;
|
|
10528
11054
|
exports.generateHydrationScript = generateHydrationScript;
|
|
10529
11055
|
exports.generateMarkdown = generateMarkdown;
|
|
11056
|
+
exports.generateOgImages = generateOgImages;
|
|
10530
11057
|
exports.generateTabsCSS = require_tabs.generateTabsCSS;
|
|
10531
11058
|
exports.generateTypes = generateTypes;
|
|
10532
11059
|
exports.hasIslands = hasIslands;
|
|
@@ -10543,6 +11070,7 @@ exports.renderAllPages = renderAllPages;
|
|
|
10543
11070
|
exports.renderPage = renderPage;
|
|
10544
11071
|
exports.renderToString = renderToString;
|
|
10545
11072
|
exports.resolveDocsOptions = resolveDocsOptions;
|
|
11073
|
+
exports.resolveOgImageOptions = resolveOgImageOptions;
|
|
10546
11074
|
exports.resolveSearchOptions = resolveSearchOptions;
|
|
10547
11075
|
exports.resolveSsgOptions = resolveSsgOptions;
|
|
10548
11076
|
exports.resolveTheme = resolveTheme;
|