nuxt-og-image 6.4.2 → 6.4.4

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 (63) 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/devtools/200.html +1 -1
  6. package/dist/devtools/404.html +1 -1
  7. package/dist/devtools/_nuxt/{BWm573-p.js → B-y6Zfh-.js} +1 -1
  8. package/dist/devtools/_nuxt/{yBiBpwD7.js → C5LFIfwi.js} +6 -6
  9. package/dist/devtools/_nuxt/CN79P4uE.js +6 -0
  10. package/dist/devtools/_nuxt/{BQDcSiMf.js → CThtpBJK.js} +1 -1
  11. package/dist/devtools/_nuxt/{BsivBvAU.js → CYOZ55V_.js} +1 -1
  12. package/dist/devtools/_nuxt/{Cy0omYQh.js → ClxM7Lmy.js} +1 -1
  13. package/dist/devtools/_nuxt/{B6Dg3dZ6.js → CqGOSK9s.js} +1 -1
  14. package/dist/devtools/_nuxt/{DziLl24l.js → CwlJb64V.js} +1 -1
  15. package/dist/devtools/_nuxt/{BMVWkjCo.js → CxhRt3FZ.js} +1 -1
  16. package/dist/devtools/_nuxt/{w_tq7NMl.js → DLMIIqSE.js} +1 -1
  17. package/dist/devtools/_nuxt/DevtoolsSection.Be9AJQOh.css +1 -0
  18. package/dist/devtools/_nuxt/DevtoolsSnippet.BubiVHug.css +1 -0
  19. package/dist/devtools/_nuxt/{CHeKziWa.js → DjEkFT0U.js} +1 -1
  20. package/dist/devtools/_nuxt/builds/latest.json +1 -1
  21. package/dist/devtools/_nuxt/builds/meta/ca9f1307-1f7b-4a5f-b061-2c680927caac.json +1 -0
  22. package/dist/devtools/_nuxt/{entry.BjD2aghs.css → entry.CJ6yFnTt.css} +1 -1
  23. package/dist/devtools/_nuxt/{pages.DO0dnDUs.css → pages.D8s6dQja.css} +1 -1
  24. package/dist/devtools/_nuxt/renderer-select.cI9Vfr5y.css +1 -0
  25. package/dist/devtools/debug/index.html +1 -1
  26. package/dist/devtools/docs/index.html +1 -1
  27. package/dist/devtools/index.html +1 -1
  28. package/dist/devtools/templates/index.html +1 -1
  29. package/dist/module.cjs +1 -1
  30. package/dist/module.d.cts +11 -0
  31. package/dist/module.d.mts +11 -0
  32. package/dist/module.d.ts +11 -0
  33. package/dist/module.json +1 -1
  34. package/dist/module.mjs +1 -1
  35. package/dist/runtime/app/client-utils.js +24 -5
  36. package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.js +9 -18
  37. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +8 -4
  38. package/dist/runtime/server/og-image/bindings/font-assets/node.js +6 -2
  39. package/dist/runtime/server/og-image/browser/screenshot.d.ts +1 -1
  40. package/dist/runtime/server/og-image/browser/screenshot.js +6 -4
  41. package/dist/runtime/server/og-image/context.js +4 -0
  42. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +106 -99
  43. package/dist/runtime/server/og-image/core/transforms/emojis/fetch.js +23 -12
  44. package/dist/runtime/server/og-image/satori/renderer.js +16 -14
  45. package/dist/runtime/server/og-image/takumi/renderer.js +27 -19
  46. package/dist/runtime/server/util/cloudflareAssets.d.ts +24 -0
  47. package/dist/runtime/server/util/cloudflareAssets.js +16 -0
  48. package/dist/runtime/server/util/eventHandlers.js +28 -5
  49. package/dist/runtime/server/util/fetchLocalAsset.d.ts +25 -0
  50. package/dist/runtime/server/util/fetchLocalAsset.js +34 -0
  51. package/dist/runtime/server/util/fetchTimeout.d.ts +2 -0
  52. package/dist/runtime/server/util/fetchTimeout.js +7 -0
  53. package/dist/runtime/server/util/timings.d.ts +17 -0
  54. package/dist/runtime/server/util/timings.js +75 -0
  55. package/dist/runtime/types.d.ts +3 -0
  56. package/dist/shared/{nuxt-og-image.BK0-aZom.mjs → nuxt-og-image.Cr3WHMk1.mjs} +16 -11
  57. package/dist/shared/{nuxt-og-image.C2oXAHiT.cjs → nuxt-og-image.TJuh6pW5.cjs} +17 -12
  58. package/package.json +9 -9
  59. package/dist/devtools/_nuxt/DDRo8-tD.js +0 -6
  60. package/dist/devtools/_nuxt/DevtoolsSection.C-PGRg5f.css +0 -1
  61. package/dist/devtools/_nuxt/DevtoolsSnippet.BipAyEUC.css +0 -1
  62. package/dist/devtools/_nuxt/builds/meta/150c0674-b387-4525-9043-e05dd343db8e.json +0 -1
  63. package/dist/devtools/_nuxt/renderer-select.J57nTUNW.css +0 -1
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.16.0"
5
5
  },
