nuxt-og-image 6.3.7 → 6.3.9

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/df0ad51c-c846-4813-9833-34c1d7a55599.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/server/og-image/bindings/font-assets/node.js +6 -3
  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.Blp5UsK8.mjs → nuxt-og-image.caStlBVv.mjs} +134 -4
  40. package/dist/shared/{nuxt-og-image.BSlBlAci.cjs → nuxt-og-image.if6TuaSI.cjs} +135 -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/8d1f6507-8803-4bf0-b0a0-b07d9247f574.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.7",
7
+ "version": "6.3.9",
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.Blp5UsK8.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';
@@ -6,9 +6,12 @@ export async function resolve(event, font) {
6
6
  const { app } = useRuntimeConfig();
7
7
  const fullPath = withBase(path, app.baseURL);
8
8
  const origin = getNitroOrigin(event);
9
- const res = await fetch(new URL(fullPath, origin).href);
10
- if (res.ok) {
9
+ const res = await fetch(new URL(fullPath, origin).href).catch(() => null);
10
+ if (res?.ok) {
11
11
  return Buffer.from(await res.arrayBuffer());
12
12
  }
13
- throw new Error(`[Nuxt OG Image] Failed to resolve font: ${path}`);
13
+ const arrayBuffer = await event.$fetch(fullPath, {
14
+ responseType: "arrayBuffer"
15
+ });
16
+ return Buffer.from(arrayBuffer);
14
17
  }
@@ -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) */
@@ -2535,6 +2535,94 @@ const ATTRIBUTE_NODE = 6;
2535
2535
  const DIRECTIVE_NODE = 7;
2536
2536
  const RE_PURE_NUMBER = /^\d+(?:\.\d+)?$/;
2537
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
+ }
2538
2626
  function escapeAttrValue(value) {
2539
2627
  return value.replace(RE_DOUBLE_QUOTE, "&quot;");
2540
2628
  }
@@ -2572,7 +2660,17 @@ async function transformVueTemplate(code, options) {
2572
2660
  }
2573
2661
  }
2574
2662
  if (prop.type === DIRECTIVE_NODE && prop.name === "bind" && prop.arg?.content === "style") {
2575
- 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
+ }
2576
2674
  }
2577
2675
  if (prop.type === ATTRIBUTE_NODE && (prop.name === "width" || prop.name === "height") && prop.value && el.tag === "img") {
2578
2676
  if (!collector.dimensionAttrs)
@@ -2580,7 +2678,7 @@ async function transformVueTemplate(code, options) {
2580
2678
  collector.dimensionAttrs.push({ name: prop.name, value: prop.value.content });
2581
2679
  }
2582
2680
  }
2583
- if (collector.classes.length > 0 || collector.existingStyle || collector.dimensionAttrs?.length) {
2681
+ if (collector.classes.length > 0 || collector.existingStyle || collector.dimensionAttrs?.length || collector.dynamicStyleProps) {
2584
2682
  if (collector.existingStyle && !collector.styleLoc) {
2585
2683
  logger.warn(`[vue-template-transform] BUG: existingStyle found but styleLoc is undefined!`);
2586
2684
  }
@@ -2621,6 +2719,9 @@ async function transformVueTemplate(code, options) {
2621
2719
  }
2622
2720
  }
2623
2721
  }
2722
+ if (collector.dynamicStyleProps) {
2723
+ Object.assign(styleProps, collector.dynamicStyleProps);
2724
+ }
2624
2725
  if (styleProps.display === "flex" && !styleProps["flex-direction"]) {
2625
2726
  styleProps["flex-direction"] = "row";
2626
2727
  }
@@ -2629,7 +2730,8 @@ async function transformVueTemplate(code, options) {
2629
2730
  const hasUnresolved = unresolvedClasses.length > 0;
2630
2731
  const resolvedSome = collector.classes.length > 0 && (unresolvedClasses.length < collector.classes.length || hasClassRewrites);
2631
2732
  const styleChanged = collector.existingStyle && styleStr !== collector.existingStyle;
2632
- 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)
2633
2735
  continue;
