@ox-content/vite-plugin 0.3.0-alpha.13 → 0.3.0-alpha.15

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 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,592 @@ 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 \0@oxc-project+runtime@0.110.0/helpers/usingCtx.js
8107
+ function _usingCtx() {
8108
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
8109
+ var n = Error();
8110
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
8111
+ }, e = {}, n = [];
8112
+ function using(r, e) {
8113
+ if (null != e) {
8114
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
8115
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
8116
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
8117
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
8118
+ t && (o = function o() {
8119
+ try {
8120
+ t.call(e);
8121
+ } catch (r) {
8122
+ return Promise.reject(r);
8123
+ }
8124
+ }), n.push({
8125
+ v: e,
8126
+ d: o,
8127
+ a: r
8128
+ });
8129
+ } else r && n.push({
8130
+ d: e,
8131
+ a: r
8132
+ });
8133
+ return e;
8134
+ }
8135
+ return {
8136
+ e,
8137
+ u: using.bind(null, !1),
8138
+ a: using.bind(null, !0),
8139
+ d: function d() {
8140
+ var o, t = this.e, s = 0;
8141
+ function next() {
8142
+ for (; o = n.pop();) try {
8143
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
8144
+ if (o.d) {
8145
+ var r = o.d.call(o.v);
8146
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
8147
+ } else s |= 1;
8148
+ } catch (r) {
8149
+ return err(r);
8150
+ }
8151
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
8152
+ if (t !== e) throw t;
8153
+ }
8154
+ function err(n) {
8155
+ return t = t !== e ? new r(n, t) : n, next();
8156
+ }
8157
+ return next();
8158
+ }
8159
+ };
8160
+ }
8161
+
8162
+ //#endregion
8163
+ //#region src/og-image/index.ts
8164
+ /**
8165
+ * Public API for Chromium-based OG image generation.
8166
+ *
8167
+ * Orchestrates browser lifecycle, template resolution, caching,
8168
+ * and batch rendering with concurrency control.
8169
+ */
8170
+ /**
8171
+ * Resolves user-provided OG image options with defaults.
8172
+ */
8173
+ function resolveOgImageOptions(options) {
8174
+ return {
8175
+ template: options?.template,
8176
+ vuePlugin: options?.vuePlugin ?? "vitejs",
8177
+ width: options?.width ?? 1200,
8178
+ height: options?.height ?? 630,
8179
+ cache: options?.cache ?? true,
8180
+ concurrency: options?.concurrency ?? 1
8181
+ };
8182
+ }
8183
+ /**
8184
+ * Resolves the template function from options.
8185
+ *
8186
+ * Dispatches by file extension:
8187
+ * - `.vue` → Vue SFC (SSR via vue/server-renderer)
8188
+ * - `.svelte` → Svelte SFC (SSR via svelte/server)
8189
+ * - `.tsx`/`.jsx` → React Server Component (SSR via react-dom/server)
8190
+ * - others → TypeScript template (direct function export)
8191
+ */
8192
+ async function resolveTemplate(options, root) {
8193
+ if (!options.template) return getDefaultTemplate();
8194
+ const templatePath = path$1.resolve(root, options.template);
8195
+ const fs = await import("fs/promises");
8196
+ try {
8197
+ await fs.access(templatePath);
8198
+ } catch {
8199
+ throw new Error(`[ox-content:og-image] Template file not found: ${templatePath}`);
8200
+ }
8201
+ switch (path$1.extname(templatePath).toLowerCase()) {
8202
+ case ".vue": return resolveVueTemplate(templatePath, options, root);
8203
+ case ".svelte": return resolveSvelteTemplate(templatePath, root);
8204
+ case ".tsx":
8205
+ case ".jsx": return resolveReactTemplate(templatePath, root);
8206
+ default: return resolveTsTemplate(templatePath, options, root);
8207
+ }
8208
+ }
8209
+ /**
8210
+ * Resolves a plain TypeScript template (existing behavior).
8211
+ */
8212
+ async function resolveTsTemplate(templatePath, options, root) {
8213
+ const fs = await import("fs/promises");
8214
+ const { rolldown } = await import("rolldown");
8215
+ const cacheDir = path$1.join(root, ".cache", "og-images");
8216
+ await fs.mkdir(cacheDir, { recursive: true });
8217
+ const outfile = path$1.join(cacheDir, "_template.mjs");
8218
+ const bundle = await rolldown({
8219
+ input: templatePath,
8220
+ platform: "node"
8221
+ });
8222
+ await bundle.write({
8223
+ file: outfile,
8224
+ format: "esm"
8225
+ });
8226
+ await bundle.close();
8227
+ const templateFn = (await import(`${outfile}?t=${Date.now()}`)).default;
8228
+ if (typeof templateFn !== "function") throw new Error(`[ox-content:og-image] Template must default-export a function: ${options.template}`);
8229
+ return templateFn;
8230
+ }
8231
+ /**
8232
+ * Resolves a Vue SFC template via SSR.
8233
+ *
8234
+ * Compiles the SFC with @vue/compiler-sfc (or @vizejs/vite-plugin),
8235
+ * bundles with rolldown, then wraps with createSSRApp + renderToString.
8236
+ */
8237
+ async function resolveVueTemplate(templatePath, options, root) {
8238
+ const fs = await import("fs/promises");
8239
+ const { rolldown } = await import("rolldown");
8240
+ const cacheDir = path$1.join(root, ".cache", "og-images");
8241
+ await fs.mkdir(cacheDir, { recursive: true });
8242
+ const outfile = path$1.join(cacheDir, "_template_vue.mjs");
8243
+ const bundle = await rolldown({
8244
+ input: templatePath,
8245
+ platform: "node",
8246
+ external: ["vue", "vue/server-renderer"],
8247
+ plugins: options.vuePlugin === "vizejs" ? await getVizejsPlugin() : [createVueCompilerPlugin()]
8248
+ });
8249
+ await bundle.write({
8250
+ file: outfile,
8251
+ format: "esm"
8252
+ });
8253
+ await bundle.close();
8254
+ const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
8255
+ if (!Component) throw new Error(`[ox-content:og-image] Vue template must have a default export: ${templatePath}`);
8256
+ const { createSSRApp } = await import("vue");
8257
+ const { renderToString } = await import("vue/server-renderer");
8258
+ return async (props) => {
8259
+ return renderToString(createSSRApp(Component, props));
8260
+ };
8261
+ }
8262
+ /**
8263
+ * Creates a rolldown plugin that compiles Vue SFCs using @vue/compiler-sfc.
8264
+ */
8265
+ function createVueCompilerPlugin() {
8266
+ return {
8267
+ name: "ox-content-vue-sfc",
8268
+ async transform(code, id) {
8269
+ if (!id.endsWith(".vue")) return null;
8270
+ let compilerSfc;
8271
+ try {
8272
+ compilerSfc = await import("@vue/compiler-sfc");
8273
+ } catch {
8274
+ throw new Error("[ox-content:og-image] @vue/compiler-sfc is required for .vue templates. Install it with: pnpm add -D @vue/compiler-sfc");
8275
+ }
8276
+ const { descriptor } = compilerSfc.parse(code, { filename: id });
8277
+ let scriptCode;
8278
+ if (descriptor.scriptSetup || descriptor.script) scriptCode = compilerSfc.compileScript(descriptor, {
8279
+ id,
8280
+ inlineTemplate: true
8281
+ }).content;
8282
+ else {
8283
+ if (!descriptor.template) throw new Error(`[ox-content:og-image] Vue SFC must have a <template> or <script>: ${id}`);
8284
+ const templateResult = compilerSfc.compileTemplate({
8285
+ source: descriptor.template.content,
8286
+ filename: id,
8287
+ id
8288
+ });
8289
+ if (templateResult.errors.length > 0) throw new Error(`[ox-content:og-image] Vue template compilation errors in ${id}: ${templateResult.errors.join(", ")}`);
8290
+ scriptCode = `${templateResult.code}\nexport default { render }`;
8291
+ }
8292
+ const isTs = !!(descriptor.scriptSetup?.lang === "ts" || descriptor.script?.lang === "ts");
8293
+ return {
8294
+ code: scriptCode,
8295
+ moduleType: isTs ? "ts" : "js"
8296
+ };
8297
+ }
8298
+ };
8299
+ }
8300
+ /**
8301
+ * Loads @vizejs/vite-plugin as a rolldown plugin for Vue SFC compilation.
8302
+ */
8303
+ async function getVizejsPlugin() {
8304
+ try {
8305
+ const vizejs = await import("@vizejs/vite-plugin");
8306
+ const plugin = vizejs.default?.() ?? vizejs;
8307
+ return Array.isArray(plugin) ? plugin : [plugin];
8308
+ } catch {
8309
+ 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");
8310
+ }
8311
+ }
8312
+ /**
8313
+ * Resolves a Svelte SFC template via SSR.
8314
+ *
8315
+ * Compiles the SFC with svelte/compiler (server mode + runes),
8316
+ * bundles with rolldown, then wraps with svelte/server render().
8317
+ */
8318
+ async function resolveSvelteTemplate(templatePath, root) {
8319
+ const fs = await import("fs/promises");
8320
+ const { rolldown } = await import("rolldown");
8321
+ const cacheDir = path$1.join(root, ".cache", "og-images");
8322
+ await fs.mkdir(cacheDir, { recursive: true });
8323
+ const outfile = path$1.join(cacheDir, "_template_svelte.mjs");
8324
+ const bundle = await rolldown({
8325
+ input: templatePath,
8326
+ platform: "node",
8327
+ external: [
8328
+ "svelte",
8329
+ "svelte/server",
8330
+ "svelte/internal",
8331
+ "svelte/internal/server"
8332
+ ],
8333
+ plugins: [createSvelteCompilerPlugin()]
8334
+ });
8335
+ await bundle.write({
8336
+ file: outfile,
8337
+ format: "esm"
8338
+ });
8339
+ await bundle.close();
8340
+ const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
8341
+ if (!Component) throw new Error(`[ox-content:og-image] Svelte template must have a default export: ${templatePath}`);
8342
+ const { render } = await import("svelte/server");
8343
+ return async (props) => {
8344
+ const { body } = render(Component, { props });
8345
+ return body;
8346
+ };
8347
+ }
8348
+ /**
8349
+ * Creates a rolldown plugin that compiles Svelte SFCs using svelte/compiler.
8350
+ */
8351
+ function createSvelteCompilerPlugin() {
8352
+ return {
8353
+ name: "ox-content-svelte-sfc",
8354
+ async transform(code, id) {
8355
+ if (!id.endsWith(".svelte")) return null;
8356
+ let svelteCompiler;
8357
+ try {
8358
+ svelteCompiler = await import("svelte/compiler");
8359
+ } catch {
8360
+ throw new Error("[ox-content:og-image] svelte is required for .svelte templates. Install it with: pnpm add -D svelte");
8361
+ }
8362
+ return { code: svelteCompiler.compile(code, {
8363
+ generate: "server",
8364
+ runes: true,
8365
+ filename: id
8366
+ }).js.code };
8367
+ }
8368
+ };
8369
+ }
8370
+ /**
8371
+ * Resolves a React (.tsx/.jsx) template via SSR.
8372
+ *
8373
+ * Bundles with rolldown (JSX transform), then wraps with
8374
+ * react-dom/server renderToReadableStream for async Server Component support.
8375
+ */
8376
+ async function resolveReactTemplate(templatePath, root) {
8377
+ const fs = await import("fs/promises");
8378
+ const { rolldown } = await import("rolldown");
8379
+ const cacheDir = path$1.join(root, ".cache", "og-images");
8380
+ await fs.mkdir(cacheDir, { recursive: true });
8381
+ const outfile = path$1.join(cacheDir, "_template_react.mjs");
8382
+ const bundle = await rolldown({
8383
+ input: templatePath,
8384
+ platform: "node",
8385
+ external: [
8386
+ "react",
8387
+ "react/jsx-runtime",
8388
+ "react/jsx-dev-runtime",
8389
+ "react-dom",
8390
+ "react-dom/server"
8391
+ ],
8392
+ transform: { jsx: "react-jsx" }
8393
+ });
8394
+ await bundle.write({
8395
+ file: outfile,
8396
+ format: "esm"
8397
+ });
8398
+ await bundle.close();
8399
+ const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
8400
+ if (!Component) throw new Error(`[ox-content:og-image] React template must have a default export: ${templatePath}`);
8401
+ let React;
8402
+ let ReactDOMServer;
8403
+ try {
8404
+ React = await import("react");
8405
+ ReactDOMServer = await import("react-dom/server");
8406
+ } catch {
8407
+ 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");
8408
+ }
8409
+ return async (props) => {
8410
+ const element = React.createElement(Component, props);
8411
+ const reader = (await ReactDOMServer.renderToReadableStream(element)).getReader();
8412
+ const chunks = [];
8413
+ while (true) {
8414
+ const { done, value } = await reader.read();
8415
+ if (done) break;
8416
+ chunks.push(value);
8417
+ }
8418
+ const decoder = new TextDecoder();
8419
+ return chunks.map((chunk) => decoder.decode(chunk, { stream: true })).join("") + decoder.decode();
8420
+ };
8421
+ }
8422
+ /**
8423
+ * Computes a stable template source identifier for cache keys.
8424
+ *
8425
+ * For custom templates, hashes the file content so cache invalidates
8426
+ * when the template changes. For the default template, returns a fixed string.
8427
+ */
8428
+ async function computeTemplateSource(options, root) {
8429
+ if (!options.template) return "__default__";
8430
+ const fs = await import("fs/promises");
8431
+ const templatePath = path$1.resolve(root, options.template);
8432
+ const content = await fs.readFile(templatePath, "utf-8");
8433
+ return crypto.createHash("sha256").update(content).digest("hex");
8434
+ }
8435
+ /**
8436
+ * Generates OG images for a batch of pages.
8437
+ *
8438
+ * Manages the full lifecycle: resolve template → launch browser (with `using`) →
8439
+ * render each page (with caching and concurrency).
8440
+ *
8441
+ * All errors are non-fatal: failures are reported in results but never throw.
8442
+ */
8443
+ async function generateOgImages(pages, options, root) {
8444
+ try {
8445
+ var _usingCtx$1 = _usingCtx();
8446
+ if (pages.length === 0) return [];
8447
+ const templateFn = await resolveTemplate(options, root);
8448
+ const templateSource = await computeTemplateSource(options, root);
8449
+ const cacheDir = path$1.join(root, ".cache", "og-images");
8450
+ if (options.cache) {
8451
+ const allCached = await tryServeAllFromCache(pages, templateSource, options, cacheDir);
8452
+ if (allCached) return allCached;
8453
+ }
8454
+ const session = _usingCtx$1.a(await openBrowser());
8455
+ if (!session) return pages.map((p) => ({
8456
+ outputPath: p.outputPath,
8457
+ cached: false,
8458
+ error: "Chromium not available"
8459
+ }));
8460
+ const results = [];
8461
+ const concurrency = Math.max(1, options.concurrency);
8462
+ for (let i = 0; i < pages.length; i += concurrency) {
8463
+ const batch = pages.slice(i, i + concurrency);
8464
+ const batchResults = await Promise.all(batch.map((entry) => renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session)));
8465
+ results.push(...batchResults);
8466
+ }
8467
+ return results;
8468
+ } catch (_) {
8469
+ _usingCtx$1.e = _;
8470
+ } finally {
8471
+ await _usingCtx$1.d();
8472
+ }
8473
+ }
8474
+ /**
8475
+ * Tries to serve all pages from cache.
8476
+ * Returns results if ALL pages are cached, null otherwise.
8477
+ */
8478
+ async function tryServeAllFromCache(pages, templateSource, options, cacheDir) {
8479
+ const fs = await import("fs/promises");
8480
+ const results = [];
8481
+ for (const entry of pages) {
8482
+ const cached = await getCached(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height));
8483
+ if (!cached) return null;
8484
+ await fs.mkdir(path$1.dirname(entry.outputPath), { recursive: true });
8485
+ await fs.writeFile(entry.outputPath, cached);
8486
+ results.push({
8487
+ outputPath: entry.outputPath,
8488
+ cached: true
8489
+ });
8490
+ }
8491
+ return results;
8492
+ }
8493
+ /**
8494
+ * Renders a single page to PNG, with cache support.
8495
+ */
8496
+ async function renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session) {
8497
+ const fs = await import("fs/promises");
8498
+ try {
8499
+ if (options.cache) {
8500
+ const cached = await getCached(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height));
8501
+ if (cached) {
8502
+ await fs.mkdir(path$1.dirname(entry.outputPath), { recursive: true });
8503
+ await fs.writeFile(entry.outputPath, cached);
8504
+ return {
8505
+ outputPath: entry.outputPath,
8506
+ cached: true
8507
+ };
8508
+ }
8509
+ }
8510
+ const html = await templateFn(entry.props);
8511
+ const png = await session.renderPage(html, options.width, options.height);
8512
+ await fs.mkdir(path$1.dirname(entry.outputPath), { recursive: true });
8513
+ await fs.writeFile(entry.outputPath, png);
8514
+ if (options.cache) await writeCache(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height), png);
8515
+ return {
8516
+ outputPath: entry.outputPath,
8517
+ cached: false
8518
+ };
8519
+ } catch (err) {
8520
+ return {
8521
+ outputPath: entry.outputPath,
8522
+ cached: false,
8523
+ error: err instanceof Error ? err.message : String(err)
8524
+ };
8525
+ }
8526
+ }
8527
+
7967
8528
  //#endregion