6
6
  "configKey": "ogImage",
7
- "version": "6.4.2",
7
+ "version": "6.4.4",
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.BK0-aZom.mjs';
10
+ export { m as default } from './shared/nuxt-og-image.Cr3WHMk1.mjs';
11
11
  import 'nuxtseo-shared/kit';
12
12
  import '../dist/runtime/logger.js';
13
13
  import 'node:crypto';
@@ -1,9 +1,28 @@
1
1
  import { componentNames } from "#build/nuxt-og-image/components.mjs";
2
2
  import { defu } from "defu";
3
- import { useHead, useRuntimeConfig } from "nuxt/app";
3
+ import { injectHead, useHead, useRuntimeConfig } from "nuxt/app";
4
4
  import { joinURL, withQuery } from "ufo";
5
5
  import { toValue } from "vue";
6
6
  import { buildOgImageUrl, generateMeta, separateProps } from "../shared.js";
7
+ const clientEntriesByHead = /* @__PURE__ */ new WeakMap();
8
+ function registerClientOgHead(ogKey, input, options) {
9
+ let entries;
10
+ try {
11
+ const head = injectHead();
12
+ if (head) {
13
+ entries = clientEntriesByHead.get(head);
14
+ if (!entries) {
15
+ entries = /* @__PURE__ */ new Map();
16
+ clientEntriesByHead.set(head, entries);
17
+ }
18
+ }
19
+ } catch {
20
+ }
21
+ entries?.get(ogKey)?.dispose();
22
+ const entry = useHead(input, options);
23
+ if (entry && entries)
24
+ entries.set(ogKey, entry);
25
+ }
7
26
  function resolveReactiveOptions(input) {
8
27
  const options = toValue(input);
9
28
  if (options === false)
@@ -56,16 +75,16 @@ export function clientProcessOgImageOptions(input, route, basePath) {
56
75
  }
57
76
  if (route.query)
58
77
  validOptions._query = route.query;
78
+ const ogKey = validOptions.key || "og";
59
79
  if (validOptions.url) {
60
80
  const url = validOptions.url;
61
- useHead({ meta: generateMeta(url, validOptions) }, { tagPriority: "high" });
81
+ registerClientOgHead(ogKey, { meta: generateMeta(url, validOptions) }, { tagPriority: "high" });
62
82
  paths.push(url);
63
83
  continue;
64
84
  }
65
85
  if (publicCfg.hasServerRuntime) {
66
- const ogKey = validOptions.key || "og";
67
86
  const finalUrl2 = buildResolverUrl(baseURL, basePath, ogKey, route.query);
68
- useHead({ meta: generateMeta(finalUrl2, validOptions) }, { tagPriority: 35 });
87
+ registerClientOgHead(ogKey, { meta: generateMeta(finalUrl2, validOptions) }, { tagPriority: 35 });
69
88
  paths.push(finalUrl2);
70
89
  continue;
71
90
  }
@@ -79,7 +98,7 @@ export function clientProcessOgImageOptions(input, route, basePath) {
79
98
  const result = buildOgImageUrl(urlOpts, extension, true, defaults, void 0);
80
99
  const resolvedUrl = joinURL("/", baseURL, result.url);
81
100
  const finalUrl = opts._query && Object.keys(opts._query).length ? withQuery(resolvedUrl, { _query: opts._query }) : resolvedUrl;
82
- useHead({ meta: generateMeta(finalUrl, opts) }, { processTemplateParams: true, tagPriority: 35 });
101
+ registerClientOgHead(ogKey, { meta: generateMeta(finalUrl, opts) }, { processTemplateParams: true, tagPriority: 35 });
83
102
  paths.push(finalUrl);
84
103
  }
85
104
  return paths;
@@ -1,27 +1,18 @@
1
1
  import { useRuntimeConfig } from "nitropack/runtime";
2
2
  import { withBase } from "ufo";
3
+ import { getCloudflareAssets } from "../../../util/cloudflareAssets.js";
4
+ import { fetchLocalAsset } from "../../../util/fetchLocalAsset.js";
5
+ import { getFetchTimeout } from "../../../util/fetchTimeout.js";
6
+ import { useOgImageRuntimeConfig } from "../../../utils.js";
3
7
  export async function resolve(event, font) {
4
8
  const path = font.src || font.localPath;
5
9
  const { app } = useRuntimeConfig();
6
10
  const fullPath = withBase(path, app.baseURL);
7
- const assets = event.context.cloudflare?.env?.ASSETS || event.context.ASSETS;
8
- if (assets && typeof assets.fetch === "function") {
9
- const origin = event.context.cloudflare?.request?.url || `https://${event.headers.get("host") || "localhost"}`;
10
- const url = new URL(fullPath, origin).href;
11
- const res = await assets.fetch(url).catch(() => null);
12
- if (res?.ok) {
13
- return Buffer.from(await res.arrayBuffer());
14
- }
15
- }
16
- if (typeof event.fetch === "function") {
17
- const origin = event.context.cloudflare?.request?.url || `https://${event.headers.get("host") || "localhost"}`;
18
- const url = new URL(fullPath, origin).href;
19
- const res = await event.fetch(url).catch(() => null);
20
- if (res?.ok) {
21
- return Buffer.from(await res.arrayBuffer());
22
- }
23
- }
24
- if (!assets && !event.context._ogImageWarnedMissingAssets) {
11
+ const timeout = getFetchTimeout(useOgImageRuntimeConfig());
12
+ const ab = await fetchLocalAsset(event, fullPath, { fetchTimeout: timeout });
13
+ if (ab)
14
+ return Buffer.from(ab);
15
+ if (!getCloudflareAssets(event) && !event.context._ogImageWarnedMissingAssets) {
25
16
  event.context._ogImageWarnedMissingAssets = true;
26
17
  console.warn(
27
18
  `[Nuxt OG Image] No ASSETS binding found on Cloudflare Workers. Font loading will fail. To fix this, add \`nitro: { cloudflare: { deployConfig: true } }\` to your nuxt.config and deploy with \`npx wrangler --cwd .output deploy\` instead of using the --assets flag.`
@@ -4,6 +4,8 @@ import { getRequestURL } from "h3";
4
4
  import { useRuntimeConfig } from "nitropack/runtime";
5
5
  import { join } from "pathe";
6
6
  import { withBase } from "ufo";
7
+ import { getFetchTimeout } from "../../../util/fetchTimeout.js";
8
+ import { useOgImageRuntimeConfig } from "../../../utils.js";
7
9
  let fontUrlMapping;
8
10
  async function loadFontUrlMapping() {
9
11
  if (fontUrlMapping)
@@ -14,6 +16,7 @@ async function loadFontUrlMapping() {
14
16
  }
15
17
  export async function resolve(event, font) {
16
18
  const path = font.src || font.localPath;
19
+ const timeout = getFetchTimeout(useOgImageRuntimeConfig());
17
20
  if (font.absolutePath) {
18
21
  const data = await readFile(font.absolutePath).catch(() => null);
19
22
  if (data?.length)
@@ -33,7 +36,7 @@ export async function resolve(event, font) {
33
36
  return cached;
34
37
  const mapping = await loadFontUrlMapping();
35
38
  if (mapping[filename2]) {
36
- const res = await fetch(mapping[filename2]).catch(() => null);
39
+ const res = await fetch(mapping[filename2], { signal: AbortSignal.timeout(timeout) }).catch(() => null);
37
40
  if (res?.ok)
38
41
  return Buffer.from(await res.arrayBuffer());
39
42
  }
@@ -54,7 +57,7 @@ export async function resolve(event, font) {
54
57
  const filename = path.slice("/_fonts/".length);
55
58
  const mapping = await loadFontUrlMapping();
56
59
  if (mapping[filename]) {
57
- const res = await fetch(mapping[filename]).catch(() => null);
60
+ const res = await fetch(mapping[filename], { signal: AbortSignal.timeout(timeout) }).catch(() => null);
58
61
  if (res?.ok)
59
62
  return Buffer.from(await res.arrayBuffer());
60
63
  }
@@ -70,14 +73,15 @@ export async function resolve(event, font) {
70
73
  const reqUrl = getRequestURL(event);
71
74
  const origin = `${reqUrl.protocol}//${reqUrl.host}`;
72
75
  const url = new URL(withBase(path, app.baseURL), origin).href;
73
- const res = await fetch(url).catch(() => null);
76
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeout) }).catch(() => null);
74
77
  if (res?.ok) {
75
78
  return Buffer.from(await res.arrayBuffer());
76
79
  }
77
80
  }
78
81
  const fullPath = withBase(path, app.baseURL);
79
82
  const arrayBuffer = await event.$fetch(fullPath, {
80
- responseType: "arrayBuffer"
83
+ responseType: "arrayBuffer",
84
+ timeout
81
85
  });
82
86
  return Buffer.from(arrayBuffer);
83
87
  }
@@ -1,17 +1,21 @@
1
1
  import { getNitroOrigin } from "#site-config/server/composables";
2
2
  import { useRuntimeConfig } from "nitropack/runtime";
3
3
  import { withBase } from "ufo";
4
+ import { getFetchTimeout } from "../../../util/fetchTimeout.js";
5
+ import { useOgImageRuntimeConfig } from "../../../utils.js";
4
6
  export async function resolve(event, font) {
5
7
  const path = font.src || font.localPath;
6
8
  const { app } = useRuntimeConfig();
7
9
  const fullPath = withBase(path, app.baseURL);
8
10
  const origin = getNitroOrigin(event);
9
- const res = await fetch(new URL(fullPath, origin).href).catch(() => null);
11
+ const timeout = getFetchTimeout(useOgImageRuntimeConfig());
12
+ const res = await fetch(new URL(fullPath, origin).href, { signal: AbortSignal.timeout(timeout) }).catch(() => null);
10
13
  if (res?.ok) {
11
14
  return Buffer.from(await res.arrayBuffer());
12
15
  }
13
16
  const arrayBuffer = await event.$fetch(fullPath, {
14
- responseType: "arrayBuffer"
17
+ responseType: "arrayBuffer",
18
+ timeout
15
19
  });
16
20
  return Buffer.from(arrayBuffer);
17
21
  }
@@ -1,4 +1,4 @@
1
1
  import type { Buffer } from 'node:buffer';
2
2
  import type { Browser } from 'playwright-core';
3
3
  import type { OgImageRenderEventContext } from '../../../types.js';
4
- export declare function createScreenshot({ basePath, e, options, extension }: OgImageRenderEventContext, browser: Browser): Promise<Buffer>;
4
+ export declare function createScreenshot({ basePath, e, options, extension, timings }: OgImageRenderEventContext, browser: Browser): Promise<Buffer>;
@@ -2,6 +2,7 @@ import { getNitroOrigin } from "#site-config/server/composables";
2
2
  import { withQuery } from "ufo";
3
3
  import { toValue } from "vue";
4
4
  import { buildOgImageUrl } from "../../../shared.js";
5
+ import { getFetchTimeout } from "../../util/fetchTimeout.js";
5
6
  import { logger } from "../../util/logger.js";
6
7
  import { useOgImageRuntimeConfig } from "../../utils.js";
7
8
  function isPlaywrightPage(page) {
@@ -53,8 +54,9 @@ async function takeScreenshot(page, selector, options) {
53
54
  }
54
55
  return await page.screenshot(puppeteerOptions);
55
56
  }
56
- export async function createScreenshot({ basePath, e, options, extension }, browser) {
57
- const { colorPreference, defaults, security } = useOgImageRuntimeConfig();
57
+ export async function createScreenshot({ basePath, e, options, extension, timings }, browser) {
58
+ const runtimeConfig = useOgImageRuntimeConfig();
59
+ const { colorPreference, defaults, security } = runtimeConfig;
58
60
  const path = options.component === "PageScreenshot" ? basePath : buildOgImageUrl(options, "html", false, defaults, security?.secret || void 0).url;
59
61
  let page;
60
62
  if (typeof browser.newPage === "function" && browser.newPage.length === 0) {
@@ -75,7 +77,7 @@ export async function createScreenshot({ basePath, e, options, extension }, brow
75
77
  logger.warn("The `html` option is deprecated and will be removed in the next major version. Use a Vue component instead.");
76
78
  }
77
79
  if (import.meta.prerender && !options.html) {
78
- options.html = await e.$fetch(path).catch(() => void 0);
80
+ options.html = await timings.measure("html-fetch", () => e.$fetch(path, { timeout: getFetchTimeout(runtimeConfig) }).catch(() => void 0));
79
81
  }
80
82
  await setViewport(
81
83
  page,
@@ -112,7 +114,7 @@ export async function createScreenshot({ basePath, e, options, extension }, brow
112
114
  el.style.display = "none";
113
115
  }, _options.mask);
114
116
  }
115
- return await takeScreenshot(page, _options.selector, screenshotOptions);
117
+ return await timings.measure("render-browser", () => takeScreenshot(page, _options.selector, screenshotOptions));
116
118
  } finally {
117
119
  await page.close();
118
120
  }
@@ -12,6 +12,7 @@ import { decodeOgImageParams, extractEncodedSegment, sanitizeProps, separateProp
12
12
  import { autoEjectCommunityTemplate } from "../util/auto-eject.js";
13
13
  import { createNitroRouteRuleMatcher } from "../util/kit.js";
14
14
  import { normaliseOptions } from "../util/options.js";
15
+ import { createTimings, TIMING_CTX_KEY } from "../util/timings.js";
15
16
  import { useOgImageRuntimeConfig } from "../utils.js";
16
17
  import { getBrowserRenderer, getSatoriRenderer, getTakumiRenderer } from "./instances.js";
17
18
  const RE_HASH_MODE = /^o_([a-z0-9]+)$/i;
@@ -186,6 +187,8 @@ export async function resolveContext(e) {
186
187
  statusMessage: `[Nuxt OG Image] Renderer "${rendererType}" is not available. Component "${normalised.component?.pascalName}" requires the ${rendererType} renderer but it's not bundled for this preset.`
187
188
  });
188
189
  }
190
+ const timings = e.context[TIMING_CTX_KEY] || createTimings();
191
+ e.context[TIMING_CTX_KEY] = timings;
189
192
  const ctx = {
190
193
  e,
191
194
  key,
@@ -196,6 +199,7 @@ export async function resolveContext(e) {
196
199
  extension,
197
200
  basePath,
198
201
  options: normalised.options,
202
+ timings,
199
203
  _nitro: useNitroApp()
200
204
  };
201
205
  await ctx._nitro.hooks.callHook("nuxt-og-image:context", ctx);
@@ -3,6 +3,8 @@ import { useStorage } from "nitropack/runtime";
3
3
  import { withBase, withoutLeadingSlash } from "ufo";
4
4
  import { toBase64Image } from "../../../../shared.js";
5
5
  import { decodeHtml } from "../../../util/encoding.js";
6
+ import { fetchLocalAsset } from "../../../util/fetchLocalAsset.js";
7
+ import { getFetchTimeout } from "../../../util/fetchTimeout.js";
6
8
  import { logger } from "../../../util/logger.js";
7
9
  import { getImageDimensions } from "../../utils/image-detector.js";
8
10
  import { defineTransformer } from "../plugins.js";
@@ -58,6 +60,7 @@ function isBlockedUrl(url) {
58
60
  }
59
61
  const RE_URL_LEADING = /^url\(['"]?/;
60
62
  const RE_URL_TRAILING = /['"]?\)$/;
63
+ const SUBREQUEST_HEADERS = { "x-nuxt-og-image": "1" };
61
64
  async function resolveLocalFilePathImage(publicStoragePath, src) {
62
65
  const normalizedSrc = withoutLeadingSlash(
63
66
  src.replace("_nuxt/@fs/", "").replace("_nuxt/", "").replace("./", "")
@@ -66,125 +69,129 @@ async function resolveLocalFilePathImage(publicStoragePath, src) {
66
69
  if (await useStorage().hasItem(key))
67
70
  return await useStorage().getItemRaw(key);
68
71
  }
72
+ function toBufferSourceAsBase64(buf) {
73
+ const ab = buf instanceof ArrayBuffer ? buf : ArrayBuffer.isView(buf) ? buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) : buf;
74
+ return toBase64Image(ab);
75
+ }
76
+ const renderCaches = /* @__PURE__ */ new WeakMap();
77
+ function getRenderCache(e) {
78
+ let cache = renderCaches.get(e);
79
+ if (!cache) {
80
+ cache = /* @__PURE__ */ new Map();
81
+ renderCaches.set(e, cache);
82
+ }
83
+ return cache;
84
+ }
85
+ function resolveSrcToBuffer(src, kind, ctx) {
86
+ const cache = getRenderCache(ctx.e);
87
+ const existing = cache.get(src);
88
+ if (existing)
89
+ return existing;
90
+ const promise = doResolveSrcToBuffer(src, kind, ctx);
91
+ cache.set(src, promise);
92
+ return promise;
93
+ }
94
+ async function doResolveSrcToBuffer(src, kind, { e, publicStoragePath, runtimeConfig, timings }) {
95
+ const fetchTimeout = getFetchTimeout(runtimeConfig);
96
+ const logFailure = (url, err) => {
97
+ logger.debug(`[og-image] ${kind} fetch failed (${url}): ${err?.message || err}`);
98
+ };
99
+ if (src.startsWith("/")) {
100
+ let buffer2;
101
+ if (import.meta.prerender || import.meta.dev) {
102
+ const srcWithoutBase = src.replace(runtimeConfig.app.baseURL, "/");
103
+ buffer2 = await resolveLocalFilePathImage(publicStoragePath, srcWithoutBase);
104
+ }
105
+ if (!buffer2 && !import.meta.prerender) {
106
+ const ab = await timings.measure("image-fetch", () => fetchLocalAsset(e, src, {
107
+ fetchTimeout,
108
+ headers: SUBREQUEST_HEADERS,
109
+ includeExternalFallback: true,
110
+ onStepFailure: logFailure
111
+ }));
112
+ if (ab)
113
+ buffer2 = new Uint8Array(ab);
114
+ }
115
+ return buffer2 ? { buffer: buffer2 } : {};
116
+ }
117
+ const decodedSrc = decodeHtml(src);
118
+ if (!import.meta.dev && isBlockedUrl(decodedSrc)) {
119
+ logger.warn(`Blocked internal ${kind} fetch: ${decodedSrc}`);
120
+ return { blocked: true };
121
+ }
122
+ const end = timings.start("image-fetch");
123
+ const buffer = await $fetch(decodedSrc, {
124
+ responseType: "arrayBuffer",
125
+ timeout: fetchTimeout
126
+ }).catch((err) => {
127
+ logFailure(decodedSrc, err);
128
+ }).finally(end);
129
+ return buffer ? { buffer } : {};
130
+ }
131
+ function applyImageDimensions(node, buffer) {
132
+ if (typeof node.props.width === "string")
133
+ node.props.width = Number(node.props.width) || void 0;
134
+ if (typeof node.props.height === "string")
135
+ node.props.height = Number(node.props.height) || void 0;
136
+ if (node.props.width && node.props.height)
137
+ return;
138
+ const view = buffer instanceof Uint8Array ? buffer : ArrayBuffer.isView(buffer) ? new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) : new Uint8Array(buffer);
139
+ const dimensions = getImageDimensions(view);
140
+ if (!dimensions?.width || !dimensions?.height)
141
+ return;
142
+ const naturalAspectRatio = dimensions.width / dimensions.height;
143
+ if (node.props.width && !node.props.height) {
144
+ node.props.height = Math.round(node.props.width / naturalAspectRatio);
145
+ } else if (node.props.height && !node.props.width) {
146
+ node.props.width = Math.round(node.props.height * naturalAspectRatio);
147
+ } else {
148
+ node.props.width = dimensions.width;
149
+ node.props.height = dimensions.height;
150
+ }
151
+ }
69
152
  export default defineTransformer([
70
153
  // fix <img src="">
71
154
  {
72
155
  filter: (node) => node.type === "img" && node.props?.src,
73
- transform: async (node, { e, publicStoragePath, runtimeConfig }) => {
156
+ transform: async (node, ctx) => {
74
157
  let src = node.props.src;
75
- const isRelative = src.startsWith("/");
76
- let dimensions;
77
- let imageBuffer;
158
+ if (src.startsWith("data:"))
159
+ return;
78
160
  if (src.endsWith(".webp")) {
79
161
  logger.warn("Using WebP images with Satori is not supported. Please consider switching image format or use the chromium renderer.", src);
80
162
  }
81
- if (isRelative) {
82
- if (import.meta.prerender || import.meta.dev) {
83
- const srcWithoutBase = src.replace(runtimeConfig.app.baseURL, "");
84
- imageBuffer = await resolveLocalFilePathImage(publicStoragePath, srcWithoutBase);
85
- }
86
- if (!imageBuffer) {
87
- imageBuffer = await e.$fetch(src, { responseType: "arrayBuffer" }).catch(() => {
88
- });
89
- if (!imageBuffer && !import.meta.prerender) {
90
- imageBuffer = await e.$fetch(src, {
91
- baseURL: getNitroOrigin(e),
92
- responseType: "arrayBuffer"
93
- }).catch(() => {
94
- });
95
- }
96
- }
97
- if (imageBuffer) {
98
- const buffer = imageBuffer instanceof ArrayBuffer ? imageBuffer : imageBuffer.buffer;
99
- node.props.src = toBase64Image(buffer);
100
- }
101
- } else if (!src.startsWith("data:")) {
102
- src = decodeHtml(src);
103
- if (!import.meta.dev && isBlockedUrl(src)) {
104
- logger.warn(`Blocked internal image fetch: ${src}`);
105
- delete node.props.src;
106
- } else {
107
- node.props.src = src;
108
- imageBuffer = await $fetch(src, {
109
- responseType: "arrayBuffer"
110
- }).catch(() => {
111
- });
112
- if (imageBuffer) {
113
- const buffer = imageBuffer instanceof ArrayBuffer ? imageBuffer : imageBuffer.buffer;
114
- node.props.src = toBase64Image(buffer);
115
- }
116
- }
117
- }
118
- if (typeof node.props.width === "string")
119
- node.props.width = Number(node.props.width) || void 0;
120
- if (typeof node.props.height === "string")
121
- node.props.height = Number(node.props.height) || void 0;
122
- if (imageBuffer && (!node.props.width || !node.props.height)) {
123
- dimensions = getImageDimensions(imageBuffer);
124
- if (dimensions?.width && dimensions?.height) {
125
- const naturalAspectRatio = dimensions.width / dimensions.height;
126
- if (node.props.width && !node.props.height) {
127
- node.props.height = Math.round(node.props.width / naturalAspectRatio);
128
- } else if (node.props.height && !node.props.width) {
129
- node.props.width = Math.round(node.props.height * naturalAspectRatio);
130
- } else if (!node.props.width && !node.props.height) {
131
- node.props.width = dimensions.width;
132
- node.props.height = dimensions.height;
133
- }
134
- }
163
+ const isRelative = src.startsWith("/");
164
+ if (!isRelative)
165
+ src = node.props.src = decodeHtml(src);
166
+ const result = await resolveSrcToBuffer(src, "image", ctx);
167
+ if (result.blocked) {
168
+ delete node.props.src;
169
+ return;
135
170
  }
136
- if (typeof node.props.src === "string" && node.props.src.startsWith("/")) {
137
- if (imageBuffer) {
138
- const buffer = imageBuffer instanceof ArrayBuffer ? imageBuffer : imageBuffer.buffer;
139
- node.props.src = toBase64Image(buffer);
140
- } else {
141
- node.props.src = `${withBase(src, `${getNitroOrigin(e)}`)}?${Date.now()}`;
142
- }
171
+ if (result.buffer) {
172
+ node.props.src = toBufferSourceAsBase64(result.buffer);
173
+ applyImageDimensions(node, result.buffer);
174
+ return;
143
175
  }
176
+ if (isRelative)
177
+ node.props.src = withBase(src, `${getNitroOrigin(ctx.e)}`);
144
178
  }
145
179
  },
146
180
  // fix style="background-image: url('')"
147
181
  {
148
182
  filter: (node) => node.props?.style?.backgroundImage?.includes("url("),
149
- transform: async (node, { e, publicStoragePath, runtimeConfig }) => {
183
+ transform: async (node, ctx) => {
150
184
  const backgroundImage = node.props.style.backgroundImage;
151
185
  const src = backgroundImage.replace(RE_URL_LEADING, "").replace(RE_URL_TRAILING, "");
152
186
  if (src.startsWith("data:"))
153
187
  return;
154
- const isRelative = src?.startsWith("/");
155
- let imageBuffer;
156
- if (isRelative) {
157
- if (import.meta.prerender || import.meta.dev) {
158
- const srcWithoutBase = src.replace(runtimeConfig.app.baseURL, "/");
159
- imageBuffer = await resolveLocalFilePathImage(publicStoragePath, srcWithoutBase);
160
- }
161
- if (!imageBuffer) {
162
- imageBuffer = await e.$fetch(src, { responseType: "arrayBuffer" }).catch(() => {
163
- });
164
- if (!imageBuffer && !import.meta.prerender) {
165
- imageBuffer = await e.$fetch(src, {
166
- baseURL: getNitroOrigin(e),
167
- responseType: "arrayBuffer"
168
- }).catch(() => {
169
- });
170
- }
171
- }
172
- } else {
173
- const decodedSrc = decodeHtml(src);
174
- if (!import.meta.dev && isBlockedUrl(decodedSrc)) {
175
- logger.warn(`Blocked internal background-image fetch: ${decodedSrc}`);
176
- delete node.props.style.backgroundImage;
177
- } else {
178
- imageBuffer = await $fetch(decodedSrc, {
179
- responseType: "arrayBuffer"
180
- }).catch(() => {
181
- });
182
- }
183
- }
184
- if (imageBuffer) {
185
- const buffer = imageBuffer instanceof ArrayBuffer ? imageBuffer : imageBuffer.buffer;
186
- node.props.style.backgroundImage = `url(${toBase64Image(buffer)})`;
188
+ const result = await resolveSrcToBuffer(src, "background-image", ctx);
189
+ if (result.blocked) {
190
+ delete node.props.style.backgroundImage;
191
+ return;
187
192
  }
193
+ if (result.buffer)
194
+ node.props.style.backgroundImage = `url(${toBufferSourceAsBase64(result.buffer)})`;
188
195
  }
189
196
  }
190
197
  ]);
@@ -1,5 +1,6 @@
1
1
  import { emojiCache } from "#og-image-cache";
2
2
  import { $fetch } from "ofetch";
3
+ import { getFetchTimeout } from "../../../../util/fetchTimeout.js";
3
4
  import { getEmojiCodePoint, getEmojiIconNames } from "./emoji-utils.js";
4
5
  export async function getEmojiSvg(ctx, emojiChar) {
5
6
  const emojiSet = ctx.options.emojis;
@@ -15,21 +16,31 @@ export async function getEmojiSvg(ctx, emojiChar) {
15
16
  }
16
17
  }
17
18
  if (!svg) {
18
- for (const iconName of possibleNames) {
19
- try {
20
- svg = await $fetch(`https://api.iconify.design/${emojiSet}/${iconName}.svg`, {
21
- responseType: "text",
22
- retry: 2,
23
- retryDelay: 500
24
- });
25
- if (svg && svg !== "404") {
26
- const key = ["1", emojiSet, iconName].join("|");
27
- await emojiCache.setItem(key, svg);
19
+ const timeout = getFetchTimeout(ctx.runtimeConfig);
20
+ const deadline = AbortSignal.timeout(timeout);
21
+ const endTiming = ctx.timings.start("emoji-fetch");
22
+ try {
23
+ for (const iconName of possibleNames) {
24
+ if (deadline.aborted)
28
25
  break;
26
+ try {
27
+ svg = await $fetch(`https://api.iconify.design/${emojiSet}/${iconName}.svg`, {
28
+ responseType: "text",
29
+ retry: 0,
30
+ signal: deadline,
31
+ timeout
32
+ });
33
+ if (svg && svg !== "404") {
34
+ const key = ["1", emojiSet, iconName].join("|");
35
+ await emojiCache.setItem(key, svg);
36
+ break;
37
+ }
38
+ } catch {
39
+ svg = null;
29
40
  }
30
- } catch {
31
- svg = null;
32
41
  }
42
+ } finally {
43
+ endTiming();
33
44
  }
34
45
  }
35
46
  if (svg) {
@@ -23,7 +23,7 @@ function withWarningCapture(fn) {
23
23
  });
24
24
  }
25
25
  export async function createSvg(event) {
26
- const { options } = event;
26
+ const { options, timings } = event;
27
27
  const { satoriOptions: _satoriOptions } = useOgImageRuntimeConfig();
28
28
  const { fontFamilyOverride, defaultFont } = getDefaultFontFamily(options);
29
29
  const [satori, vnodes] = await Promise.all([
@@ -32,13 +32,13 @@ export async function createSvg(event) {
32
32
  ]);
33
33
  const codepoints = extractCodepoints(vnodes);
34
34
  const hasCustomFonts = Array.isArray(options.fonts) && options.fonts.length > 0;
35
- const fonts = await loadFontsForRenderer(event, {
35
+ const fonts = await timings.measure("font-load", () => loadFontsForRenderer(event, {
36
36
  supportedFormats: /* @__PURE__ */ new Set(["ttf", "otf", "woff"]),
37
37
  component: options.component,
38
38
  fontFamilyOverride: fontFamilyOverride || defaultFont,
39
39
  codepoints,
40
40
  fontDefs: options.fonts
41
- });
41
+ }));
42
42
  await event._nitro.hooks.callHook("nuxt-og-image:satori:vnodes", vnodes, event);
43
43
  const satoriFonts = !hasCustomFonts && _satoriFontCache.get(fonts) || fonts.map((f) => ({ ...f, name: f.family }));
44
44
  if (!hasCustomFonts)
@@ -80,9 +80,9 @@ export async function createSvg(event) {
80
80
  width: options.width,
81
81
  height: options.height
82
82
  });
83
- const { result, warnings } = await withWarningCapture(
83
+ const { result, warnings } = await timings.measure("render-satori", () => withWarningCapture(
84
84
  () => satori(vnodes, satoriOptions)
85
- );
85
+ ));
86
86
  return { svg: result, warnings, fonts };
87
87
  }
88
88
  async function createPng(event) {
@@ -92,14 +92,16 @@ async function createPng(event) {
92
92
  throw new Error("Failed to create SVG");
93
93
  const options = defu(event.options.resvg, resvgOptions);
94
94
  const Resvg = await getResvg();
95
- const resvg = new Resvg(svg, options);
96
- const pngData = resvg.render();
97
- const png = pngData.asPng();
98
- if (typeof pngData.free === "function")
99
- pngData.free();
100
- if (typeof resvg.free === "function")
101
- resvg.free();
102
- return png;
95
+ return event.timings.measure("render-resvg", () => {
96
+ const resvg = new Resvg(svg, options);
97
+ const pngData = resvg.render();
98
+ const png = pngData.asPng();
99
+ if (typeof pngData.free === "function")
100
+ pngData.free();
101
+ if (typeof resvg.free === "function")
102
+ resvg.free();
103
+ return png;
104
+ });
103
105
  }
104
106
  async function createJpeg(event) {
105
107
  const { sharpOptions } = useOgImageRuntimeConfig();
@@ -115,7 +117,7 @@ async function createJpeg(event) {
115
117
  throw new Error("Sharp dependency could not be loaded. Please check you have it installed and are using a compatible runtime.");
116
118
  });
117
119
  const options = defu(event.options.sharp, sharpOptions);
118
- return sharp(svgBuffer, options).jpeg(options).toBuffer();
120
+ return event.timings.measure("render-sharp", () => sharp(svgBuffer, options).jpeg(options).toBuffer());
119
121
  }
120
122
  function rewriteVNodeFontFamilies(node, subsetChains) {
121
123
  const style = node.props?.style;