@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.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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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.svg");
9806
+ return path.join(outDir, dirPath, "og-image.png");
9311
9807
  }
9312
- return path.join(outDir, baseName, "og-image.svg");
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.svg`;
9322
- else relativePath = `${base}${urlPath}/og-image.svg`;
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
- let pageOgImage = ssgOptions.ogImage;
9460
- if (ssgOptions.generateOgImage && !ssgOptions.bare) {
9461
- const svg = await generateOgImageSvg({
9462
- title,
9463
- description,
9464
- siteName,
9465
- author: result.frontmatter.author
9466
- }, options.ogImageOptions ? {
9467
- width: options.ogImageOptions.width,
9468
- height: options.ogImageOptions.height,
9469
- backgroundColor: options.ogImageOptions.background,
9470
- textColor: options.ogImageOptions.textColor
9471
- } : void 0);
9472
- if (svg) {
9473
- const ogImageOutputPath = getOgImagePath(inputPath, srcDir, outDir);
9474
- await fs.mkdir(path.dirname(ogImageOutputPath), { recursive: true });
9475
- await fs.writeFile(ogImageOutputPath, svg, "utf-8");
9476
- generatedFiles.push(ogImageOutputPath);
9477
- pageOgImage = getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl);
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 (result.frontmatter.layout === "entry") entryPage = {
9482
- hero: result.frontmatter.hero,
9483
- features: result.frontmatter.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: result.toc,
9492
- frontmatter: result.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 process ${inputPath}: ${errorMessage}`);
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