nuxt-og-image 6.9.0 → 6.9.1

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.
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import { pathToFileURL } from 'node:url';
3
3
  import { resolveModulePath } from 'exsolve';
4
4
  import { dirname, join } from 'pathe';
5
- import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.CTD0ZPNd.mjs';
5
+ import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.CNS1IVAi.mjs';
6
6
  import 'node:crypto';
7
7
  import 'node:fs';
8
8
  import '@nuxt/kit';
@@ -1,8 +1,8 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { defu } from 'defu';
3
3
  import { logger } from '../../dist/runtime/logger.js';
4
- import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.CTD0ZPNd.mjs';
5
- export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.CTD0ZPNd.mjs';
4
+ import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.CNS1IVAi.mjs';
5
+ export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.CNS1IVAi.mjs';
6
6
  import 'node:crypto';
7
7
  import 'node:fs';
8
8
  import 'node:url';
package/dist/module.d.mts CHANGED
@@ -142,7 +142,7 @@ interface ModuleOptions {
142
142
  /**
143
143
  * Browser renderer configuration.
144
144
  *
145
- * When using browser-based rendering (screenshots), configure the browser provider.
145
+ * Set to true to enable defineOgImageScreenshot(), or configure a browser provider.
146
146
  * For Cloudflare deployments, specify the browser binding name.
147
147
  *
148
148
  * @example { provider: 'cloudflare', binding: 'BROWSER' }
package/dist/module.d.ts CHANGED
@@ -142,7 +142,7 @@ interface ModuleOptions {
142
142
  /**
143
143
  * Browser renderer configuration.
144
144
  *
145
- * When using browser-based rendering (screenshots), configure the browser provider.
145
+ * Set to true to enable defineOgImageScreenshot(), or configure a browser provider.
146
146
  * For Cloudflare deployments, specify the browser binding name.
147
147
  *
148
148
  * @example { provider: 'cloudflare', binding: 'BROWSER' }
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.16.0"
5
5
  },
6
6
  "configKey": "ogImage",
7
- "version": "6.9.0",
7
+ "version": "6.9.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -10,7 +10,7 @@ import 'nuxtseo-shared/kit';
10
10
  import 'pathe';
11
11
  import 'pkg-types';
12
12
  import 'std-env';
13
- export { n as default } from './shared/nuxt-og-image.CTD0ZPNd.mjs';
13
+ export { n as default } from './shared/nuxt-og-image.CNS1IVAi.mjs';
14
14
  import '../dist/runtime/logger.js';
15
15
  import 'magic-string';
16
16
  import 'oxc-walker';
@@ -1,6 +1,9 @@
1
- import { useRouter } from "#app";
1
+ import { createError, useRouter } from "#app";
2
+ import { useOgImageRuntimeConfig } from "../utils.js";
2
3
  import { defineOgImageRaw } from "./_defineOgImageRaw.js";
3
4
  export function defineOgImageScreenshot(options = {}) {
5
+ if (!useOgImageRuntimeConfig().browserEnabled)
6
+ throw createError({ statusCode: 500, statusMessage: "[nuxt-og-image] Screenshots require browser opt-in. Set ogImage.browser to true or a browser provider in nuxt.config.ts." });
4
7
  const router = useRouter();
5
8
  const route = router.currentRoute.value?.path || "/";
6
9
  const { delay, mask, selector, colorScheme, ...rest } = options;
@@ -1,7 +1,7 @@
1
1
  import { parse } from "devalue";
2
- import { parseURL } from "ufo";
2
+ import { parseURL, withoutBase } from "ufo";
3
3
  import { appendResponseHeader } from "#nuxtseo/h3";
4
- import { defineNitroPlugin } from "#nuxtseo/nitro";
4
+ import { defineNitroPlugin, useRuntimeConfig } from "#nuxtseo/nitro";
5
5
  import { prerenderOptionsCache } from "#og-image-cache";
6
6
  import { createSitePathResolver } from "#site-config/server/composables/utils";
7
7
  import { isInternalRoute } from "../../shared.js";
@@ -37,9 +37,10 @@ export default defineNitroPlugin(async (nitro) => {
37
37
  });
38
38
  const key = resolvePathCacheKey(ctx.event, resolvePathWithBase(path));
39
39
  await prerenderOptionsCache.setItem(key, payloads);
40
+ const pagePath = withoutBase(path, useRuntimeConfig().app.baseURL);
40
41
  for (const [_ogKey, opt] of payloads) {
41
42
  if (opt._hash) {
42
- await prerenderOptionsCache.setItem(`hash:${opt._hash}`, opt);
43
+ await prerenderOptionsCache.setItem(`hash:${opt._hash}`, { ...opt, _path: pagePath });
43
44
  }
44
45
  }
45
46
  const prerenderPaths = ctx.event.context._ogImagePrerenderPaths;
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * Generate a deterministic hash from options object
3
- * Excludes _path so images with same options can be cached across pages
3
+ * Excludes _path so images with same options can be cached across pages,
4
+ * except for PageScreenshot, which renders the page itself.
4
5
  * Optionally includes componentHash and version for cache busting
6
+ *
7
+ * Uses the first 64 bits of a SHA-256 digest over a canonical (key-sorted)
8
+ * serialization. The previous 32-bit rolling hash collided on large sites,
9
+ * silently overwriting other pages' prerendered images.
5
10
  */
6
11
  export declare function hashOgImageOptions(options: Record<string, any>, componentHash?: string, version?: string): string;
7
12
  /**
@@ -25,7 +30,8 @@ export interface BuildOgImageUrlResult {
25
30
  /**
26
31
  * Build full OG image URL
27
32
  *
28
- * When encoded params exceed MAX_PATH_LENGTH, falls back to hash mode:
33
+ * Static URLs fall back to hash mode when encoded params exceed MAX_PATH_LENGTH
34
+ * or hold a character outside [A-Za-z0-9_.~-]:
29
35
  * - Returns short path: /_og/s/o_<hash>.png
30
36
  * - Returns hash in result for cache storage
31
37
  *
@@ -34,7 +40,7 @@ export interface BuildOgImageUrlResult {
34
40
  * @param isStatic - Whether this is a static/prerendered image
35
41
  * @param defaults - Optional defaults to skip from URL (keeps URLs shorter)
36
42
  * @example buildOgImageUrl({ width: 1200, props: { title: 'Hello' } }, 'png', true)
37
- * // Returns: { url: "/_og/s/w_1200,title_Hello.png" }
43
+ * // Returns: { url: "/_og/s/o_<hash>.png", hash: "<hash>" }
38
44
  */
39
45
  export declare function buildOgImageUrl(options: Record<string, any>, extension?: string, isStatic?: boolean, defaults?: Record<string, any>, secret?: string): BuildOgImageUrlResult;
40
46
  /**
@@ -19,6 +19,7 @@ const RE_COMMA_PARAM_SEPARATOR = /,(?=\w+_)/;
19
19
  const RE_SIGNATURE_SUFFIX = /,s_[^,]+$/;
20
20
  const RE_NON_ASCII = /[^\u0000-\u007F]/;
21
21
  const RE_WINDOWS_RESERVED_FILENAME_CHARACTERS = /[<>:"/\\|?*\u0000-\u001F]/;
22
+ const RE_NOT_UNRESERVED = /[^\w.~-]/;
22
23
  const PARAM_ALIASES = {
23
24
  w: "width",
24
25
  h: "height",
@@ -85,19 +86,35 @@ function b64Decode(str) {
85
86
  }
86
87
  return Buffer.from(padded, "base64").toString("utf8");
87
88
  }
88
- function simpleHash(str) {
89
- let hash = 0;
90
- for (let i = 0; i < str.length; i++) {
91
- const char = str.charCodeAt(i);
92
- hash = (hash << 5) - hash + char;
93
- hash = hash & hash;
89
+ function canonicalJson(value) {
90
+ if (value === null || typeof value !== "object")
91
+ return JSON.stringify(value) ?? "null";
92
+ if (Array.isArray(value))
93
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
94
+ const record = value;
95
+ const entries = Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`);
96
+ return `{${entries.join(",")}}`;
97
+ }
98
+ const B64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
99
+ function base64UrlToHex(input) {
100
+ let hex = "";
101
+ let buffer = 0;
102
+ let bits = 0;
103
+ for (const char of input) {
104
+ buffer = buffer << 6 | B64URL_ALPHABET.indexOf(char);
105
+ bits += 6;
106
+ if (bits >= 8) {
107
+ bits -= 8;
108
+ hex += (buffer >> bits & 255).toString(16).padStart(2, "0");
109
+ }
94
110
  }
95
- return Math.abs(hash).toString(36);
111
+ return hex;
96
112
  }
97
113
  export function hashOgImageOptions(options, componentHash, version) {
98
- const { _path, _hash, ...hashableOptions } = options;
114
+ const { _path, _hash, ...rest } = options;
115
+ const hashableOptions = rest.component === "PageScreenshot" ? { ...rest, _path } : rest;
99
116
  const hashInput = componentHash || version ? [hashableOptions, componentHash || "", version || ""] : hashableOptions;
100
- return simpleHash(JSON.stringify(hashInput));
117
+ return base64UrlToHex(digest(canonicalJson(hashInput))).slice(0, 16);
101
118
  }
102
119
  export function encodeOgImageParams(options, defaults) {
103
120
  const parts = [];
@@ -233,7 +250,7 @@ export function decodeOgImageParams(encoded) {
233
250
  export function buildOgImageUrl(options, extension = "png", isStatic = false, defaults, secret) {
234
251
  const encoded = encodeOgImageParams(options, defaults);
235
252
  const prefix = isStatic ? "/_og/s" : "/_og/d";
236
- if (isStatic && (encoded.length > MAX_PATH_LENGTH || encoded.includes("%"))) {
253
+ if (isStatic && (encoded.length > MAX_PATH_LENGTH || RE_NOT_UNRESERVED.test(encoded))) {
237
254
  const hash = hashOgImageOptions(options);
238
255
  return {
239
256
  url: `${prefix}/o_${hash}.${extension}`,
@@ -31,6 +31,7 @@ export interface OgImageIconsData {
31
31
  height: number;
32
32
  }
33
33
  export interface OgImageRuntimeConfig {
34
+ browserEnabled: boolean;
34
35
  version: string;
35
36
  satoriOptions: SatoriOptions;
36
37
  resvgOptions: ResvgRenderOptions;
@@ -77,7 +78,7 @@ export interface OgImageRuntimeConfig {
77
78
  }
78
79
  export type RendererType = 'satori' | 'browser' | 'takumi';
79
80
  export type BrowserProvider = 'playwright' | 'chrome-launcher' | 'on-demand' | 'cloudflare';
80
- export type BrowserConfig = false | {
81
+ export type BrowserConfig = boolean | {
81
82
  provider: BrowserProvider;
82
83
  /** Required when provider is 'cloudflare' - the wrangler binding name */
83
84
  binding?: string;
@@ -446,6 +446,12 @@ async function setupBuildHandler(config, resolve, getDetectedRenderers, getCompa
446
446
  metadata: getCompatibilityMeta()
447
447
  });
