nuxt-og-image 6.3.6 → 6.3.8

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.
Files changed (45) hide show
  1. package/dist/chunks/tw4.cjs +1 -1
  2. package/dist/chunks/tw4.mjs +1 -1
  3. package/dist/chunks/uno.cjs +1 -1
  4. package/dist/chunks/uno.mjs +1 -1
  5. package/dist/cli.cjs +1 -0
  6. package/dist/cli.mjs +1 -0
  7. package/dist/devtools/200.html +1 -1
  8. package/dist/devtools/404.html +1 -1
  9. package/dist/devtools/_nuxt/{D6HqPJ2P.js → B1vWG-dr.js} +1 -1
  10. package/dist/devtools/_nuxt/{DO62csGn.js → B4O-Ri_H.js} +1 -1
  11. package/dist/devtools/_nuxt/{DJOUfd_s.js → B4pt0d5N.js} +1 -1
  12. package/dist/devtools/_nuxt/{C4ssBbxk.js → C-BKEkNl.js} +6 -6
  13. package/dist/devtools/_nuxt/{Dgwhb1xi.js → C-rB6lUS.js} +1 -1
  14. package/dist/devtools/_nuxt/{Dn8E1mDk.js → CFv1BN3Z.js} +1 -1
  15. package/dist/devtools/_nuxt/{DggwDRkJ.js → DOBkniDZ.js} +1 -1
  16. package/dist/devtools/_nuxt/DevtoolsSection.BkXKwKS6.css +1 -0
  17. package/dist/devtools/_nuxt/DevtoolsSnippet.CiX5uaV5.css +1 -0
  18. package/dist/devtools/_nuxt/builds/latest.json +1 -1
  19. package/dist/devtools/_nuxt/builds/meta/d4af52db-d87a-45fc-9901-69797a3ca24f.json +1 -0
  20. package/dist/devtools/_nuxt/{entry.D0N1PjT9.css → entry.D7zgwaiY.css} +1 -1
  21. package/dist/devtools/_nuxt/{pages.DjPHX4aO.css → pages.Bkgf-fNs.css} +1 -1
  22. package/dist/devtools/_nuxt/renderer-select.Bvsk444V.css +1 -0
  23. package/dist/devtools/debug/index.html +1 -1
  24. package/dist/devtools/docs/index.html +1 -1
  25. package/dist/devtools/index.html +1 -1
  26. package/dist/devtools/templates/index.html +1 -1
  27. package/dist/module.cjs +1 -1
  28. package/dist/module.d.cts +6 -5
  29. package/dist/module.d.mts +6 -5
  30. package/dist/module.d.ts +6 -5
  31. package/dist/module.json +1 -1
  32. package/dist/module.mjs +1 -1
  33. package/dist/runtime/app/utils.js +3 -1
  34. package/dist/runtime/server/og-image/context.js +1 -1
  35. package/dist/runtime/server/util/cache.d.ts +1 -0
  36. package/dist/runtime/server/util/cache.js +23 -4
  37. package/dist/runtime/server/util/eventHandlers.js +4 -3
  38. package/dist/runtime/types.d.ts +0 -1
  39. package/dist/shared/{nuxt-og-image.BWSGGMOE.mjs → nuxt-og-image.caStlBVv.mjs} +135 -4
  40. package/dist/shared/{nuxt-og-image.DD0uPnWP.cjs → nuxt-og-image.if6TuaSI.cjs} +136 -5
  41. package/package.json +5 -5
  42. package/dist/devtools/_nuxt/DevtoolsSection.BA22BKWd.css +0 -1
  43. package/dist/devtools/_nuxt/DevtoolsSnippet.5f9DiVqQ.css +0 -1
  44. package/dist/devtools/_nuxt/builds/meta/4bfaee23-0bae-4d94-b1da-6726b2c174f9.json +0 -1
  45. package/dist/devtools/_nuxt/renderer-select.Dd3SlzOB.css +0 -1
