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.
- package/dist/chunks/tw4.cjs +1 -1
- package/dist/chunks/tw4.mjs +1 -1
- package/dist/chunks/uno.cjs +1 -1
- package/dist/chunks/uno.mjs +1 -1
- package/dist/devtools/200.html +1 -1
- package/dist/devtools/404.html +1 -1
- package/dist/devtools/_nuxt/{BWm573-p.js → B-y6Zfh-.js} +1 -1
- package/dist/devtools/_nuxt/{yBiBpwD7.js → C5LFIfwi.js} +6 -6
- package/dist/devtools/_nuxt/CN79P4uE.js +6 -0
- package/dist/devtools/_nuxt/{BQDcSiMf.js → CThtpBJK.js} +1 -1
- package/dist/devtools/_nuxt/{BsivBvAU.js → CYOZ55V_.js} +1 -1
- package/dist/devtools/_nuxt/{Cy0omYQh.js → ClxM7Lmy.js} +1 -1
- package/dist/devtools/_nuxt/{B6Dg3dZ6.js → CqGOSK9s.js} +1 -1
- package/dist/devtools/_nuxt/{DziLl24l.js → CwlJb64V.js} +1 -1
- package/dist/devtools/_nuxt/{BMVWkjCo.js → CxhRt3FZ.js} +1 -1
- package/dist/devtools/_nuxt/{w_tq7NMl.js → DLMIIqSE.js} +1 -1
- package/dist/devtools/_nuxt/DevtoolsSection.Be9AJQOh.css +1 -0
- package/dist/devtools/_nuxt/DevtoolsSnippet.BubiVHug.css +1 -0
- package/dist/devtools/_nuxt/{CHeKziWa.js → DjEkFT0U.js} +1 -1
- package/dist/devtools/_nuxt/builds/latest.json +1 -1
- package/dist/devtools/_nuxt/builds/meta/ca9f1307-1f7b-4a5f-b061-2c680927caac.json +1 -0
- package/dist/devtools/_nuxt/{entry.BjD2aghs.css → entry.CJ6yFnTt.css} +1 -1
- package/dist/devtools/_nuxt/{pages.DO0dnDUs.css → pages.D8s6dQja.css} +1 -1
- package/dist/devtools/_nuxt/renderer-select.cI9Vfr5y.css +1 -0
- package/dist/devtools/debug/index.html +1 -1
- package/dist/devtools/docs/index.html +1 -1
- package/dist/devtools/index.html +1 -1
- package/dist/devtools/templates/index.html +1 -1
- package/dist/module.cjs +1 -1
- package/dist/module.d.cts +11 -0
- package/dist/module.d.mts +11 -0
- package/dist/module.d.ts +11 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/app/client-utils.js +24 -5
- package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.js +9 -18
- package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +8 -4
- package/dist/runtime/server/og-image/bindings/font-assets/node.js +6 -2
- package/dist/runtime/server/og-image/browser/screenshot.d.ts +1 -1
- package/dist/runtime/server/og-image/browser/screenshot.js +6 -4
- package/dist/runtime/server/og-image/context.js +4 -0
- package/dist/runtime/server/og-image/core/plugins/imageSrc.js +106 -99
- package/dist/runtime/server/og-image/core/transforms/emojis/fetch.js +23 -12
- package/dist/runtime/server/og-image/satori/renderer.js +16 -14
- package/dist/runtime/server/og-image/takumi/renderer.js +27 -19
- package/dist/runtime/server/util/cloudflareAssets.d.ts +24 -0
- package/dist/runtime/server/util/cloudflareAssets.js +16 -0
- package/dist/runtime/server/util/eventHandlers.js +28 -5
- package/dist/runtime/server/util/fetchLocalAsset.d.ts +25 -0
- package/dist/runtime/server/util/fetchLocalAsset.js +34 -0
- package/dist/runtime/server/util/fetchTimeout.d.ts +2 -0
- package/dist/runtime/server/util/fetchTimeout.js +7 -0
- package/dist/runtime/server/util/timings.d.ts +17 -0
- package/dist/runtime/server/util/timings.js +75 -0
- package/dist/runtime/types.d.ts +3 -0
- package/dist/shared/{nuxt-og-image.BK0-aZom.mjs → nuxt-og-image.Cr3WHMk1.mjs} +16 -11
- package/dist/shared/{nuxt-og-image.C2oXAHiT.cjs → nuxt-og-image.TJuh6pW5.cjs} +17 -12
- package/package.json +9 -9
- package/dist/devtools/_nuxt/DDRo8-tD.js +0 -6
- package/dist/devtools/_nuxt/DevtoolsSection.C-PGRg5f.css +0 -1
- package/dist/devtools/_nuxt/DevtoolsSnippet.BipAyEUC.css +0 -1
- package/dist/devtools/_nuxt/builds/meta/150c0674-b387-4525-9043-e05dd343db8e.json +0 -1
- package/dist/devtools/_nuxt/renderer-select.J57nTUNW.css +0 -1
package/dist/module.json
CHANGED
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
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,
|
|
156
|
+
transform: async (node, ctx) => {
|
|
74
157
|
let src = node.props.src;
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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,
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
pngData.free
|
|
100
|
-
|
|
101
|
-
resvg.free
|
|
102
|
-
|
|
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;
|