448
448
  nuxt.hooks.hook("nitro:init", async (nitro) => {
449
+ nitro.hooks.hook("prerender:config", (prerenderConfig) => {
450
+ if (prerenderConfig.virtual?.["playwright-core"]) {
451
+ const { "playwright-core": _, ...virtual } = prerenderConfig.virtual;
452
+ prerenderConfig.virtual = virtual;
453
+ }
454
+ });
449
455
  await applyNitroPresetCompatibility(nitro.options, {
450
456
  compatibility: config.compatibility?.runtime,
451
457
  resolve,
@@ -651,6 +657,14 @@ const PROVIDER_DEPENDENCIES = [
651
657
  }
652
658
  }
653
659
  ];
660
+ function resolveAutoDetectedProvider(input) {
661
+ if (input.hasUserComponents)
662
+ return { preferred: null, fallbackToDefault: false };
663
+ const preferred = input.installedProviders.find((p) => p === "takumi") ?? input.installedProviders[0] ?? null;
664
+ if (preferred)
665
+ return { preferred, fallbackToDefault: false };
666
+ return { preferred: null, fallbackToDefault: true };
667
+ }
654
668
  async function getInstalledProviders() {
655
669
  const installed = [];
656
670
  for (const provider of PROVIDER_DEPENDENCIES) {
@@ -5657,19 +5671,25 @@ ${familyInfo}`);
5657
5671
  }
5658
5672
  }
5659
5673
  }
5674
+ if (config.browser)
5675
+ ogImageComponentCtx.detectedRenderers.add("browser");
5660
5676
  if (!nuxt.options._prepare && !hasUserComponents) {
5661
- const installedProviders = await getInstalledProviders();
5662
- const preferred = installedProviders.find((p) => p.provider === "takumi") || installedProviders[0];
5677
+ const { preferred, fallbackToDefault } = resolveAutoDetectedProvider({
5678
+ hasUserComponents,
5679
+ installedProviders: (await getInstalledProviders()).map((p) => p.provider)
5680
+ });
5663
5681
  if (preferred) {
5664
- ogImageComponentCtx.detectedRenderers.add(preferred.provider);
5665
- logger.debug(`Using ${preferred.provider} renderer`);
5666
- } else if (nuxt.options.dev && !nuxt.options._prepare && canPromptInteractively()) {
5667
- const renderer = await promptForRendererSelection();
5668
- ogImageComponentCtx.detectedRenderers.add(renderer);
5669
- logger.debug(`Using ${renderer} renderer`);
5670
- } else {
5671
- ogImageComponentCtx.detectedRenderers.add("takumi");
5672
- logger.warn(`No OG image renderer dependency detected. Defaulting to \`takumi\` (non-interactive environment). Install \`${TAKUMI_CORE_PACKAGE}\`, or add a renderer component (e.g. components/OgImage/Default.satori.vue) to choose explicitly.`);
5682
+ ogImageComponentCtx.detectedRenderers.add(preferred);
5683
+ logger.debug(`Using ${preferred} renderer`);
5684
+ } else if (fallbackToDefault) {
5685
+ if (nuxt.options.dev && !nuxt.options._prepare && canPromptInteractively()) {
5686
+ const renderer = await promptForRendererSelection();
5687
+ ogImageComponentCtx.detectedRenderers.add(renderer);
5688
+ logger.debug(`Using ${renderer} renderer`);
5689
+ } else {
5690
+ ogImageComponentCtx.detectedRenderers.add("takumi");
5691
+ logger.warn(`No OG image renderer dependency detected. Defaulting to \`takumi\` (non-interactive environment). Install \`${TAKUMI_CORE_PACKAGE}\`, or add a renderer component (e.g. components/OgImage/Default.satori.vue) to choose explicitly.`);
5692
+ }
5673
5693
  }
5674
5694
  }
5675
5695
  const availableRenderers = /* @__PURE__ */ new Set();
@@ -6075,6 +6095,7 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6075
6095
  // @ts-expect-error runtime type
6076
6096
  isNuxtContentDocumentDriven: !!nuxt.options.content?.documentDriven,
6077
6097
  cssFramework: cssFramework || "none",
6098
+ browserEnabled: !!config.browser,
6078
6099
  // Browser renderer config for cloudflare binding access
6079
6100
  browser: typeof config.browser === "object" ? {
6080
6101
  provider: config.browser.provider,
@@ -6109,6 +6130,7 @@ export const staticFontCacheDir = ${JSON.stringify(getStaticFontCacheDir(nuxt.op
6109
6130
  "nuxt-og-image": {
6110
6131
  defaults: runtimeConfig.defaults,
6111
6132
  includeTwitter: runtimeConfig.includeTwitter,
6133
+ browserEnabled: runtimeConfig.browserEnabled,
6112
6134
  hasServerRuntime: !nuxt.options._generate && !nuxt.options.nitro?.static
6113
6135
  }
6114
6136
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-og-image",
3
3
  "type": "module",
4
- "version": "6.9.0",
4
+ "version": "6.9.1",
5
5
  "description": "Enlightened OG Image generation for Nuxt.",
6
6
  "author": {
7
7
  "website": "https://harlanzw.com",