2634
2736
  hasChanges = true;
2635
2737
  if (collector.classLoc) {
@@ -2639,6 +2741,9 @@ async function transformVueTemplate(code, options) {
2639
2741
  s.remove(collector.classLoc.start, collector.classLoc.end);
2640
2742
  }
2641
2743
  }
2744
+ if (collector.dynamicStyleLoc) {
2745
+ s.remove(collector.dynamicStyleLoc.start, collector.dynamicStyleLoc.end);
2746
+ }
2642
2747
  if (hasResolvedStyles) {
2643
2748
  if (collector.styleLoc) {
2644
2749
  s.overwrite(collector.styleLoc.start, collector.styleLoc.end, `style="${styleStr}"`);
@@ -3550,6 +3655,7 @@ const REMOVED_CONFIG = {
3550
3655
  "runtimeSatori": "compatibility.runtime.satori",
3551
3656
  "cacheTtl": "cacheMaxAgeSeconds",
3552
3657
  "cache": "cacheMaxAgeSeconds",
3658
+ "cacheQueryParams": "Removed (all options are encoded in the URL path)",
3553
3659
  "cacheKey": "Removed",
3554
3660
  "static": "zeroRuntime",
3555
3661
  "chromium-node": "compatibility.runtime.browser: 'playwright'",
@@ -3671,6 +3777,7 @@ const DEPRECATED_CONFIG = {
3671
3777
  runtimeSatori: "Use compatibility.runtime.satori",
3672
3778
  cacheTtl: "Use cacheMaxAgeSeconds",
3673
3779
  cache: "Use cacheMaxAgeSeconds",
3780
+ cacheQueryParams: "Removed (all options are encoded in the URL path)",
3674
3781
  cacheKey: "Removed",
3675
3782
  static: "Removed - use zeroRuntime",
3676
3783
  fonts: "Use @nuxt/fonts module instead"
@@ -4217,6 +4324,10 @@ const module$1 = defineNuxtModule({
4217
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 }`).");
4218
4325
  return;
4219
4326
  }
4327
+ if (config.cacheMaxAgeSeconds != null) {
4328
+ config.defaults = config.defaults || {};
4329
+ config.defaults.cacheMaxAgeSeconds = config.cacheMaxAgeSeconds;
4330
+ }
4220
4331
  if (config.debug && !nuxt.options.dev) {
4221
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.");
4222
4333
  }
@@ -4622,6 +4733,26 @@ ${familyInfo}`);
4622
4733
  route: "/_og/s/**",
4623
4734
  handler: resolve(`${basePath}/image`)
4624
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
+ }
4625
4756
  if (!nuxt.options.dev) {
4626
4757
  nuxt.options.optimization.treeShake.composables.client["nuxt-og-image"] = [];
4627
4758
  }
@@ -5072,7 +5203,6 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5072
5203
  colorPreference,
5073
5204
  // @ts-expect-error runtime type
5074
5205
  isNuxtContentDocumentDriven: !!nuxt.options.content?.documentDriven,
5075
- cacheQueryParams: config.cacheQueryParams ?? false,
5076
5206
  cssFramework: cssFramework || "none",
5077
5207
  // Browser renderer config for cloudflare binding access
5078
5208
  browser: typeof config.browser === "object" ? {
@@ -2555,6 +2555,94 @@ const ATTRIBUTE_NODE = 6;
2555
2555
  const DIRECTIVE_NODE = 7;
2556
2556
  const RE_PURE_NUMBER = /^\d+(?:\.\d+)?$/;
2557
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
+ }
2558
2646
  function escapeAttrValue(value) {
2559
2647
  return value.replace(RE_DOUBLE_QUOTE, "&quot;");
2560
2648
  }
@@ -2592,7 +2680,17 @@ async function transformVueTemplate(code, options) {
2592
2680
  }
2593
2681
  }
2594
2682
  if (prop.type === DIRECTIVE_NODE && prop.name === "bind" && prop.arg?.content === "style") {
2595
- 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
+ }
2596
2694
  }
2597
2695
  if (prop.type === ATTRIBUTE_NODE && (prop.name === "width" || prop.name === "height") && prop.value && el.tag === "img") {
2598
2696
  if (!collector.dimensionAttrs)
@@ -2600,7 +2698,7 @@ async function transformVueTemplate(code, options) {
2600
2698
  collector.dimensionAttrs.push({ name: prop.name, value: prop.value.content });
2601
2699
  }
2602
2700
  }
2603
- if (collector.classes.length > 0 || collector.existingStyle || collector.dimensionAttrs?.length) {
2701
+ if (collector.classes.length > 0 || collector.existingStyle || collector.dimensionAttrs?.length || collector.dynamicStyleProps) {
2604
2702
  if (collector.existingStyle && !collector.styleLoc) {
2605
2703
  logger_js.logger.warn(`[vue-template-transform] BUG: existingStyle found but styleLoc is undefined!`);
2606
2704
  }
@@ -2641,6 +2739,9 @@ async function transformVueTemplate(code, options) {
2641
2739
  }
2642
2740
  }
2643
2741
  }
2742
+ if (collector.dynamicStyleProps) {
2743
+ Object.assign(styleProps, collector.dynamicStyleProps);
2744
+ }
2644
2745
  if (styleProps.display === "flex" && !styleProps["flex-direction"]) {
2645
2746
  styleProps["flex-direction"] = "row";
2646
2747
  }
@@ -2649,7 +2750,8 @@ async function transformVueTemplate(code, options) {
2649
2750
  const hasUnresolved = unresolvedClasses.length > 0;
2650
2751
  const resolvedSome = collector.classes.length > 0 && (unresolvedClasses.length < collector.classes.length || hasClassRewrites);
2651
2752
  const styleChanged = collector.existingStyle && styleStr !== collector.existingStyle;
2652
- 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)
2653
2755
  continue;
2654
2756
  hasChanges = true;
2655
2757
  if (collector.classLoc) {
@@ -2659,6 +2761,9 @@ async function transformVueTemplate(code, options) {
2659
2761
  s.remove(collector.classLoc.start, collector.classLoc.end);
2660
2762
  }
2661
2763
  }
2764
+ if (collector.dynamicStyleLoc) {
2765
+ s.remove(collector.dynamicStyleLoc.start, collector.dynamicStyleLoc.end);
2766
+ }
2662
2767
  if (hasResolvedStyles) {
2663
2768
  if (collector.styleLoc) {
2664
2769
  s.overwrite(collector.styleLoc.start, collector.styleLoc.end, `style="${styleStr}"`);
@@ -3570,6 +3675,7 @@ const REMOVED_CONFIG = {
3570
3675
  "runtimeSatori": "compatibility.runtime.satori",
3571
3676
  "cacheTtl": "cacheMaxAgeSeconds",
3572
3677
  "cache": "cacheMaxAgeSeconds",
3678
+ "cacheQueryParams": "Removed (all options are encoded in the URL path)",
3573
3679
  "cacheKey": "Removed",
3574
3680
  "static": "zeroRuntime",
3575
3681
  "chromium-node": "compatibility.runtime.browser: 'playwright'",
@@ -3691,6 +3797,7 @@ const DEPRECATED_CONFIG = {
3691
3797
  runtimeSatori: "Use compatibility.runtime.satori",
3692
3798
  cacheTtl: "Use cacheMaxAgeSeconds",
3693
3799
  cache: "Use cacheMaxAgeSeconds",
3800
+ cacheQueryParams: "Removed (all options are encoded in the URL path)",
3694
3801
  cacheKey: "Removed",
3695
3802
  static: "Removed - use zeroRuntime",
3696
3803
  fonts: "Use @nuxt/fonts module instead"
@@ -4207,7 +4314,7 @@ const module$1 = kit.defineNuxtModule({
4207
4314
  await onUpgrade(nuxt, options, previousVersion);
4208
4315
  },
4209
4316
  async setup(config, nuxt) {
4210
- 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.BSlBlAci.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)));
4211
4318
  const fixSharedPath = (p) => {
4212
4319
  if (p.includes("/shared/runtime/"))
4213
4320
  return p.replace("/shared/runtime/", "/runtime/");
@@ -4237,6 +4344,10 @@ const module$1 = kit.defineNuxtModule({
4237
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 }`).");
4238
4345
  return;
4239
4346
  }
4347
+ if (config.cacheMaxAgeSeconds != null) {
4348
+ config.defaults = config.defaults || {};
4349
+ config.defaults.cacheMaxAgeSeconds = config.cacheMaxAgeSeconds;
4350
+ }
4240
4351
  if (config.debug && !nuxt.options.dev) {
4241
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.");
4242
4353
  }
@@ -4642,6 +4753,26 @@ ${familyInfo}`);
4642
4753
  route: "/_og/s/**",
4643
4754
  handler: resolve(`${basePath}/image`)
4644
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
+ }
4645
4776
  if (!nuxt.options.dev) {
4646
4777
  nuxt.options.optimization.treeShake.composables.client["nuxt-og-image"] = [];
4647
4778
  }
@@ -5092,7 +5223,6 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5092
5223
  colorPreference,
5093
5224
  // @ts-expect-error runtime type
5094
5225
  isNuxtContentDocumentDriven: !!nuxt.options.content?.documentDriven,
5095
- cacheQueryParams: config.cacheQueryParams ?? false,
5096
5226
  cssFramework: cssFramework || "none",
5097
5227
  // Browser renderer config for cloudflare binding access
5098
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.7",
4
+ "version": "6.3.9",
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}
@@ -1 +0,0 @@
1
- {"id":"8d1f6507-8803-4bf0-b0a0-b07d9247f574","timestamp":1775803048648,"prerendered":[]}
@@ -1 +0,0 @@
1
- .devtools-alert[data-v-9b9ce76b]{border-bottom:1px solid var(--color-border);align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.8125rem;display:flex}.devtools-alert-icon[data-v-9b9ce76b]{flex-shrink:0}.devtools-alert-content[data-v-9b9ce76b]{flex:1;min-width:0}.devtools-alert-action[data-v-9b9ce76b]{flex-shrink:0;margin-left:auto}.devtools-alert-info[data-v-9b9ce76b]{color:oklch(55% .12 230);background:oklch(85% .1 230/.1);border-bottom-color:oklch(75% .1 230/.2)}.dark .devtools-alert-info[data-v-9b9ce76b]{color:oklch(80% .1 230);background:oklch(45% .1 230/.15)}.devtools-alert-error[data-v-9b9ce76b]{color:oklch(52% .18 25);background:oklch(65% .18 25/.1);border-bottom-color:oklch(55% .15 25/.25)}.dark .devtools-alert-error[data-v-9b9ce76b]{color:oklch(75% .14 25);background:oklch(40% .14 25/.18)}.devtools-alert-warning[data-v-9b9ce76b]{color:oklch(55% .15 85);background:oklch(85% .12 85/.1);border-bottom-color:oklch(75% .12 85/.2)}.dark .devtools-alert-warning[data-v-9b9ce76b]{color:oklch(80% .12 85);background:oklch(45% .12 85/.15)}.devtools-alert-success[data-v-9b9ce76b]{color:oklch(50% .15 145);background:oklch(75% .15 145/.12);border-bottom-color:oklch(65% .12 145/.2)}.dark .devtools-alert-success[data-v-9b9ce76b]{color:oklch(75% .18 145);background:oklch(50% .15 145/.15)}.devtools-alert-production[data-v-9b9ce76b]{color:oklch(45% .15 145);background:oklch(85% .12 145/.1);border-bottom-color:oklch(75% .12 145/.2)}.dark .devtools-alert-production[data-v-9b9ce76b]{color:oklch(75% .12 145);background:oklch(35% .08 145/.15)}