package/dist/module.d.ts CHANGED
@@ -109,14 +109,15 @@ interface ModuleOptions {
109
109
  */
110
110
  emojiStrategy?: 'auto' | 'local' | 'fetch';
111
111
  /**
112
- * Include query parameters in cache keys.
112
+ * Default cache duration in seconds for generated OG images.
113
113
  *
114
- * When enabled, requests like `/page?foo=bar` will have a separate cache from `/page`.
115
- * Enable this if your OG image content depends on query params.
114
+ * Controls the internal storage TTL, HTTP Cache-Control headers, and the
115
+ * auto-configured SWR route rule. Can be overridden per-image via
116
+ * `defineOgImage`'s `cacheMaxAgeSeconds` option.
116
117
  *
117
- * @default false
118
+ * @default 60 * 60 * 24 * 3 (3 days)
118
119
  */
119
- cacheQueryParams?: boolean;
120
+ cacheMaxAgeSeconds?: number;
120
121
  /**
121
122
  * Font subsets to download when resolving missing font families via fontless.
122
123
  *
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.16.0"
5
5
  },
6
6
  "configKey": "ogImage",
7
- "version": "6.3.6",
7
+ "version": "6.3.8",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -7,7 +7,7 @@ import 'nuxt-site-config/kit';
7
7
  import 'ohash';
8
8
  import 'pathe';
9
9
  import 'pkg-types';
10
- export { m as default } from './shared/nuxt-og-image.BWSGGMOE.mjs';
10
+ export { m as default } from './shared/nuxt-og-image.caStlBVv.mjs';
11
11
  import 'nuxtseo-shared/kit';
12
12
  import '../dist/runtime/logger.js';
13
13
  import 'node:crypto';
@@ -98,8 +98,10 @@ export function createOgImageMeta(src, input, ssrContext, pagePath, head) {
98
98
  if (component?.hash)
99
99
  urlOpts._componentHash = component.hash;
100
100
  const result = buildOgImageUrl(urlOpts, extension, isStatic, defaults, ogImageConfig.security?.secret || void 0);
101
- if (result.hash)
101
+ if (result.hash) {
102
102
  opts._hash = result.hash;
103
+ options._hash = result.hash;
104
+ }
103
105
  const resolvedUrl = joinURL("/", baseURL, result.url);
104
106
  const finalUrl = opts._query && Object.keys(opts._query).length ? withQuery(resolvedUrl, { _query: opts._query }) : resolvedUrl;
105
107
  if (isStatic && import.meta.prerender && ssrContext.event) {
@@ -99,7 +99,7 @@ export async function resolveContext(e) {
99
99
  }
100
100
  }
101
101
  let queryParams = {};
102
- if (!secret || import.meta.dev || import.meta.prerender) {
102
+ if (import.meta.dev || import.meta.prerender) {
103
103
  const query = getQuery(e);
104
104
  for (const k in query) {
105
105
  const v = String(query[k]);
@@ -3,6 +3,7 @@ import type { OgImageRenderEventContext } from '../../types.js';
3
3
  export declare function useOgImageBufferCache(ctx: OgImageRenderEventContext, options: {
4
4
  baseCacheKey: string | false;
5
5
  cacheMaxAgeSeconds?: number;
6
+ secret?: string;
6
7
  }): Promise<void | H3Error | {
7
8
  cachedItem: false | BufferSource;
8
9
  enabled: boolean;
@@ -3,9 +3,19 @@ import { useStorage } from "nitropack/runtime";
3
3
  import { digest } from "ohash";
4
4
  import { withTrailingSlash } from "ufo";
5
5
  import { prefixStorage } from "unstorage";
6
+ import { logger } from "../../logger.js";
7
+ function safeCompare(a, b) {
8
+ if (a.length !== b.length)
9
+ return false;
10
+ let mismatch = 0;
11
+ for (let i = 0; i < a.length; i++)
12
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
13
+ return mismatch === 0;
14
+ }
6
15
  export async function useOgImageBufferCache(ctx, options) {
7
16
  const maxAge = Number(options.cacheMaxAgeSeconds);
8
- let enabled = !import.meta.dev && import.meta.env?.MODE !== "test" && maxAge > 0;
17
+ const intentionallyEnabled = !import.meta.dev && maxAge > 0;
18
+ let enabled = intentionallyEnabled;
9
19
  const cache = prefixStorage(useStorage(), withTrailingSlash(options.baseCacheKey || "/"));
10
20
  const key = ctx.key;
11
21
  let cachedItem = false;
@@ -25,7 +35,14 @@ export async function useOgImageBufferCache(ctx, options) {
25
35
  value: null,
26
36
  expiresAt: Date.now()
27
37
  }));
28
- if (typeof getQuery(ctx.e).purge !== "undefined") {
38
+ const purgeValue = getQuery(ctx.e).purge;
39
+ if (typeof purgeValue !== "undefined") {
40
+ if (options.secret && !safeCompare(String(purgeValue), options.secret)) {
41
+ return createError({
42
+ statusCode: 403,
43
+ statusMessage: "[Nuxt OG Image] Invalid purge token. Provide the signing secret as ?purge=<secret>."
44
+ });
45
+ }
29
46
  await cache.removeItem(key).catch(() => {
30
47
  });
31
48
  } else if (expiresAt > Date.now()) {
@@ -38,6 +55,7 @@ export async function useOgImageBufferCache(ctx, options) {
38
55
  return;
39
56
  }
40
57
  setHeaders(ctx.e, headers);
58
+ setHeader(ctx.e, "X-OG-Cache", "HIT");
41
59
  } else {
42
60
  await cache.removeItem(key).catch(() => {
43
61
  });
@@ -53,6 +71,7 @@ export async function useOgImageBufferCache(ctx, options) {
53
71
  enabled,
54
72
  cachedItem,
55
73
  async update(item) {
74
+ setHeader(ctx.e, "X-OG-Cache", intentionallyEnabled ? "MISS" : "DISABLED");
56
75
  if (!enabled)
57
76
  return;
58
77
  const value = Buffer.from(item).toString("base64");
@@ -61,14 +80,14 @@ export async function useOgImageBufferCache(ctx, options) {
61
80
  "Vary": "accept-encoding, host",
62
81
  "etag": `W/"${digest(value)}"`,
63
82
  "last-modified": (/* @__PURE__ */ new Date()).toUTCString(),
64
- "cache-control": `public, s-maxage=${maxAge}, stale-while-revalidate`
83
+ "cache-control": `public, max-age=${maxAge}, s-maxage=${maxAge}, stale-while-revalidate=${maxAge}, stale-if-error=${maxAge}`
65
84
  };
66
85
  setHeaders(ctx.e, headers);
67
86
  await cache.setItem(key, {
68
87
  value,
69
88
  headers,
70
89
  expiresAt: Date.now() + maxAge * 1e3
71
- });
90
+ }).catch((err) => logger.warn(`[Nuxt OG Image] Failed to write cache for key "${key}": ${err?.message || err}`));
72
91
  }
