nuxt-og-image 6.10.3 → 7.0.0

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 (103) hide show
  1. package/README.md +10 -14
  2. package/dist/chunks/tw4.mjs +6 -6
  3. package/dist/chunks/uno.mjs +7 -7
  4. package/dist/cli.mjs +4 -4
  5. package/dist/devtools/components/og-image/RendererSelectModal.vue +1 -1
  6. package/dist/devtools/lib/og-image/fonts.ts +38 -0
  7. package/dist/devtools/lib/og-image/types.ts +7 -0
  8. package/dist/devtools/pages/og-image/index.vue +19 -14
  9. package/dist/module.d.mts +14 -10
  10. package/dist/module.d.ts +14 -10
  11. package/dist/module.json +2 -2
  12. package/dist/module.mjs +6 -6
  13. package/dist/runtime/app/client-utils.js +7 -4
  14. package/dist/runtime/app/components/Templates/Community/SaaS.satori.vue +1 -1
  15. package/dist/runtime/app/components/Templates/Community/SimpleBlog.satori.vue +1 -1
  16. package/dist/runtime/app/disabled/components.d.ts +1 -0
  17. package/dist/runtime/app/disabled/components.js +0 -0
  18. package/dist/runtime/app/disabled/composables/defineOgImage.d.ts +1 -0
  19. package/dist/runtime/app/disabled/composables/defineOgImage.js +1 -0
  20. package/dist/runtime/app/disabled/composables/defineOgImageComponent.d.ts +1 -0
  21. package/dist/runtime/app/disabled/composables/defineOgImageComponent.js +1 -0
  22. package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.d.ts +1 -0
  23. package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.js +1 -0
  24. package/dist/runtime/app/disabled/composables/mock.d.ts +1 -0
  25. package/dist/runtime/app/disabled/composables/mock.js +1 -0
  26. package/dist/runtime/app/disabled/index.d.ts +3 -0
  27. package/dist/runtime/app/disabled/index.js +3 -0
  28. package/dist/runtime/app/index.d.ts +4 -0
  29. package/dist/runtime/app/index.js +4 -0
  30. package/dist/runtime/app/utils/plugins.js +1 -1
  31. package/dist/runtime/app/utils.js +2 -2
  32. package/dist/runtime/server/index.d.ts +1 -0
  33. package/dist/runtime/server/index.js +1 -0
  34. package/dist/runtime/server/middleware/signing.d.ts +2 -0
  35. package/dist/runtime/server/middleware/signing.js +19 -0
  36. package/dist/runtime/server/nitro.d.ts +3 -3
  37. package/dist/runtime/server/nitro.js +4 -4
  38. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +8 -49
  39. package/dist/runtime/server/og-image/bindings/font-assets/node.js +3 -2
  40. package/dist/runtime/server/og-image/bindings/satori/node.d.ts +1 -2
  41. package/dist/runtime/server/og-image/bindings/satori/wasm-fs.d.ts +1 -2
  42. package/dist/runtime/server/og-image/bindings/satori/wasm-fs.js +1 -1
  43. package/dist/runtime/server/og-image/bindings/satori/wasm.d.ts +1 -2
  44. package/dist/runtime/server/og-image/bindings/satori/wasm.js +1 -1
  45. package/dist/runtime/server/og-image/browser/screenshot.js +5 -3
  46. package/dist/runtime/server/og-image/cache/publish.d.ts +43 -0
  47. package/dist/runtime/server/og-image/cache/publish.js +91 -0
  48. package/dist/runtime/server/og-image/cache/published.d.ts +17 -0
  49. package/dist/runtime/server/og-image/cache/published.js +49 -0
  50. package/dist/runtime/server/og-image/context.js +1 -1
  51. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -5
  52. package/dist/runtime/server/og-image/core/transforms/emojis/index.d.ts +3 -2
  53. package/dist/runtime/server/og-image/core/vnodes.js +19 -1
  54. package/dist/runtime/server/og-image/font-source.d.ts +1 -0
  55. package/dist/runtime/server/og-image/font-source.js +1 -0
  56. package/dist/runtime/server/og-image/fonts.d.ts +1 -1
  57. package/dist/runtime/server/og-image/fonts.js +1 -1
  58. package/dist/runtime/server/og-image/satori/filters.d.ts +1 -0
  59. package/dist/runtime/server/og-image/satori/filters.js +14 -0
  60. package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
  61. package/dist/runtime/server/og-image/satori/plugins/classes.js +0 -7
  62. package/dist/runtime/server/og-image/satori/renderer.js +2 -1
  63. package/dist/runtime/server/og-image/satori/vnodes.js +1 -4
  64. package/dist/runtime/server/og-image/takumi/renderer.js +11 -47
  65. package/dist/runtime/server/og-image/utils/css.d.ts +0 -5
  66. package/dist/runtime/server/og-image/utils/css.js +0 -5
  67. package/dist/runtime/server/plugins/auto-eject-nitro3.d.ts +2 -0
  68. package/dist/runtime/server/plugins/auto-eject-nitro3.js +9 -0
  69. package/dist/runtime/server/plugins/auto-eject.js +3 -31
  70. package/dist/runtime/server/plugins/prerender.js +68 -2
  71. package/dist/runtime/server/plugins/publish.d.ts +2 -0
  72. package/dist/runtime/server/plugins/publish.js +44 -0
  73. package/dist/runtime/server/routes/debug.json.d.ts +10 -1
  74. package/dist/runtime/server/routes/debug.json.js +4 -4
  75. package/dist/runtime/server/routes/static-font.d.ts +2 -0
  76. package/dist/runtime/server/routes/static-font.js +22 -0
  77. package/dist/runtime/server/signing-secret.d.ts +8 -0
  78. package/dist/runtime/server/signing-secret.js +75 -0
  79. package/dist/runtime/server/util/auto-eject-lifecycle.d.ts +13 -0
  80. package/dist/runtime/server/util/auto-eject-lifecycle.js +31 -0
  81. package/dist/runtime/server/util/cache.d.ts +1 -0
  82. package/dist/runtime/server/util/cache.js +12 -6
  83. package/dist/runtime/server/util/cloudflare.d.ts +2 -2
  84. package/dist/runtime/server/util/eventHandlers.js +69 -3
  85. package/dist/runtime/server/util/fetchLocalAsset.js +2 -2
  86. package/dist/runtime/server/util/origin.d.ts +3 -0
  87. package/dist/runtime/server/util/origin.js +4 -0
  88. package/dist/runtime/server/util/publishWarnings.d.ts +6 -0
  89. package/dist/runtime/server/util/publishWarnings.js +6 -0
  90. package/dist/runtime/server/utils.d.ts +3 -3
  91. package/dist/runtime/server/utils.js +5 -10
  92. package/dist/runtime/shared/publish.d.ts +13 -0
  93. package/dist/runtime/shared/publish.js +17 -0
  94. package/dist/runtime/shared/urlEncoding.js +9 -4
  95. package/dist/runtime/types.d.ts +11 -16
  96. package/dist/shared/{nuxt-og-image.Bcegsloy.mjs → nuxt-og-image.DR_SxdDv.mjs} +1224 -1109
  97. package/package.json +60 -62
  98. package/skills/nuxt-og-image/SKILL.md +31 -14
  99. package/types/virtual.d.ts +8 -0
  100. package/dist/runtime/server/og-image/satori/plugins/emojis.d.ts +0 -2
  101. package/dist/runtime/server/og-image/satori/plugins/emojis.js +0 -112
  102. package/dist/runtime/server/og-image/satori/plugins/flex.d.ts +0 -2
  103. package/dist/runtime/server/og-image/satori/plugins/flex.js +0 -48