7968
8529
  //#region src/plugins/index.ts
7969
8530
  /**
@@ -9267,9 +9828,9 @@ function getOgImagePath(inputPath, srcDir, outDir) {
9267
9828
  const baseName = path$1.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
9268
9829
  if (baseName === "index" || baseName.endsWith("/index")) {
9269
9830
  const dirPath = baseName.replace(/\/?index$/, "") || "";
9270
- return path$1.join(outDir, dirPath, "og-image.svg");
9831
+ return path$1.join(outDir, dirPath, "og-image.png");
9271
9832
  }
9272
- return path$1.join(outDir, baseName, "og-image.svg");
9833
+ return path$1.join(outDir, baseName, "og-image.png");
9273
9834
  }
9274
9835
  /**
9275
9836
  * Gets the OG image URL for use in meta tags.
@@ -9278,8 +9839,8 @@ function getOgImagePath(inputPath, srcDir, outDir) {
9278
9839
  function getOgImageUrl(inputPath, srcDir, base, siteUrl) {
9279
9840
  const urlPath = getUrlPath(inputPath, srcDir);
9280
9841
  let relativePath;
9281
- if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.svg`;
9282
- else relativePath = `${base}${urlPath}/og-image.svg`;
9842
+ if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.png`;
9843
+ else relativePath = `${base}${urlPath}/og-image.png`;
9283
9844
  if (siteUrl) return `${siteUrl.replace(/\/$/, "")}${relativePath}`;
9284
9845
  return relativePath;
9285
9846
  }
@@ -9394,6 +9955,10 @@ async function buildSsg(options, root) {
9394
9955
  const pkg = JSON.parse(await fs_promises.readFile(pkgPath, "utf-8"));
9395
9956
  if (pkg.name) siteName = formatTitle(pkg.name);
9396
9957
  } catch {}
9958
+ const ogImageEntries = [];
9959
+ const ogImageUrlMap = /* @__PURE__ */ new Map();
9960
+ const shouldGenerateOgImages = (options.ogImage || ssgOptions.generateOgImage) && !ssgOptions.bare;
9961
+ const pageResults = [];
9397
9962
  for (const inputPath of markdownFiles) try {
9398
9963
  const result = await transformMarkdown(await fs_promises.readFile(inputPath, "utf-8"), inputPath, options, {
9399
9964
  convertMdLinks: true,
@@ -9416,31 +9981,56 @@ async function buildSsg(options, root) {
9416
9981
  transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
9417
9982
  const title = extractTitle(transformedHtml, result.frontmatter);
9418
9983
  const description = result.frontmatter.description;
9419
- let pageOgImage = ssgOptions.ogImage;
9420
- if (ssgOptions.generateOgImage && !ssgOptions.bare) {
9421
- const svg = await generateOgImageSvg({
9422
- title,
9423
- description,
9424
- siteName,
9425
- author: result.frontmatter.author
9426
- }, options.ogImageOptions ? {
9427
- width: options.ogImageOptions.width,
9428
- height: options.ogImageOptions.height,
9429
- backgroundColor: options.ogImageOptions.background,
9430
- textColor: options.ogImageOptions.textColor
9431
- } : void 0);
9432
- if (svg) {
9433
- const ogImageOutputPath = getOgImagePath(inputPath, srcDir, outDir);
9434
- await fs_promises.mkdir(path$1.dirname(ogImageOutputPath), { recursive: true });
9435
- await fs_promises.writeFile(ogImageOutputPath, svg, "utf-8");
9436
- generatedFiles.push(ogImageOutputPath);
9437
- pageOgImage = getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl);
9438
- }
9984
+ pageResults.push({
9985
+ inputPath,
9986
+ transformedHtml,
9987
+ title,
9988
+ description,
9989
+ frontmatter: result.frontmatter,
9990
+ toc: result.toc
9991
+ });
9992
+ if (shouldGenerateOgImages) {
9993
+ const ogImageOutputPath = getOgImagePath(inputPath, srcDir, outDir);
9994
+ const { layout: _layout, ...frontmatterRest } = result.frontmatter;
9995
+ ogImageEntries.push({
9996
+ props: {
9997
+ ...frontmatterRest,
9998
+ title,
9999
+ description,
10000
+ siteName
10001
+ },
10002
+ outputPath: ogImageOutputPath
10003
+ });
10004
+ ogImageUrlMap.set(inputPath, getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl));
9439
10005
  }
10006
+ } catch (err) {
10007
+ const errorMessage = err instanceof Error ? err.message : String(err);
10008
+ errors.push(`Failed to process ${inputPath}: ${errorMessage}`);
10009
+ }
10010
+ if (shouldGenerateOgImages && ogImageEntries.length > 0) try {
10011
+ const ogResults = await generateOgImages(ogImageEntries, options.ogImageOptions, root);
10012
+ let ogSuccessCount = 0;
10013
+ for (const result of ogResults) if (result.error) errors.push(`OG image failed for ${result.outputPath}: ${result.error}`);
10014
+ else {
10015
+ generatedFiles.push(result.outputPath);
10016
+ ogSuccessCount++;
10017
+ }
10018
+ if (ogSuccessCount > 0) {
10019
+ const cachedCount = ogResults.filter((r) => r.cached && !r.error).length;
10020
+ console.log(`[ox-content:og-image] Generated ${ogSuccessCount} OG images` + (cachedCount > 0 ? ` (${cachedCount} from cache)` : ""));
10021
+ }
10022
+ } catch (err) {
10023
+ const errorMessage = err instanceof Error ? err.message : String(err);
10024
+ console.warn(`[ox-content:og-image] Batch generation failed: ${errorMessage}`);
10025
+ }
10026
+ for (const pageResult of pageResults) try {
10027
+ const { inputPath, transformedHtml, title, description, frontmatter, toc } = pageResult;
10028
+ let pageOgImage = ssgOptions.ogImage;
10029
+ if (shouldGenerateOgImages && ogImageUrlMap.has(inputPath)) pageOgImage = ogImageUrlMap.get(inputPath);
9440
10030
  let entryPage;
9441
- if (result.frontmatter.layout === "entry") entryPage = {
9442
- hero: result.frontmatter.hero,
9443
- features: result.frontmatter.features
10031
+ if (frontmatter.layout === "entry") entryPage = {
10032
+ hero: frontmatter.hero,
10033
+ features: frontmatter.features
9444
10034
  };
9445
10035
  let html;
9446
10036
  if (ssgOptions.bare) html = generateBareHtmlPage(transformedHtml, title);
@@ -9448,8 +10038,8 @@ async function buildSsg(options, root) {
9448
10038
  title,
9449
10039
  description,
9450
10040
  content: transformedHtml,
9451
- toc: result.toc,
9452
- frontmatter: result.frontmatter,
10041
+ toc,
10042
+ frontmatter,
9453
10043
  path: getUrlPath(inputPath, srcDir),
9454
10044
  href: getHref(inputPath, srcDir, base, ssgOptions.extension),
9455
10045
  entryPage
@@ -9460,7 +10050,7 @@ async function buildSsg(options, root) {
9460
10050
  generatedFiles.push(outputPath);
9461
10051
  } catch (err) {
9462
10052
  const errorMessage = err instanceof Error ? err.message : String(err);
9463
- errors.push(`Failed to process ${inputPath}: ${errorMessage}`);
10053
+ errors.push(`Failed to generate HTML for ${pageResult.inputPath}: ${errorMessage}`);
9464
10054
  }
9465
10055
  return {
9466
10056
  files: generatedFiles,
@@ -10481,7 +11071,7 @@ function resolveOptions(options) {
10481
11071
  toc: options.toc ?? true,
10482
11072
  tocMaxDepth: options.tocMaxDepth ?? 3,
10483
11073
  ogImage: options.ogImage ?? false,
10484
- ogImageOptions: options.ogImageOptions ?? {},
11074
+ ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
10485
11075
  transformers: options.transformers ?? [],
10486
11076
  docs: resolveDocsOptions(options.docs),
10487
11077
  search: resolveSearchOptions(options.search)
@@ -10527,6 +11117,7 @@ exports.fetchRepoData = require_github.fetchRepoData;
10527
11117
  exports.generateFrontmatterTypes = generateFrontmatterTypes;
10528
11118
  exports.generateHydrationScript = generateHydrationScript;
10529
11119
  exports.generateMarkdown = generateMarkdown;
11120
+ exports.generateOgImages = generateOgImages;
10530
11121
  exports.generateTabsCSS = require_tabs.generateTabsCSS;
10531
11122
  exports.generateTypes = generateTypes;
10532
11123
  exports.hasIslands = hasIslands;
@@ -10543,6 +11134,7 @@ exports.renderAllPages = renderAllPages;
10543
11134
  exports.renderPage = renderPage;
10544
11135
  exports.renderToString = renderToString;
10545
11136
  exports.resolveDocsOptions = resolveDocsOptions;
11137
+ exports.resolveOgImageOptions = resolveOgImageOptions;
10546
11138
  exports.resolveSearchOptions = resolveSearchOptions;
10547
11139
  exports.resolveSsgOptions = resolveSsgOptions;
10548
11140
  exports.resolveTheme = resolveTheme;