73
92
  };
74
93
  }
@@ -82,6 +82,7 @@ export async function imageEventHandler(e) {
82
82
  case "png":
83
83
  case "jpeg":
84
84
  case "jpg":
85
+ case "webp":
85
86
  if (!renderer.supportedFormats.includes(extension)) {
86
87
  return createError({
87
88
  statusCode: 400,
@@ -102,7 +103,8 @@ export async function imageEventHandler(e) {
102
103
  }
103
104
  const cacheApi = await useOgImageBufferCache(ctx, {
104
105
  cacheMaxAgeSeconds: ctx.options.cacheMaxAgeSeconds,
105
- baseCacheKey
106
+ baseCacheKey,
107
+ secret: security?.secret
106
108
  });
107
109
  if (typeof cacheApi === "undefined")
108
110
  return;
@@ -110,8 +112,7 @@ export async function imageEventHandler(e) {
110
112
  return cacheApi;
111
113
  let image = cacheApi.cachedItem;
112
114
  if (!image) {
113
- const { security: security2 } = useOgImageRuntimeConfig();
114
- const timeout = security2?.renderTimeout || 15e3;
115
+ const timeout = security?.renderTimeout || 15e3;
115
116
  let timer;
116
117
  image = await Promise.race([
117
118
  renderer.createImage(ctx),
@@ -38,7 +38,6 @@ export interface OgImageRuntimeConfig {
38
38
  colorPreference: 'light' | 'dark';
39
39
  isNuxtContentDocumentDriven: boolean;
40
40
  zeroRuntime: boolean;
41
- cacheQueryParams: boolean;
42
41
  cssFramework: 'tailwind' | 'unocss' | 'none';
43
42
  componentDirs?: string[];
44
43
  /** Directory for persistent build cache (CI caching) */
@@ -2240,6 +2240,7 @@ function setupPrerenderHandler(options, resolve, getDetectedRenderers, nuxt = us
2240
2240
  orphanedOgHashes.push(route.route);
2241
2241
  route.skip = true;
2242
2242
  delete route.error;
2243
+ route.contents = "";
2243
2244
  });
2244
2245
  nitro.hooks.hook("prerender:done", async () => {
2245
2246
  if (orphanedOgHashes.length > 0) {
@@ -2534,6 +2535,94 @@ const ATTRIBUTE_NODE = 6;
2534
2535
  const DIRECTIVE_NODE = 7;
2535
2536
  const RE_PURE_NUMBER = /^\d+(?:\.\d+)?$/;
2536
2537
  const RE_DOUBLE_QUOTE = /"/g;
2538
+ const RE_CAMEL_TO_KEBAB = /[A-Z]/g;
2539
+ const RE_STRIP_QUOTES = /^['"]|['"]$/g;
2540
+ const RE_SINGLE_QUOTED = /^'([^']*)'$/;
2541
+ const RE_DOUBLE_QUOTED = /^"([^"]*)"$/;
2542
+ const RE_BACKTICK_QUOTED = /^`([^`]*)`$/;
2543
+ const RE_CSS_NUMBER = /^[\d.]+(?:px|em|rem|%|vh|vw|ch|ex|vmin|vmax|pt|pc|in|cm|mm)?$/;
2544
+ function camelToKebab(str) {
2545
+ return str.replace(RE_CAMEL_TO_KEBAB, (m) => `-${m.toLowerCase()}`);
2546
+ }
2547
+ function parseStaticStyleObject(expr) {
2548
+ const trimmed = expr.trim();
2549
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
2550
+ return void 0;
2551
+ const inner = trimmed.slice(1, -1).trim();
2552
+ if (!inner)
2553
+ return {};
2554
+ const result = {};
2555
+ let depth = 0;
2556
+ let inQuote = null;
2557
+ let current = "";
2558
+ for (let i = 0; i < inner.length; i++) {
2559
+ const ch = inner[i];
2560
+ if (inQuote) {
2561
+ current += ch;
2562
+ if (ch === inQuote && inner[i - 1] !== "\\")
2563
+ inQuote = null;
2564
+ continue;
2565
+ }
2566
+ if (ch === "'" || ch === '"' || ch === "`") {
2567
+ inQuote = ch;
2568
+ current += ch;
2569
+ continue;
2570
+ }
2571
+ if (ch === "(" || ch === "[" || ch === "{") {
2572
+ depth++;
2573
+ current += ch;
2574
+ continue;
2575
+ }
2576
+ if (ch === ")" || ch === "]" || ch === "}") {
2577
+ depth--;
2578
+ current += ch;
2579
+ continue;
2580
+ }
2581
+ if (ch === "," && depth === 0) {
2582
+ const parsed = parseStyleEntry(current);
2583
+ if (!parsed)
2584
+ return void 0;
2585
+ result[parsed[0]] = parsed[1];
2586
+ current = "";
2587
+ continue;
2588
+ }
2589
+ current += ch;
2590
+ }
2591
+ if (current.trim()) {
2592
+ const parsed = parseStyleEntry(current);
2593
+ if (!parsed)
2594
+ return void 0;
2595
+ result[parsed[0]] = parsed[1];
2596
+ }
2597
+ return result;
2598
+ }
2599
+ function parseStyleEntry(entry) {
2600
+ const colonIdx = entry.indexOf(":");
2601
+ if (colonIdx === -1)
2602
+ return void 0;
2603
+ const rawKey = entry.slice(0, colonIdx).trim();
2604
+ const rawValue = entry.slice(colonIdx + 1).trim();
2605
+ const key = rawKey.replace(RE_STRIP_QUOTES, "");
2606
+ if (!key)
2607
+ return void 0;
2608
+ let value;
2609
+ const singleQuoteMatch = rawValue.match(RE_SINGLE_QUOTED);
2610
+ const doubleQuoteMatch = rawValue.match(RE_DOUBLE_QUOTED);
2611
+ const backtickMatch = rawValue.match(RE_BACKTICK_QUOTED);
2612
+ const numberMatch = rawValue.match(RE_CSS_NUMBER);
2613
+ if (singleQuoteMatch) {
2614
+ value = singleQuoteMatch[1];
2615
+ } else if (doubleQuoteMatch) {
2616
+ value = doubleQuoteMatch[1];
2617
+ } else if (backtickMatch && !backtickMatch[1].includes("${")) {
2618
+ value = backtickMatch[1];
2619
+ } else if (numberMatch) {
2620
+ value = rawValue;
2621
+ } else {
2622
+ return void 0;
2623
+ }
2624
+ return [camelToKebab(key), value];
2625
+ }
2537
2626
  function escapeAttrValue(value) {
2538
2627
  return value.replace(RE_DOUBLE_QUOTE, "&quot;");
2539
2628
  }
@@ -2571,7 +2660,17 @@ async function transformVueTemplate(code, options) {
2571
2660
  }
2572
2661
  }
2573
2662
  if (prop.type === DIRECTIVE_NODE && prop.name === "bind" && prop.arg?.content === "style") {
2574
- collector.hasDynamicStyle = true;
2663
+ const exp = prop.exp?.content;
2664
+ if (exp) {
2665
+ const parsed = parseStaticStyleObject(exp);
2666
+ if (parsed) {
2667
+ collector.dynamicStyleProps = parsed;
2668
+ collector.dynamicStyleLoc = {
2669
+ start: prop.loc.start.offset,
2670
+ end: prop.loc.end.offset
2671
+ };
2672
+ }
2673
+ }
2575
2674
  }
2576
2675
  if (prop.type === ATTRIBUTE_NODE && (prop.name === "width" || prop.name === "height") && prop.value && el.tag === "img") {
2577
2676
  if (!collector.dimensionAttrs)
@@ -2579,7 +2678,7 @@ async function transformVueTemplate(code, options) {
2579
2678
  collector.dimensionAttrs.push({ name: prop.name, value: prop.value.content });
2580
2679
  }
2581
2680
  }
2582
- if (collector.classes.length > 0 || collector.existingStyle || collector.dimensionAttrs?.length) {
2681
+ if (collector.classes.length > 0 || collector.existingStyle || collector.dimensionAttrs?.length || collector.dynamicStyleProps) {
2583
2682
  if (collector.existingStyle && !collector.styleLoc) {
2584
2683
  logger.warn(`[vue-template-transform] BUG: existingStyle found but styleLoc is undefined!`);
2585
2684
  }
@@ -2620,6 +2719,9 @@ async function transformVueTemplate(code, options) {
2620
2719
  }
2621
2720
  }
2622
2721
  }
2722
+ if (collector.dynamicStyleProps) {
2723
+ Object.assign(styleProps, collector.dynamicStyleProps);
2724
+ }
2623
2725
  if (styleProps.display === "flex" && !styleProps["flex-direction"]) {
2624
2726
  styleProps["flex-direction"] = "row";
2625
2727
  }
@@ -2628,7 +2730,8 @@ async function transformVueTemplate(code, options) {
2628
2730
  const hasUnresolved = unresolvedClasses.length > 0;
2629
2731
  const resolvedSome = collector.classes.length > 0 && (unresolvedClasses.length < collector.classes.length || hasClassRewrites);
2630
2732
  const styleChanged = collector.existingStyle && styleStr !== collector.existingStyle;
2631
- if (!resolvedSome && !styleChanged && !collector.dimensionAttrs?.length)
2733
+ const hasDynamicStyleResolved = collector.dynamicStyleProps && Object.keys(collector.dynamicStyleProps).length > 0;
2734
+ if (!resolvedSome && !styleChanged && !collector.dimensionAttrs?.length && !hasDynamicStyleResolved)
2632
2735
  continue;
2633
2736
  hasChanges = true;
2634
2737
  if (collector.classLoc) {
@@ -2638,6 +2741,9 @@ async function transformVueTemplate(code, options) {
2638
2741
  s.remove(collector.classLoc.start, collector.classLoc.end);
2639
2742
  }
2640
2743
  }
2744
+ if (collector.dynamicStyleLoc) {
2745
+ s.remove(collector.dynamicStyleLoc.start, collector.dynamicStyleLoc.end);
2746
+ }
2641
2747
  if (hasResolvedStyles) {
2642
2748
  if (collector.styleLoc) {
2643
2749
  s.overwrite(collector.styleLoc.start, collector.styleLoc.end, `style="${styleStr}"`);
@@ -3549,6 +3655,7 @@ const REMOVED_CONFIG = {
3549
3655
  "runtimeSatori": "compatibility.runtime.satori",
3550
3656
  "cacheTtl": "cacheMaxAgeSeconds",
3551
3657
  "cache": "cacheMaxAgeSeconds",
3658
+ "cacheQueryParams": "Removed (all options are encoded in the URL path)",
3552
3659
  "cacheKey": "Removed",
3553
3660
  "static": "zeroRuntime",
3554
3661
  "chromium-node": "compatibility.runtime.browser: 'playwright'",
@@ -3670,6 +3777,7 @@ const DEPRECATED_CONFIG = {
3670
3777
  runtimeSatori: "Use compatibility.runtime.satori",
3671
3778
  cacheTtl: "Use cacheMaxAgeSeconds",
3672
3779
  cache: "Use cacheMaxAgeSeconds",
3780
+ cacheQueryParams: "Removed (all options are encoded in the URL path)",
3673
3781
  cacheKey: "Removed",
3674
3782
  static: "Removed - use zeroRuntime",
3675
3783
  fonts: "Use @nuxt/fonts module instead"
@@ -4216,6 +4324,10 @@ const module$1 = defineNuxtModule({
4216
4324
  logger.warn("Nuxt OG Image is enabled but SSR is disabled.\n\nYou should enable SSR (`ssr: true`) or disable the module (`ogImage: { enabled: false }`).");
4217
4325
  return;
4218
4326
  }
4327
+ if (config.cacheMaxAgeSeconds != null) {
4328
+ config.defaults = config.defaults || {};
4329
+ config.defaults.cacheMaxAgeSeconds = config.cacheMaxAgeSeconds;
4330
+ }
4219
4331
  if (config.debug && !nuxt.options.dev) {
4220
4332
  logger.warn("`ogImage.debug` is enabled in production. This exposes the `/_og/debug.json` endpoint and should not be enabled in production. Disable it before deploying.");
4221
4333
  }
@@ -4621,6 +4733,26 @@ ${familyInfo}`);
4621
4733
  route: "/_og/s/**",
4622
4734
  handler: resolve(`${basePath}/image`)
4623
4735
  });
4736
+ if (!nuxt.options.dev) {
4737
+ nuxt.options.routeRules = nuxt.options.routeRules || {};
4738
+ if (config.runtimeCacheStorage !== false && !nuxt.options.test) {
4739
+ const ogDynamicRule = nuxt.options.routeRules["/_og/d/**"];
4740
+ if (!ogDynamicRule?.swr && !ogDynamicRule?.isr && !ogDynamicRule?.cache && !ogDynamicRule?.headers) {
4741
+ const ttl = config.cacheMaxAgeSeconds ?? config.defaults?.cacheMaxAgeSeconds ?? 60 * 60 * 24 * 3;
4742
+ nuxt.options.routeRules["/_og/d/**"] = defu(
4743
+ nuxt.options.routeRules["/_og/d/**"] || {},
4744
+ { swr: ttl }
4745
+ );
4746
+ }
4747
+ }
4748
+ const ogStaticRule = nuxt.options.routeRules["/_og/s/**"];
4749
+ if (!ogStaticRule?.swr && !ogStaticRule?.isr && !ogStaticRule?.cache && !ogStaticRule?.headers) {
4750
+ nuxt.options.routeRules["/_og/s/**"] = defu(
4751
+ nuxt.options.routeRules["/_og/s/**"] || {},
4752
+ { headers: { "cache-control": "public, max-age=31536000, immutable" } }
4753
+ );
4754
+ }
4755
+ }
4624
4756
  if (!nuxt.options.dev) {
4625
4757
  nuxt.options.optimization.treeShake.composables.client["nuxt-og-image"] = [];
4626
4758
  }
@@ -5071,7 +5203,6 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5071
5203
  colorPreference,
5072
5204
  // @ts-expect-error runtime type
5073
5205
  isNuxtContentDocumentDriven: !!nuxt.options.content?.documentDriven,
5074
- cacheQueryParams: config.cacheQueryParams ?? false,
5075
5206
  cssFramework: cssFramework || "none",
5076
5207
  // Browser renderer config for cloudflare binding access
5077
5208
  browser: typeof config.browser === "object" ? {
@@ -2260,6 +2260,7 @@ function setupPrerenderHandler(options, resolve, getDetectedRenderers, nuxt = ki
2260
2260
  orphanedOgHashes.push(route.route);
2261
2261
  route.skip = true;
2262
2262
  delete route.error;
2263
+ route.contents = "";
2263
2264
  });
2264
2265
  nitro.hooks.hook("prerender:done", async () => {
2265
2266
  if (orphanedOgHashes.length > 0) {
@@ -2554,6 +2555,94 @@ const ATTRIBUTE_NODE = 6;
2554
2555
  const DIRECTIVE_NODE = 7;
2555
2556
  const RE_PURE_NUMBER = /^\d+(?:\.\d+)?$/;
2556
2557
  const RE_DOUBLE_QUOTE = /"/g;
2558
+ const RE_CAMEL_TO_KEBAB = /[A-Z]/g;
2559
+ const RE_STRIP_QUOTES = /^['"]|['"]$/g;
2560
+ const RE_SINGLE_QUOTED = /^'([^']*)'$/;
2561
+ const RE_DOUBLE_QUOTED = /^"([^"]*)"$/;
2562
+ const RE_BACKTICK_QUOTED = /^`([^`]*)`$/;
2563
+ const RE_CSS_NUMBER = /^[\d.]+(?:px|em|rem|%|vh|vw|ch|ex|vmin|vmax|pt|pc|in|cm|mm)?$/;
2564
+ function camelToKebab(str) {
2565
+ return str.replace(RE_CAMEL_TO_KEBAB, (m) => `-${m.toLowerCase()}`);
2566
+ }
2567
+ function parseStaticStyleObject(expr) {
2568
+ const trimmed = expr.trim();
2569
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
2570
+ return void 0;
2571
+ const inner = trimmed.slice(1, -1).trim();
2572
+ if (!inner)
2573
+ return {};
2574
+ const result = {};
2575
+ let depth = 0;
2576
+ let inQuote = null;
2577
+ let current = "";
2578
+ for (let i = 0; i < inner.length; i++) {
2579
+ const ch = inner[i];
2580
+ if (inQuote) {
2581
+ current += ch;
2582
+ if (ch === inQuote && inner[i - 1] !== "\\")
2583
+ inQuote = null;
2584
+ continue;
2585
+ }
2586
+ if (ch === "'" || ch === '"' || ch === "`") {
2587
+ inQuote = ch;
2588
+ current += ch;
2589
+ continue;
2590
+ }
2591
+ if (ch === "(" || ch === "[" || ch === "{") {
2592
+ depth++;
2593
+ current += ch;
2594
+ continue;
2595
+ }
2596
+ if (ch === ")" || ch === "]" || ch === "}") {
2597
+ depth--;
2598
+ current += ch;
2599
+ continue;
2600
+ }
2601
+ if (ch === "," && depth === 0) {
2602
+ const parsed = parseStyleEntry(current);
2603
+ if (!parsed)
2604
+ return void 0;
2605
+ result[parsed[0]] = parsed[1];
2606
+ current = "";
2607
+ continue;
2608
+ }
2609
+ current += ch;
2610
+ }
2611
+ if (current.trim()) {
2612
+ const parsed = parseStyleEntry(current);
2613
+ if (!parsed)
2614
+ return void 0;
2615
+ result[parsed[0]] = parsed[1];
2616
+ }
2617
+ return result;
2618
+ }
2619
+ function parseStyleEntry(entry) {
2620
+ const colonIdx = entry.indexOf(":");
2621
+ if (colonIdx === -1)
2622
+ return void 0;
2623
+ const rawKey = entry.slice(0, colonIdx).trim();
2624
+ const rawValue = entry.slice(colonIdx + 1).trim();
2625
+ const key = rawKey.replace(RE_STRIP_QUOTES, "");
2626
+ if (!key)
2627
+ return void 0;
2628
+ let value;
2629
+ const singleQuoteMatch = rawValue.match(RE_SINGLE_QUOTED);
2630
+ const doubleQuoteMatch = rawValue.match(RE_DOUBLE_QUOTED);
2631
+ const backtickMatch = rawValue.match(RE_BACKTICK_QUOTED);
2632
+ const numberMatch = rawValue.match(RE_CSS_NUMBER);
2633
+ if (singleQuoteMatch) {
2634
+ value = singleQuoteMatch[1];
2635
+ } else if (doubleQuoteMatch) {
2636
+ value = doubleQuoteMatch[1];
2637
+ } else if (backtickMatch && !backtickMatch[1].includes("${")) {
2638
+ value = backtickMatch[1];
2639
+ } else if (numberMatch) {
2640
+ value = rawValue;
2641
+ } else {
2642
+ return void 0;
2643
+ }
2644
+ return [camelToKebab(key), value];
2645
+ }
2557
2646
  function escapeAttrValue(value) {
2558
2647
  return value.replace(RE_DOUBLE_QUOTE, "&quot;");
2559
2648
  }
@@ -2591,7 +2680,17 @@ async function transformVueTemplate(code, options) {
2591
2680
  }
2592
2681
  }
2593
2682
  if (prop.type === DIRECTIVE_NODE && prop.name === "bind" && prop.arg?.content === "style") {
2594
- collector.hasDynamicStyle = true;
2683
+ const exp = prop.exp?.content;
2684
+ if (exp) {
2685
+ const parsed = parseStaticStyleObject(exp);
2686
+ if (parsed) {
2687
+ collector.dynamicStyleProps = parsed;
2688
+ collector.dynamicStyleLoc = {
2689
+ start: prop.loc.start.offset,
2690
+ end: prop.loc.end.offset
2691
+ };
2692
+ }
2693
+ }
2595
2694
  }
2596
2695
  if (prop.type === ATTRIBUTE_NODE && (prop.name === "width" || prop.name === "height") && prop.value && el.tag === "img") {
2597
2696
  if (!collector.dimensionAttrs)
@@ -2599,7 +2698,7 @@ async function transformVueTemplate(code, options) {
2599
2698
  collector.dimensionAttrs.push({ name: prop.name, value: prop.value.content });
2600
2699
  }
2601
2700
  }
2602
- if (collector.classes.length > 0 || collector.existingStyle || collector.dimensionAttrs?.length) {
2701
+ if (collector.classes.length > 0 || collector.existingStyle || collector.dimensionAttrs?.length || collector.dynamicStyleProps) {
2603
2702
  if (collector.existingStyle && !collector.styleLoc) {
2604
2703
  logger_js.logger.warn(`[vue-template-transform] BUG: existingStyle found but styleLoc is undefined!`);
2605
2704
  }
@@ -2640,6 +2739,9 @@ async function transformVueTemplate(code, options) {
2640
2739
  }
2641
2740
  }
2642
2741
  }
2742
+ if (collector.dynamicStyleProps) {
2743
+ Object.assign(styleProps, collector.dynamicStyleProps);
2744
+ }
2643
2745
  if (styleProps.display === "flex" && !styleProps["flex-direction"]) {
2644
2746
  styleProps["flex-direction"] = "row";
2645
2747
  }
@@ -2648,7 +2750,8 @@ async function transformVueTemplate(code, options) {
2648
2750
  const hasUnresolved = unresolvedClasses.length > 0;
2649
2751
  const resolvedSome = collector.classes.length > 0 && (unresolvedClasses.length < collector.classes.length || hasClassRewrites);
2650
2752
  const styleChanged = collector.existingStyle && styleStr !== collector.existingStyle;
2651
- if (!resolvedSome && !styleChanged && !collector.dimensionAttrs?.length)
2753
+ const hasDynamicStyleResolved = collector.dynamicStyleProps && Object.keys(collector.dynamicStyleProps).length > 0;
2754
+ if (!resolvedSome && !styleChanged && !collector.dimensionAttrs?.length && !hasDynamicStyleResolved)
2652
2755
  continue;
2653
2756
  hasChanges = true;
2654
2757
  if (collector.classLoc) {
@@ -2658,6 +2761,9 @@ async function transformVueTemplate(code, options) {
2658
2761
  s.remove(collector.classLoc.start, collector.classLoc.end);
2659
2762
  }
2660
2763
  }
2764
+ if (collector.dynamicStyleLoc) {
2765
+ s.remove(collector.dynamicStyleLoc.start, collector.dynamicStyleLoc.end);
2766
+ }
2661
2767
  if (hasResolvedStyles) {
2662
2768
  if (collector.styleLoc) {
2663
2769
  s.overwrite(collector.styleLoc.start, collector.styleLoc.end, `style="${styleStr}"`);
@@ -3569,6 +3675,7 @@ const REMOVED_CONFIG = {
3569
3675
  "runtimeSatori": "compatibility.runtime.satori",
3570
3676
  "cacheTtl": "cacheMaxAgeSeconds",
3571
3677
  "cache": "cacheMaxAgeSeconds",
3678
+ "cacheQueryParams": "Removed (all options are encoded in the URL path)",
3572
3679
  "cacheKey": "Removed",
3573
3680
  "static": "zeroRuntime",
3574
3681
  "chromium-node": "compatibility.runtime.browser: 'playwright'",
@@ -3690,6 +3797,7 @@ const DEPRECATED_CONFIG = {
3690
3797
  runtimeSatori: "Use compatibility.runtime.satori",
3691
3798
  cacheTtl: "Use cacheMaxAgeSeconds",
3692
3799
  cache: "Use cacheMaxAgeSeconds",
3800
+ cacheQueryParams: "Removed (all options are encoded in the URL path)",
3693
3801
  cacheKey: "Removed",
3694
3802
  static: "Removed - use zeroRuntime",
3695
3803
  fonts: "Use @nuxt/fonts module instead"
@@ -4206,7 +4314,7 @@ const module$1 = kit.defineNuxtModule({
4206
4314
  await onUpgrade(nuxt, options, previousVersion);
4207
4315
  },
4208
4316
  async setup(config, nuxt) {
4209
- const _resolver = kit.createResolver((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/nuxt-og-image.DD0uPnWP.cjs', document.baseURI).href)));
4317
+ const _resolver = kit.createResolver((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/nuxt-og-image.if6TuaSI.cjs', document.baseURI).href)));
4210
4318
  const fixSharedPath = (p) => {
4211
4319
  if (p.includes("/shared/runtime/"))
4212
4320
  return p.replace("/shared/runtime/", "/runtime/");
@@ -4236,6 +4344,10 @@ const module$1 = kit.defineNuxtModule({
4236
4344
  logger_js.logger.warn("Nuxt OG Image is enabled but SSR is disabled.\n\nYou should enable SSR (`ssr: true`) or disable the module (`ogImage: { enabled: false }`).");
4237
4345
  return;
4238
4346
  }
4347
+ if (config.cacheMaxAgeSeconds != null) {
4348
+ config.defaults = config.defaults || {};
4349
+ config.defaults.cacheMaxAgeSeconds = config.cacheMaxAgeSeconds;
4350
+ }
4239
4351
  if (config.debug && !nuxt.options.dev) {
4240
4352
  logger_js.logger.warn("`ogImage.debug` is enabled in production. This exposes the `/_og/debug.json` endpoint and should not be enabled in production. Disable it before deploying.");
4241
4353
  }
@@ -4641,6 +4753,26 @@ ${familyInfo}`);
4641
4753
  route: "/_og/s/**",
4642
4754
  handler: resolve(`${basePath}/image`)
4643
4755
  });
4756
+ if (!nuxt.options.dev) {
4757
+ nuxt.options.routeRules = nuxt.options.routeRules || {};
4758
+ if (config.runtimeCacheStorage !== false && !nuxt.options.test) {
4759
+ const ogDynamicRule = nuxt.options.routeRules["/_og/d/**"];
4760
+ if (!ogDynamicRule?.swr && !ogDynamicRule?.isr && !ogDynamicRule?.cache && !ogDynamicRule?.headers) {
4761
+ const ttl = config.cacheMaxAgeSeconds ?? config.defaults?.cacheMaxAgeSeconds ?? 60 * 60 * 24 * 3;
4762
+ nuxt.options.routeRules["/_og/d/**"] = defu.defu(
4763
+ nuxt.options.routeRules["/_og/d/**"] || {},
4764
+ { swr: ttl }
4765
+ );
4766
+ }
4767
+ }
4768
+ const ogStaticRule = nuxt.options.routeRules["/_og/s/**"];
4769
+ if (!ogStaticRule?.swr && !ogStaticRule?.isr && !ogStaticRule?.cache && !ogStaticRule?.headers) {
4770
+ nuxt.options.routeRules["/_og/s/**"] = defu.defu(
4771
+ nuxt.options.routeRules["/_og/s/**"] || {},
4772
+ { headers: { "cache-control": "public, max-age=31536000, immutable" } }
4773
+ );
4774
+ }
4775
+ }
4644
4776
  if (!nuxt.options.dev) {
4645
4777
  nuxt.options.optimization.treeShake.composables.client["nuxt-og-image"] = [];
4646
4778
  }
@@ -5091,7 +5223,6 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5091
5223
  colorPreference,
5092
5224
  // @ts-expect-error runtime type
5093
5225
  isNuxtContentDocumentDriven: !!nuxt.options.content?.documentDriven,
5094
- cacheQueryParams: config.cacheQueryParams ?? false,
5095
5226
  cssFramework: cssFramework || "none",
5096
5227
  // Browser renderer config for cloudflare binding access
5097
5228
  browser: typeof config.browser === "object" ? {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-og-image",
3
3
  "type": "module",
4
- "version": "6.3.6",
4
+ "version": "6.3.8",
5
5
  "description": "Enlightened OG Image generation for Nuxt.",
6
6
  "author": {
7
7
  "website": "https://harlanzw.com",
@@ -135,8 +135,8 @@
135
135
  "@iconify-json/tabler": "^1.2.33",
136
136
  "@img/sharp-linux-x64": "^0.34.5",
137
137
  "@nuxt/content": "^3.12.0",
138
- "@nuxt/devtools": "^3.2.4",
139
- "@nuxt/devtools-kit": "^3.2.4",
138
+ "@nuxt/devtools": "4.0.0-alpha.4",
139
+ "@nuxt/devtools-kit": "4.0.0-alpha.4",
140
140
  "@nuxt/fonts": "^0.14.0",
141
141
  "@nuxt/icon": "^2.2.1",
142
142
  "@nuxt/image": "^2.0.0",
@@ -152,8 +152,8 @@
152
152
  "@shikijs/langs": "^4.0.2",
153
153
  "@shikijs/themes": "^4.0.2",
154
154
  "@tailwindcss/vite": "^4.2.2",
155
- "@takumi-rs/core": "1.0.0-beta.13",
156
- "@takumi-rs/wasm": "1.0.0-beta.13",
155
+ "@takumi-rs/core": "1.0.1",
156
+ "@takumi-rs/wasm": "1.0.1",
157
157
  "@unocss/nuxt": "^66.6.8",
158
158
  "@vitejs/plugin-vue": "^6.0.5",
159
159
  "@vueuse/nuxt": "^14.2.1",
@@ -1 +0,0 @@
1
- .section-block[data-v-959e3b7d]{background:var(--color-surface-elevated);border:1px solid var(--color-border);border-radius:var(--radius-lg);transition:border-color .2s;overflow:hidden}.section-block[data-v-959e3b7d]:hover{border-color:var(--color-neutral-300)}.dark .section-block[data-v-959e3b7d]:hover{border-color:var(--color-neutral-700)}.section-header[data-v-959e3b7d]{cursor:pointer;-webkit-user-select:none;user-select:none;padding:.875rem 1rem;list-style:none;transition:background .15s}.section-header[data-v-959e3b7d]::-webkit-details-marker{display:none}.section-header[data-v-959e3b7d]:hover{background:var(--color-surface-sunken)}details[open] .section-header[data-v-959e3b7d]{border-bottom:1px solid var(--color-border)}.section-title[data-v-959e3b7d]{align-items:center;gap:.625rem;transition:opacity .15s;display:flex}.section-icon[data-v-959e3b7d]{color:var(--color-text-muted);flex-shrink:0;font-size:1.125rem}.section-label[data-v-959e3b7d]{color:var(--color-text);font-size:.875rem;font-weight:600}.section-description[data-v-959e3b7d]{color:var(--color-text-muted);margin-top:.125rem;font-size:.75rem}.chevron[data-v-959e3b7d]{color:var(--color-text-subtle);flex-shrink:0;font-size:.875rem;transition:transform .2s cubic-bezier(.22,1,.36,1)}details[open] .chevron[data-v-959e3b7d]{color:var(--color-text-muted);transform:rotate(180deg)}.section-content[data-v-959e3b7d]{background:var(--color-surface-sunken);flex-direction:column;gap:.75rem;padding:1rem;display:flex}
@@ -1 +0,0 @@
1
- .devtools-snippet[data-v-97299906]{margin:.5rem .75rem}.devtools-snippet-header[data-v-97299906]{justify-content:space-between;align-items:center;margin-bottom:.375rem;display:flex}.devtools-snippet-label[data-v-97299906]{color:var(--color-text-muted);font-family:var(--font-mono);letter-spacing:-.01em;font-size:.6875rem;font-weight:500}.devtools-snippet-block[data-v-97299906]{border-radius:var(--radius-sm);max-height:300px;font-size:.6875rem;line-height:1.6;overflow-y:auto;padding:.5rem .625rem!important}