@@ -1,11 +1,11 @@
1
1
  import { defu } from "defu";
2
2
  import { withBase } from "ufo";
3
- import compatibility from "#og-image/compatibility";
4
3
  import { logger } from "../../../logger.js";
5
4
  import { fetchLocalAsset } from "../../util/fetchLocalAsset.js";
6
5
  import { getFetchTimeout } from "../../util/fetchTimeout.js";
7
6
  import { fetchWithRedirectValidation, isBlockedUrl } from "../../util/ssrf.js";
8
7
  import { withTimeout } from "../../util/withTimeout.js";
8
+ import { TAKUMI_FORMATS } from "../font-source.js";
9
9
  import { buildSubsetFamilyChain, extractCodepoints, getDefaultFontFamily, loadFontsForRenderer, resolveSubsetChain } from "../fonts.js";
10
10
  import { getExtractResourceUrls, getTakumi } from "./instances.js";
11
11
  import { createTakumiNodes } from "./nodes.js";
@@ -17,8 +17,6 @@ async function getTakumiState(event) {
17
17
  const Renderer = await getTakumi();
18
18
  nitro._takumiState = {
19
19
  renderer: new Renderer(),
20
- loadedFontKeys: /* @__PURE__ */ new Set(),
21
- loadedFamilies: /* @__PURE__ */ new Set(),
22
20
  lock: Promise.resolve()
23
21
  };
24
22
  return nitro._takumiState;
@@ -40,8 +38,6 @@ function withTakumiLock(state, timeoutMs, fn, onLockTimeout) {
40
38
  try {
41
39
  const Renderer = await getTakumi();
42
40
  state.renderer = new Renderer();
43
- state.loadedFontKeys.clear();
44
- state.loadedFamilies.clear();
45
41
  } catch (resetErr) {
46
42
  logger.warn(`failed to reset takumi renderer after lock timeout: ${resetErr?.message || resetErr}`);
47
43
  }
@@ -106,39 +102,15 @@ function toUint8Array(data) {
106
102
  return new Uint8Array(data);
107
103
  return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
108
104
  }
109
- function createFontDescriptor(entry, includeKey = false) {
105
+ function createFontDescriptor(entry) {
110
106
  return {
111
- ...includeKey ? { key: entry.binaryKey } : {},
107
+ key: entry.binaryKey,
112
108
  name: entry.family,
113
109
  data: toUint8Array(entry.data),
114
110
  ...entry.weight !== void 0 ? { weight: entry.weight } : {},
115
111
  ...entry.style ? { style: entry.style } : {}
116
112
  };
117
113
  }
118
- function addLoadedFamilies(state, entry, registeredFamilies) {
119
- if (Array.isArray(registeredFamilies) && registeredFamilies.length) {
120
- for (const family of registeredFamilies) {
121
- const name = family?.name;
122
- if (typeof name === "string")
123
- state.loadedFamilies.add(name);
124
- }
125
- } else {
126
- state.loadedFamilies.add(entry.family);
127
- }
128
- }
129
- async function loadFontsIntoRenderer(state, entries) {
130
- for (const entry of entries) {
131
- if (state.loadedFontKeys.has(entry.binaryKey))
132
- continue;
133
- try {
134
- const registeredFamilies = await state.renderer.loadFont(createFontDescriptor(entry));
135
- addLoadedFamilies(state, entry, registeredFamilies);
136
- } catch (err) {
137
- logger.warn(`Failed to load font "${entry.family}" with takumi renderer: ${err.message}`);
138
- }
139
- state.loadedFontKeys.add(entry.binaryKey);
140
- }
141
- }
142
114
  function normalizeImageSources(images) {
143
115
  return (images || []).map((image) => ({
144
116
  src: image.src,
@@ -183,11 +155,10 @@ function rewriteFontFamilies(node, loadedFamilies, subsetChains) {
183
155
  }
184
156
  async function createImage(event, format) {
185
157
  const { options, timings } = event;
186
- const isTakumiV2 = compatibility.takumiVersion === 2;
187
158
  const { fontFamilyOverride, defaultFont } = getDefaultFontFamily(options);
188
159
  const nodes = await createTakumiNodes(event);
189
160
  const codepoints = extractCodepoints(nodes);
190
- const fonts = await timings.measure("font-load", () => loadFontsForRenderer(event, { supportedFormats: /* @__PURE__ */ new Set(["ttf", "woff2"]), component: options.component, fontFamilyOverride: fontFamilyOverride || defaultFont, codepoints }));
161
+ const fonts = await timings.measure("font-load", () => loadFontsForRenderer(event, { supportedFormats: TAKUMI_FORMATS, component: options.component, fontFamilyOverride: fontFamilyOverride || defaultFont, codepoints }));
191
162
  const fontEntries = dedupeFontsByBinary(fonts);
192
163
  const hookTimeout = event.runtimeConfig.security?.renderTimeout ?? 15e3;
193
164
  await withTimeout(
@@ -200,11 +171,9 @@ async function createImage(event, format) {
200
171
  const extractResourceUrls = await timings.measure("takumi-extract-init", () => getExtractResourceUrls());
201
172
  const resourceUrls = await extractResourceUrls(nodes);
202
173
  const baseURL = event.runtimeConfig.app.baseURL;
203
- const { images: optionImages, fetchedResources: optionFetchedResources, persistentImages, ...takumiOptions } = options.takumi || {};
174
+ const { images: optionImages, ...takumiOptions } = options.takumi || {};
204
175
  const images = [
205
- ...normalizeImageSources(optionImages),
206
- ...normalizeImageSources(optionFetchedResources),
207
- ...normalizeImageSources(persistentImages)
176
+ ...normalizeImageSources(optionImages)
208
177
  ];
209
178
  if (resourceUrls.length) {
210
179
  const fetchTimeout = getFetchTimeout(event.runtimeConfig);
@@ -245,7 +214,7 @@ async function createImage(event, format) {
245
214
  width: Math.min(Number(options.width) * dpr, maxDim),
246
215
  height: Math.min(Number(options.height) * dpr, maxDim),
247
216
  format,
248
- ...isTakumiV2 ? { images } : { fetchedResources: images },
217
+ images,
249
218
  devicePixelRatio: dpr
250
219
  });
251
220
  const lockTimeout = event.runtimeConfig.security?.renderTimeout ?? 15e3;
@@ -253,14 +222,9 @@ async function createImage(event, format) {
253
222
  return await withTakumiLock(state, lockTimeout, () => {
254
223
  endLockWait();
255
224
  return timings.measure("render-takumi", async () => {
256
- let loadedFamilies = state.loadedFamilies;
257
- if (isTakumiV2) {
258
- loadedFamilies = new Set(fontEntries.map((entry) => entry.family));
259
- renderOptions.fonts = fontEntries.map((entry) => createFontDescriptor(entry, true));
260
- renderOptions.fontFamilies = [...loadedFamilies];
261
- } else {
262
- await loadFontsIntoRenderer(state, fontEntries);
263
- }
225
+ const loadedFamilies = new Set(fontEntries.map((entry) => entry.family));
226
+ renderOptions.fonts = fontEntries.map((entry) => createFontDescriptor(entry));
227
+ renderOptions.fontFamilies = [...loadedFamilies];
264
228
  const rootStyle = nodes.style ?? {};
265
229
  if (fontFamilyOverride) {
266
230
  const chain = subsetChains.get(fontFamilyOverride);
@@ -293,7 +257,7 @@ const TakumiRenderer = {
293
257
  async debug(e) {
294
258
  const [vnodes, fonts] = await Promise.all([
295
259
  createTakumiNodes(e),
296
- loadFontsForRenderer(e, { supportedFormats: /* @__PURE__ */ new Set(["ttf", "woff2"]), component: e.options.component })
260
+ loadFontsForRenderer(e, { supportedFormats: TAKUMI_FORMATS, component: e.options.component })
297
261
  ]);
298
262
  return {
299
263
  vnodes,
@@ -6,9 +6,4 @@
6
6
  */
7
7
  export declare function splitCssDeclarations(style: string): string[];
8
8
  export declare const GRADIENT_COLOR_SPACE_RE: RegExp;
9
- /**
10
- * Strip gradient color interpolation methods (e.g. `in oklab`, `in oklch`)
11
- * from CSS gradient values. Image renderers (Satori, Takumi) don't support them.
12
- */
13
- export declare function stripGradientColorSpace(value: string): string;
14
9
  export declare function resolveColorMix(value: string): string;
@@ -29,11 +29,6 @@ export function splitCssDeclarations(style) {
29
29
  return declarations;
30
30
  }
31
31
  export const GRADIENT_COLOR_SPACE_RE = /\s+in\s+(?:oklab|oklch|srgb(?:-linear)?|display-p3|a98-rgb|prophoto-rgb|rec2020|xyz(?:-d(?:50|65))?|hsl|hwb|lab|lch)/g;
32
- export function stripGradientColorSpace(value) {
33
- if (typeof value !== "string")
34
- return value;
35
- return value.replace(GRADIENT_COLOR_SPACE_RE, "");
36
- }
37
32
  const COLOR_MIX_HEX_RE = /color-mix\(in\s+\w+,\s*#([0-9a-fA-F]{3,8})\s+([\d.]+%?)\s*,\s*transparent\s*\)/g;
38
33
  const COLOR_MIX_RGBA_RE = /color-mix\(in\s+\w+,\s*rgba?\((\d+)[\x09-\x0D\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*(?: |(?: \s*)?,)\s*(\d+)[\x09-\x0D\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]*(?: |(?: \s*)?,)\s*(\d+)(?:\s*[/,]\s*[\d.]+)?\)\s+([\d.]+%?)\s*,\s*transparent\s*\)/g;
39
34
  function hexToRgb(hex) {
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { defineNitroPlugin } from "#nuxtseo/nitro";
2
+ import { collectAutoEjectTemplates, ejectCollectedTemplates } from "../util/auto-eject-lifecycle.js";
3
+ export default defineNitroPlugin((nitro) => {
4
+ if (!import.meta.dev)
5
+ return;
6
+ const hooks = nitro.hooks;
7
+ hooks.hook("render:html", (html, { event }) => collectAutoEjectTemplates(html, event));
8
+ hooks.hook("response", (_response, event) => ejectCollectedTemplates(event));
9
+ });
@@ -1,36 +1,8 @@
1
- import { parse } from "devalue";
2
1
  import { defineNitroPlugin } from "#nuxtseo/nitro";
3
- import { autoEjectCommunityTemplate } from "../util/auto-eject.js";
4
- import { normaliseOptions } from "../util/options.js";
5
- import { useOgImageRuntimeConfig } from "../utils.js";
6
- const PAYLOAD_REGEX = /<script.+id="nuxt-og-image-options"[^>]*>(.+?)<\/script>/;
2
+ import { collectAutoEjectTemplates, ejectCollectedTemplates } from "../util/auto-eject-lifecycle.js";
7
3
  export default defineNitroPlugin((nitro) => {
8
4
  if (!import.meta.dev)
9
5
  return;
10
- nitro.hooks.hook("render:html", (html, ctx) => {
11
- const payload = [...html.head, ...html.bodyAppend].join("\n").match(PAYLOAD_REGEX)?.[1];
12
- if (!payload)
13
- return;
14
- for (const options of parse(payload)) {
15
- if (!options?.component)
16
- continue;
17
- const normalised = (() => {
18
- try {
19
- return normaliseOptions({ ...options });
20
- } catch {
21
- return null;
22
- }
23
- })();
24
- if (normalised?.component?.category === "community")
25
- (ctx.event.context._ogImageAutoEject ||= []).push(normalised.component);
26
- }
27
- });
28
- nitro.hooks.hook("afterResponse", (event) => {
29
- const components = event.context._ogImageAutoEject;
30
- if (!components?.length)
31
- return;
32
- const runtimeConfig = useOgImageRuntimeConfig(event);
33
- for (const component of components)
34
- autoEjectCommunityTemplate(component, runtimeConfig);
35
- });
6
+ nitro.hooks.hook("render:html", (html, { event }) => collectAutoEjectTemplates(html, event));
7
+ nitro.hooks.hook("afterResponse", ejectCollectedTemplates);
36
8
  });
@@ -1,12 +1,16 @@
1
1
  import { parse } from "devalue";
2
- import { parseURL, withoutBase } from "ufo";
2
+ import { joinURL, parseURL, withoutBase, withQuery } from "ufo";
3
3
  import { appendResponseHeader } from "#nuxtseo/h3";
4
- import { defineNitroPlugin, useRuntimeConfig } from "#nuxtseo/nitro";
4
+ import { defineNitroPlugin, useRuntimeConfig, useStorage } from "#nuxtseo/nitro";
5
5
  import { prerenderOptionsCache } from "#og-image-cache";
6
+ import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
6
7
  import { createSitePathResolver } from "#site-config/server/composables/utils";
8
+ import { logger } from "../../logger.js";
7
9
  import { isInternalRoute } from "../../shared.js";
10
+ import { getPublishKey, publishImage } from "../og-image/cache/publish.js";
8
11
  import { resolvePathCacheKey } from "../og-image/context.js";
9
12
  import { createNitroRouteRuleMatcher } from "../util/kit.js";
13
+ import { useOgImageRuntimeConfig } from "../utils.js";
10
14
  const PAYLOAD_REGEX = /<script.+id="nuxt-og-image-options"[^>]*>(.+?)<\/script>/;
11
15
  const RE_SCRIPT_OPTIONS = /<script id="nuxt-og-image-options" type="application\/json">[\s\S]*?<\/script>/;
12
16
  const RE_SCRIPT_OVERRIDES = /<script id="nuxt-og-image-overrides" type="application\/json">[\s\S]*?<\/script>/;
@@ -14,10 +18,21 @@ function getPayloadFromHtml(html) {
14
18
  const match = String(html).match(PAYLOAD_REGEX);
15
19
  return match ? String(match[1]) : null;
16
20
  }
21
+ function replaceImageUrls(head, path, resolve) {
22
+ return head.map((entry) => entry.replace(/(<meta\b[^>]+\bcontent=")([^"]*)("[^>]*>)/g, (tag, start, url, end) => {
23
+ if (!/\b(?:property|name)="(?:og:image(?::url|:secure_url)?|twitter:image(?::src)?)"/.test(tag))
24
+ return tag;
25
+ const source = url.replace(/&amp;/g, "&");
26
+ const urlPath = parseURL(source).pathname;
27
+ return urlPath?.replace(/,/g, "%2C") === path ? `${start}${resolve(source).replace(/&/g, "&amp;")}${end}` : tag;
28
+ }));
29
+ }
17
30
  export default defineNitroPlugin(async (nitro) => {
18
31
  if (!import.meta.prerender)
19
32
  return;
20
33
  const routeRuleMatcher = createNitroRouteRuleMatcher();
34
+ const publishingPages = /* @__PURE__ */ new Set();
35
+ let publishWarningShown = false;
21
36
  nitro.hooks.hook("render:html", async (html, ctx) => {
22
37
  const { head, bodyAppend } = html;
23
38
  const path = parseURL(ctx.event.path).pathname;
@@ -45,7 +60,58 @@ export default defineNitroPlugin(async (nitro) => {
45
60
  }
46
61
  const prerenderPaths = ctx.event.context._ogImagePrerenderPaths;
47
62
  if (prerenderPaths) {
63
+ const config = useOgImageRuntimeConfig(ctx.event);
64
+ const publish = config.publish;
65
+ if (publish && !publishingPages.has(path)) {
66
+ publishingPages.add(path);
67
+ try {
68
+ for (const [ogKey, prerenderPath] of prerenderPaths) {
69
+ const extension = prerenderPath.split(".").pop();
70
+ if (!prerenderPath.includes("/_og/s/") || extension !== "png" && extension !== "jpeg" && extension !== "jpg" && extension !== "webp")
71
+ continue;
72
+ const opt = payloads.find(([key2]) => key2 === ogKey)?.[1];
73
+ if (!opt)
74
+ continue;
75
+ const result = await publishImage({
76
+ storage: useStorage(),
77
+ mount: publish.storage,
78
+ baseURL: publish.baseURL,
79
+ key: getPublishKey(new URL(prerenderPath, getSiteConfig(ctx.event).url).href, config.defaults, publish.cacheVersion),
80
+ extension,
81
+ maxAgeSeconds: Number(opt.cacheMaxAgeSeconds ?? config.defaults.cacheMaxAgeSeconds),
82
+ timeoutMs: config.security.renderTimeout,
83
+ now: Date.now,
84
+ render: async () => {
85
+ if (opt.component === "PageScreenshot") {
86
+ opt._prerenderHtml = `<!DOCTYPE html><html ${(html.htmlAttrs || []).join(" ")}><head>${html.head.join("")}</head><body ${(html.bodyAttrs || []).join(" ")}>${[...html.bodyPrepend || [], ...html.body || [], ...html.bodyAppend].join("")}</body></html>`;
87
+ await prerenderOptionsCache.setItem(key, payloads);
88
+ if (opt._hash)
89
+ await prerenderOptionsCache.setItem(`hash:${opt._hash}`, { ...opt, _path: pagePath });
90
+ }
91
+ const response = await nitro.localFetch(opt._query ? withQuery(prerenderPath, { _query: opt._query }) : prerenderPath);
92
+ if (!response.ok || !response.headers.get("content-type")?.startsWith("image/"))
93
+ throw new Error(`Image render failed: ${response.status}. ${(await response.text()).slice(0, 1e3)}`);
94
+ return new Uint8Array(await response.arrayBuffer());
95
+ }
96
+ });
97
+ if (result._tag === "Published") {
98
+ html.head = replaceImageUrls(html.head, prerenderPath, () => result.url);
99
+ prerenderPaths.delete(ogKey);
100
+ } else if (!publishWarningShown) {
101
+ publishWarningShown = true;
102
+ logger.warn("[Nuxt OG Image] Publish storage is unavailable. Using local image URLs.", result.reason);
103
+ }
104
+ }
105
+ } finally {
106
+ publishingPages.delete(path);
107
+ }
108
+ }
109
+ const app = useRuntimeConfig(ctx.event).app;
48
110
  for (const prerenderPath of prerenderPaths.values()) {
111
+ if (app.cdnURL && prerenderPath.includes("/_og/s/")) {
112
+ const imageURL = joinURL(app.cdnURL, withoutBase(prerenderPath, app.baseURL));
113
+ html.head = replaceImageUrls(html.head, prerenderPath, (source) => `${imageURL}${parseURL(source).search || ""}`);
114
+ }
49
115
  appendResponseHeader(ctx.event, "x-nitro-prerender", prerenderPath);
50
116
  }
51
117
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,44 @@
1
+ import { parseURL, withoutBase } from "ufo";
2
+ import { defineNitroPlugin } from "#nuxtseo/nitro";
3
+ import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
4
+ import { logger } from "../../logger.js";
5
+ import { isInternalRoute } from "../../shared.js";
6
+ import { getPublishKey } from "../og-image/cache/publish.js";
7
+ import { getPublishedImageCache, getPublishedImageCacheKey } from "../og-image/cache/published.js";
8
+ import { useOgImageRuntimeConfig } from "../utils.js";
9
+ const IMAGE_META = /<meta\b[^>]+\b(?:property|name)="(?:og:image(?::url|:secure_url)?|twitter:image(?::src)?)"[^>]*>/g;
10
+ const CONTENT = /\bcontent="([^"]+)"/;
11
+ export default defineNitroPlugin((nitro) => {
12
+ if (import.meta.dev || import.meta.prerender)
13
+ return;
14
+ nitro.hooks.hook("render:html", (html, { event }) => {
15
+ if (isInternalRoute(parseURL(event.path).pathname))
16
+ return;
17
+ const config = useOgImageRuntimeConfig(event);
18
+ const publish = config.publish;
19
+ if (!publish)
20
+ return;
21
+ const siteURL = new URL(getSiteConfig(event).url);
22
+ const replacements = /* @__PURE__ */ new Map();
23
+ const urls = new Set(html.head.flatMap((entry) => [...entry.matchAll(IMAGE_META)].map((match) => match[0].match(CONTENT)?.[1]).filter((url) => !!url)));
24
+ const cache = getPublishedImageCache(nitro);
25
+ for (const source of urls) {
26
+ try {
27
+ const url = new URL(source.replace(/&amp;/g, "&"), siteURL);
28
+ const path = withoutBase(url.pathname, config.app.baseURL);
29
+ if (url.origin !== siteURL.origin || !path.startsWith("/_og/d/"))
30
+ continue;
31
+ const extension = path.split(".").pop();
32
+ if (extension !== "png" && extension !== "jpeg" && extension !== "jpg" && extension !== "webp")
33
+ continue;
34
+ const key = getPublishedImageCacheKey(publish, getPublishKey(url.href, config.defaults, publish.cacheVersion), extension);
35
+ const result = cache.get(key, Date.now());
36
+ if (result)
37
+ replacements.set(source, result.url.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;"));
38
+ } catch (err) {
39
+ logger.debug("[Nuxt OG Image] Published URL lookup failed. Keeping the app image URL.", err);
40
+ }
41
+ }
42
+ html.head = html.head.map((entry) => entry.replace(IMAGE_META, (tag) => tag.replace(CONTENT, (content, source) => replacements.has(source) ? `content="${replacements.get(source)}"` : content)));
43
+ });
44
+ });
@@ -1,2 +1,11 @@
1
- declare const _default: any;
1
+ declare const _default: import("nuxt/server").EventHandler<Promise<{
2
+ siteConfigUrl: string;
3
+ origin: string;
4
+ componentNames: any;
5
+ runtimeConfig: import("../../types.js").OgImageRuntimeConfig;
6
+ compatibility: any;
7
+ resolvedFonts: any;
8
+ availableFonts: any;
9
+ fontRequirements: any;
10
+ }>>;
2
11
  export default _default;
@@ -1,18 +1,18 @@
1
- import { defineEventHandler, setHeader } from "#nuxtseo/h3";
1
+ import { defineEventHandler } from "nuxt/server";
2
2
  import { componentNames } from "#og-image-virtual/component-names.mjs";
3
3
  import compatibility from "#og-image/compatibility";
4
4
  import { fontRequirements } from "#og-image/font-requirements";
5
5
  import resolvedFonts from "#og-image/fonts";
6
6
  import availableFonts from "#og-image/fonts-available";
7
- import { getNitroOrigin } from "#site-config/server/composables";
8
7
  import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
8
+ import { getOgImageOrigin } from "../util/origin.js";
9
9
  import { useOgImageRuntimeConfig } from "../utils.js";
10
10
  export default defineEventHandler(async (e) => {
11
- setHeader(e, "Content-Type", "application/json");
11
+ e.res.headers.set("Content-Type", "application/json");
12
12
  const runtimeConfig = useOgImageRuntimeConfig(e);
13
13
  return {
14
14
  siteConfigUrl: getSiteConfig(e).url,
15
- origin: getNitroOrigin(e),
15
+ origin: getOgImageOrigin(e),
16
16
  componentNames,
17
17
  runtimeConfig,
18
18
  compatibility,
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/server").EventHandler<Promise<Response>>;
2
+ export default _default;
@@ -0,0 +1,22 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createError, defineEventHandler, getRequestURL, useRuntimeConfig } from "nuxt/server";
3
+ import { join } from "pathe";
4
+ import { withoutBase } from "ufo";
5
+ import { staticFontCacheDir } from "#og-image-virtual/build-dir.mjs";
6
+ import resolvedFonts from "#og-image/fonts";
7
+ import availableFonts from "#og-image/fonts-available";
8
+ export default defineEventHandler(async (event) => {
9
+ const path = withoutBase(getRequestURL(event).pathname, useRuntimeConfig().app.baseURL);
10
+ const font = [...resolvedFonts, ...availableFonts].find((face) => face.src === path || face.satoriSrc === path);
11
+ if (!font)
12
+ throw createError({ status: 404, statusText: "Not Found" });
13
+ const file = path === font.src && font.absolutePath ? font.absolutePath : join(staticFontCacheDir, path.slice("/_og-static-fonts/".length));
14
+ const data = await readFile(file);
15
+ return new Response(new Uint8Array(data), {
16
+ headers: {
17
+ "Content-Type": "application/octet-stream",
18
+ "Cache-Control": "no-store",
19
+ "Access-Control-Allow-Origin": "*"
20
+ }
21
+ });
22
+ });
@@ -0,0 +1,8 @@
1
+ import type { RequestEvent } from 'nuxt/server';
2
+ export declare const OG_IMAGE_SECRET_PURPOSE = "nuxt-og-image:url-signing";
3
+ /** Resolve once per request before synchronous app and server URL helpers run. */
4
+ export declare function initializeOgImageSigning(event: Pick<RequestEvent, 'context'>): Promise<void>;
5
+ /** Only the image endpoints need the secret. Every other request can render without it. */
6
+ export declare function isOgImageEndpointPath(pathname: string, baseURL: string): boolean;
7
+ /** Log a missing or invalid secret once per server instance, with the fix. */
8
+ export declare function reportSigningFailure(failure: unknown): void;
@@ -0,0 +1,75 @@
1
+ import { createError, deriveSecret, useRuntimeConfig } from "nuxt/server";
2
+ import { joinURL } from "ufo";
3
+ import { getCloudflareEnv } from "./util/cloudflare.js";
4
+ export const OG_IMAGE_SECRET_PURPOSE = "nuxt-og-image:url-signing";
5
+ let observed;
6
+ let warnedLegacy = false;
7
+ export async function initializeOgImageSigning(event) {
8
+ const config = useRuntimeConfig();
9
+ const security = config["nuxt-og-image"].security;
10
+ if (security.secret === false || import.meta.prerender && !security.strict)
11
+ return;
12
+ const cloudflareEnv = getCloudflareEnv(event);
13
+ const configRoot = config.appSecret;
14
+ const processRoot = globalThis.process?.env?.NUXT_OG_IMAGE_SECRET;
15
+ if (cloudflareEnv !== void 0) {
16
+ const root2 = resolveLegacyRoot(
17
+ cloudflareEnv.NUXT_APP_SECRET === void 0 ? configRoot : cloudflareEnv.NUXT_APP_SECRET,
18
+ cloudflareEnv.NUXT_OG_IMAGE_SECRET
19
+ );
20
+ const secret = observed !== void 0 && observed.root === root2 ? observed.secret : root2 === configRoot ? await deriveSecret(OG_IMAGE_SECRET_PURPOSE) : await deriveRootSecret(root2);
21
+ observed = { root: root2, configRoot, processRoot, secret };
22
+ event.context._ogImageSigningSecret = secret;
23
+ return;
24
+ }
25
+ if (observed !== void 0 && configRoot === observed.configRoot && processRoot === observed.processRoot) {
26
+ event.context._ogImageSigningSecret = observed.secret;
27
+ return;
28
+ }
29
+ const root = resolveLegacyRoot(configRoot, processRoot);
30
+ if (root !== configRoot) {
31
+ event.context._ogImageSigningSecret = await deriveRootSecret(root);
32
+ return;
33
+ }
34
+ event.context._ogImageSigningSecret = await deriveSecret(OG_IMAGE_SECRET_PURPOSE);
35
+ }
36
+ export function isOgImageEndpointPath(pathname, baseURL) {
37
+ return pathname.startsWith("/_og/") || pathname.startsWith(joinURL(baseURL, "_og/"));
38
+ }
39
+ let reportedFailure = false;
40
+ export function reportSigningFailure(failure) {
41
+ if (reportedFailure)
42
+ return;
43
+ reportedFailure = true;
44
+ const cause = failure instanceof Error ? failure.message : String(failure);
45
+ console.error([
46
+ "[nuxt-og-image] URL signing has no valid secret. OG image requests return 500 until you fix this. Pages still render.",
47
+ "Run `npx nuxt-og-image generate-secret`, then set NUXT_APP_SECRET in the production environment.",
48
+ "The secret must have at least 32 characters. To turn off signing, set `ogImage.security.secret` to false.",
49
+ `Cause: ${cause}`
50
+ ].join("\n"));
51
+ }
52
+ function resolveLegacyRoot(root, legacy) {
53
+ if ((root === void 0 || root === "") && legacy !== void 0 && legacy !== "") {
54
+ if (!warnedLegacy) {
55
+ warnedLegacy = true;
56
+ console.warn("[nuxt-og-image] NUXT_OG_IMAGE_SECRET is deprecated. Rename it to NUXT_APP_SECRET. Existing signed URLs must be refreshed.");
57
+ }
58
+ return legacy;
59
+ }
60
+ return root;
61
+ }
62
+ async function deriveRootSecret(root) {
63
+ if (typeof root !== "string" || root.length < 32) {
64
+ throw createError({ status: 500, message: "Set NUXT_APP_SECRET to a string with at least 32 characters." });
65
+ }
66
+ const encoder = new TextEncoder();
67
+ const key = await crypto.subtle.importKey("raw", encoder.encode(root), "HKDF", false, ["deriveBits"]);
68
+ const bits = await crypto.subtle.deriveBits({
69
+ name: "HKDF",
70
+ hash: "SHA-256",
71
+ salt: encoder.encode("nuxt"),
72
+ info: encoder.encode(OG_IMAGE_SECRET_PURPOSE)
73
+ }, key, 256);
74
+ return Array.from(new Uint8Array(bits), (byte) => byte.toString(16).padStart(2, "0")).join("");
75
+ }
@@ -0,0 +1,13 @@
1
+ import type { RequestEvent } from 'nuxt/server';
2
+ import type { OgImageComponent } from '../../types.js';
3
+ export interface AutoEjectEvent {
4
+ context: RequestEvent['context'] & {
5
+ _ogImageAutoEject?: OgImageComponent[];
6
+ };
7
+ }
8
+ export interface AutoEjectHtml {
9
+ head: string[];
10
+ bodyAppend: string[];
11
+ }
12
+ export declare function collectAutoEjectTemplates(html: AutoEjectHtml, event: AutoEjectEvent): void;
13
+ export declare function ejectCollectedTemplates(event: AutoEjectEvent): void;
@@ -0,0 +1,31 @@
1
+ import { parse } from "devalue";
2
+ import { useOgImageRuntimeConfig } from "../utils.js";
3
+ import { autoEjectCommunityTemplate } from "./auto-eject.js";
4
+ import { normaliseOptions } from "./options.js";
5
+ const PAYLOAD_REGEX = /<script.+id="nuxt-og-image-options"[^>]*>(.+?)<\/script>/;
6
+ export function collectAutoEjectTemplates(html, event) {
7
+ const payload = [...html.head, ...html.bodyAppend].join("\n").match(PAYLOAD_REGEX)?.[1];
8
+ if (!payload)
9
+ return;
10
+ for (const options of parse(payload)) {
11
+ if (!options?.component)
12
+ continue;
13
+ const normalised = (() => {
14
+ try {
15
+ return normaliseOptions({ ...options });
16
+ } catch {
17
+ return null;
18
+ }
19
+ })();
20
+ if (normalised?.component?.category === "community")
21
+ (event.context._ogImageAutoEject ||= []).push(normalised.component);
22
+ }
23
+ }
24
+ export function ejectCollectedTemplates(event) {
25
+ const components = event.context._ogImageAutoEject;
26
+ if (!components?.length)
27
+ return;
28
+ const runtimeConfig = useOgImageRuntimeConfig(event);
29
+ for (const component of components)
30
+ autoEjectCommunityTemplate(component, runtimeConfig);
31
+ }
@@ -7,5 +7,6 @@ export declare function useOgImageBufferCache(ctx: OgImageRenderEventContext, op
7
7
  }): Promise<void | H3Error | {
8
8
  cachedItem: false | BufferSource;
9
9
  enabled: boolean;
10
+ expiresAt: number;
10
11
  update: (image: BufferSource | Buffer | Uint8Array) => Promise<void>;
11
12
  }>;
@@ -21,12 +21,13 @@ function warnCacheBackendUnreachable(ctx, baseCacheKey, e) {
21
21
  }
22
22
  export async function useOgImageBufferCache(ctx, options) {
23
23
  const maxAge = Number(options.cacheMaxAgeSeconds);
24
- const intentionallyEnabled = !import.meta.dev && !import.meta.prerender && maxAge > 0;
24
+ const intentionallyEnabled = options.baseCacheKey !== false && !import.meta.dev && !import.meta.prerender && maxAge > 0;
25
25
  let enabled = intentionallyEnabled;
26
- const cache = prefixStorage(useStorage(), withTrailingSlash(options.baseCacheKey || "/"));
26
+ const cache = options.baseCacheKey === false ? void 0 : prefixStorage(useStorage(), withTrailingSlash(options.baseCacheKey));
27
27
  const key = ctx.key;
28
28
  let cachedItem = false;
29
- if (enabled) {
29
+ let cacheExpiresAt = 0;
30
+ if (enabled && cache) {
30
31
  const hasItem = await cache.hasItem(key).catch((e) => {
31
32
  enabled = false;
32
33
  warnCacheBackendUnreachable(ctx, options.baseCacheKey, e);
@@ -51,6 +52,7 @@ export async function useOgImageBufferCache(ctx, options) {
51
52
  void err;
52
53
  });
53
54
  } else if (expiresAt > Date.now()) {
55
+ cacheExpiresAt = expiresAt;
54
56
  cachedItem = Buffer.from(value, "base64");
55
57
  if (handleCacheHeaders(ctx.e, {
56
58
  modifiedTime: new Date(headers["last-modified"]),
@@ -76,11 +78,15 @@ export async function useOgImageBufferCache(ctx, options) {
76
78
  return {
77
79
  enabled,
78
80
  cachedItem,
81
+ get expiresAt() {
82
+ return cacheExpiresAt;
83
+ },
79
84
  async update(item) {
80
85
  setHeader(ctx.e, "X-OG-Cache", enabled ? "MISS" : "DISABLED");
81
- if (!enabled)
86
+ if (!enabled || !cache)
82
87
  return;
83
88
  const value = Buffer.from(item).toString("base64");
89
+ cacheExpiresAt = Date.now() + maxAge * 1e3;
84
90
  const headers = {
85
91
  // avoid multi-tenancy cache issues
86
92
  "Vary": "accept-encoding, host",
@@ -92,8 +98,8 @@ export async function useOgImageBufferCache(ctx, options) {
92
98
  await cache.setItem(key, {
93
99
  value,
94
100
  headers,
95
- expiresAt: Date.now() + maxAge * 1e3
96
- }).catch((err) => logger.warn(`[Nuxt OG Image] Failed to write cache for key "${key}": ${err?.message || err}`));
101
+ expiresAt: cacheExpiresAt
102
+ }, { ttl: Math.max(maxAge, 60) }).catch((err) => logger.warn(`[Nuxt OG Image] Failed to write cache for key "${key}": ${err?.message || err}`));
97
103
  }
98
104
  };
99
105
  }
@@ -1,3 +1,3 @@
1
- import type { H3Event } from '#nuxtseo/h3';
1
+ import type { RequestEvent } from 'nuxt/server';
2
2
  export type CloudflareEnv = Record<string, unknown>;
3
- export declare function getCloudflareEnv(event?: H3Event): CloudflareEnv | undefined;
3
+ export declare function getCloudflareEnv(event?: Pick<RequestEvent, 'context'>): CloudflareEnv | undefined;