nuxt-og-image 6.4.7 → 6.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/Axr0GNgb.js +23 -0
- package/dist/devtools/_nuxt/BFnUFv7a.js +6 -0
- package/dist/devtools/_nuxt/BLdiDWsd.js +3 -0
- package/dist/devtools/_nuxt/BPuj2Ioi.js +1 -0
- package/dist/devtools/_nuxt/BhUO5EEy.js +4 -0
- package/dist/devtools/_nuxt/{C2qZkcnS.js → Bu77Nt5h.js} +1 -1
- package/dist/devtools/_nuxt/CbxDj8dt.js +2 -0
- package/dist/devtools/_nuxt/CeQOmnUG.js +1 -0
- package/dist/devtools/_nuxt/Ceo32Wod.js +1 -0
- package/dist/devtools/_nuxt/{B75jVnAo.js → D-SbM8XJ.js} +1 -1
- package/dist/devtools/_nuxt/DWbsyffp.js +152 -0
- package/dist/devtools/_nuxt/Dd8dEdTA.js +3 -0
- package/dist/devtools/_nuxt/DevtoolsSection.BsOFzz12.css +1 -0
- package/dist/devtools/_nuxt/DevtoolsSnippet.DTkjQmsJ.css +1 -0
- package/dist/devtools/_nuxt/{IFrameLoader.HTWy4e5S.css → IFrameLoader.Bd09J70F.css} +1 -1
- package/dist/devtools/_nuxt/builds/latest.json +1 -1
- package/dist/devtools/_nuxt/builds/meta/df7e4f86-26df-4aa8-839c-9d7fb31dd16b.json +1 -0
- package/dist/devtools/_nuxt/entry.D7nBa7-d.css +2 -0
- package/dist/devtools/_nuxt/{D5adIWRn.js → oq4DzcWM.js} +1 -1
- package/dist/devtools/_nuxt/{pages.DNfeVIDd.css → pages.D6uci9iO.css} +1 -1
- package/dist/devtools/_nuxt/renderer-select.Pd5oWk7V.css +1 -0
- package/dist/devtools/_nuxt/{templates.C7e57cTp.css → templates.BHO7w0UG.css} +1 -1
- 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.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/app/client-utils.js +1 -1
- package/dist/runtime/app/components/Templates/Community/SimpleBlog.satori.vue +1 -1
- package/dist/runtime/app/utils/plugins.js +4 -4
- package/dist/runtime/app/utils.js +1 -1
- package/dist/runtime/server/og-image/bindings/browser/chrome-launcher.js +2 -1
- package/dist/runtime/server/og-image/bindings/browser/cloudflare.js +4 -2
- package/dist/runtime/server/og-image/bindings/browser/on-demand.js +2 -1
- package/dist/runtime/server/og-image/bindings/browser/playwright.js +2 -1
- package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +1 -1
- package/dist/runtime/server/og-image/bindings/font-assets/node.js +1 -1
- package/dist/runtime/server/og-image/browser/renderer.js +1 -1
- package/dist/runtime/server/og-image/browser/screenshot.js +2 -2
- package/dist/runtime/server/og-image/cache/buildCache.js +1 -1
- package/dist/runtime/server/og-image/context.js +12 -5
- package/dist/runtime/server/og-image/core/plugins/imageSrc.js +30 -59
- package/dist/runtime/server/og-image/core/transforms/emojis/fetch.js +1 -1
- package/dist/runtime/server/og-image/core/vnodes.js +3 -2
- package/dist/runtime/server/og-image/devtools.js +1 -1
- package/dist/runtime/server/og-image/font-source.d.ts +20 -0
- package/dist/runtime/server/og-image/font-source.js +20 -0
- package/dist/runtime/server/og-image/fonts.d.ts +8 -1
- package/dist/runtime/server/og-image/fonts.js +22 -32
- package/dist/runtime/server/og-image/satori/plugins/emojis.js +1 -1
- package/dist/runtime/server/og-image/satori/renderer.js +9 -3
- package/dist/runtime/server/og-image/takumi/nodes.js +1 -1
- package/dist/runtime/server/og-image/takumi/renderer.js +76 -24
- package/dist/runtime/server/og-image/templates/html.js +3 -2
- package/dist/runtime/server/plugins/prerender.js +2 -2
- package/dist/runtime/server/routes/debug.json.js +2 -2
- package/dist/runtime/server/routes/resolve.js +2 -2
- package/dist/runtime/server/util/eventHandlers.js +3 -2
- package/dist/runtime/server/util/fetchLocalAsset.js +1 -1
- package/dist/runtime/server/util/kit.d.ts +1 -1
- package/dist/runtime/server/util/kit.js +5 -2
- package/dist/runtime/server/util/options.js +1 -1
- package/dist/runtime/server/util/ssrf.d.ts +27 -0
- package/dist/runtime/server/util/ssrf.js +165 -0
- package/dist/runtime/server/util/withTimeout.d.ts +1 -0
- package/dist/runtime/server/util/withTimeout.js +12 -0
- package/dist/runtime/server/utils.js +6 -2
- package/dist/shared/{nuxt-og-image.Cr3WHMk1.mjs → nuxt-og-image.CW3PwVsO.mjs} +7 -2
- package/dist/shared/{nuxt-og-image.TJuh6pW5.cjs → nuxt-og-image.DnwTVrnM.cjs} +8 -3
- package/package.json +18 -17
- package/types/virtual.d.ts +1 -1
- package/dist/devtools/_nuxt/671cdwR5.js +0 -1
- package/dist/devtools/_nuxt/8kkJjsM6.js +0 -2
- package/dist/devtools/_nuxt/BJ9iyiD_.js +0 -3
- package/dist/devtools/_nuxt/BcGh1UWn.js +0 -23
- package/dist/devtools/_nuxt/CHmOAOu6.js +0 -3
- package/dist/devtools/_nuxt/Ci8ZvNRB.js +0 -1
- package/dist/devtools/_nuxt/DVtbGlya.js +0 -152
- package/dist/devtools/_nuxt/DbOyl855.js +0 -1
- package/dist/devtools/_nuxt/DevtoolsSection.DTZAmexP.css +0 -1
- package/dist/devtools/_nuxt/DevtoolsSnippet.Cd7XR-3f.css +0 -1
- package/dist/devtools/_nuxt/DrqfEi04.js +0 -4
- package/dist/devtools/_nuxt/builds/meta/d8fa30f4-68dd-4f57-a1c2-1c3a852f7ee7.json +0 -1
- package/dist/devtools/_nuxt/cIRSrPLM.js +0 -6
- package/dist/devtools/_nuxt/entry.zgUFdiSv.css +0 -2
- package/dist/devtools/_nuxt/renderer-select.elTTxv30.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { useSiteConfig } from "#site-config/app/composables";
|
|
3
2
|
import { parseURL } from "ufo";
|
|
4
3
|
import { computed } from "vue";
|
|
4
|
+
import { useSiteConfig } from "#site-config/app/composables";
|
|
5
5
|
const props = defineProps({
|
|
6
6
|
colorMode: { type: String, required: false, default: "light" },
|
|
7
7
|
title: { type: String, required: false, default: "title" },
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { useRequestEvent } from "#app";
|
|
2
|
-
import { withSiteUrl } from "#site-config/app/composables";
|
|
3
1
|
import { TemplateParamsPlugin } from "@unhead/vue/plugins";
|
|
4
2
|
import { defu } from "defu";
|
|
5
3
|
import { createRouter as createRadixRouter, toRouteMatcher } from "radix3";
|
|
6
4
|
import { parseURL, withoutBase } from "ufo";
|
|
7
5
|
import { toValue } from "vue";
|
|
6
|
+
import { useRequestEvent } from "#app";
|
|
7
|
+
import { withSiteUrl } from "#site-config/app/composables";
|
|
8
8
|
import { createOgImageMeta, getOgImagePath } from "../../app/utils.js";
|
|
9
9
|
import { isInternalRoute } from "../../shared.js";
|
|
10
10
|
const RE_COMMA = /,/g;
|
|
@@ -19,7 +19,7 @@ export function ogImageCanonicalUrls(nuxtApp) {
|
|
|
19
19
|
ssrContext?.head.use({
|
|
20
20
|
key: "nuxt-og-image:overrides-and-canonical-urls",
|
|
21
21
|
hooks: {
|
|
22
|
-
"tags:afterResolve":
|
|
22
|
+
"tags:afterResolve": (ctx2) => {
|
|
23
23
|
let title = "";
|
|
24
24
|
let description = "";
|
|
25
25
|
for (const tag of ctx2.tags) {
|
|
@@ -39,7 +39,7 @@ export function ogImageCanonicalUrls(nuxtApp) {
|
|
|
39
39
|
}
|
|
40
40
|
tag.props.content = tag.props.content.replaceAll("%title", title).replaceAll("%description", description).replaceAll(" ", "+");
|
|
41
41
|
if (!tag.props.content?.startsWith("https")) {
|
|
42
|
-
|
|
42
|
+
nuxtApp.runWithContext(() => {
|
|
43
43
|
tag.props.content = toValue(withSiteUrl(tag.props.content || "", {
|
|
44
44
|
withBase: true,
|
|
45
45
|
canonical: !import.meta.dev
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { componentNames } from "#build/nuxt-og-image/components.mjs";
|
|
2
1
|
import { defu } from "defu";
|
|
3
2
|
import { stringify } from "devalue";
|
|
4
3
|
import { useHead, useRuntimeConfig } from "nuxt/app";
|
|
5
4
|
import { joinURL, withQuery } from "ufo";
|
|
6
5
|
import { isRef, toValue } from "vue";
|
|
6
|
+
import { componentNames } from "#build/nuxt-og-image/components.mjs";
|
|
7
7
|
import { buildOgImageUrl, generateMeta, separateProps } from "../shared.js";
|
|
8
8
|
function resolveUnrefHeadInput(input) {
|
|
9
9
|
if (input == null)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withTimeout } from "../../../util/withTimeout.js";
|
|
1
2
|
import { useOgImageRuntimeConfig } from "../../../utils.js";
|
|
2
3
|
let puppeteer;
|
|
3
4
|
async function getPuppeteer() {
|
|
@@ -25,7 +26,8 @@ export async function createBrowser(event) {
|
|
|
25
26
|
`[Nuxt OG Image] Cloudflare browser binding "${bindingName}" not found. Ensure it's configured in wrangler.toml and the request has cloudflare context.`
|
|
26
27
|
);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
const launchTimeout = useOgImageRuntimeConfig().security?.renderTimeout ?? 15e3;
|
|
30
|
+
browserPromise = withTimeout((async () => {
|
|
29
31
|
const pptr = await getPuppeteer();
|
|
30
32
|
const sessions = await pptr.sessions(binding);
|
|
31
33
|
const existingSession = sessions.find((s) => !s.connected);
|
|
@@ -38,7 +40,7 @@ export async function createBrowser(event) {
|
|
|
38
40
|
browser = null;
|
|
39
41
|
});
|
|
40
42
|
return browser;
|
|
41
|
-
})();
|
|
43
|
+
})(), launchTimeout, "cloudflare browser launch");
|
|
42
44
|
browser = await browserPromise;
|
|
43
45
|
browserPromise = null;
|
|
44
46
|
return browser;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { buildDir, rootDir } from "#og-image-virtual/build-dir.mjs";
|
|
3
2
|
import { getRequestURL } from "h3";
|
|
4
3
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
5
4
|
import { join } from "pathe";
|
|
6
5
|
import { withBase } from "ufo";
|
|
6
|
+
import { buildDir, rootDir } from "#og-image-virtual/build-dir.mjs";
|
|
7
7
|
import { getFetchTimeout } from "../../../util/fetchTimeout.js";
|
|
8
8
|
import { useOgImageRuntimeConfig } from "../../../utils.js";
|
|
9
9
|
let fontUrlMapping;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getNitroOrigin } from "#site-config/server/composables";
|
|
2
1
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
2
|
import { withBase } from "ufo";
|
|
3
|
+
import { getNitroOrigin } from "#site-config/server/composables";
|
|
4
4
|
import { getFetchTimeout } from "../../../util/fetchTimeout.js";
|
|
5
5
|
import { useOgImageRuntimeConfig } from "../../../utils.js";
|
|
6
6
|
export async function resolve(event, font) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getNitroOrigin } from "#site-config/server/composables";
|
|
2
1
|
import { withQuery } from "ufo";
|
|
3
2
|
import { toValue } from "vue";
|
|
3
|
+
import { getNitroOrigin } from "#site-config/server/composables";
|
|
4
4
|
import { buildOgImageUrl } from "../../../shared.js";
|
|
5
5
|
import { getFetchTimeout } from "../../util/fetchTimeout.js";
|
|
6
6
|
import { logger } from "../../util/logger.js";
|
|
@@ -55,7 +55,7 @@ async function takeScreenshot(page, selector, options) {
|
|
|
55
55
|
return await page.screenshot(puppeteerOptions);
|
|
56
56
|
}
|
|
57
57
|
export async function createScreenshot({ basePath, e, options, extension, timings }, browser) {
|
|
58
|
-
const runtimeConfig = useOgImageRuntimeConfig();
|
|
58
|
+
const runtimeConfig = useOgImageRuntimeConfig(e);
|
|
59
59
|
const { colorPreference, defaults, security } = runtimeConfig;
|
|
60
60
|
const path = options.component === "PageScreenshot" ? basePath : buildOgImageUrl(options, "html", false, defaults, security?.secret || void 0).url;
|
|
61
61
|
let page;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { componentNames } from "#og-image-virtual/component-names.mjs";
|
|
3
2
|
import { join } from "pathe";
|
|
3
|
+
import { componentNames } from "#og-image-virtual/component-names.mjs";
|
|
4
4
|
import { hashOgImageOptions } from "../../../shared/urlEncoding.js";
|
|
5
5
|
import { useOgImageRuntimeConfig } from "../../utils.js";
|
|
6
6
|
export function getComponentHash(componentName) {
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { prerenderOptionsCache } from "#og-image-cache";
|
|
2
|
-
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
3
|
-
import { createSitePathResolver } from "#site-config/server/composables/utils";
|
|
4
1
|
import { defu } from "defu";
|
|
5
2
|
import { createError, getQuery } from "h3";
|
|
6
3
|
import { useNitroApp } from "nitropack/runtime";
|
|
7
4
|
import { hash } from "ohash";
|
|
8
5
|
import { parseURL, withoutLeadingSlash, withoutTrailingSlash, withQuery } from "ufo";
|
|
9
6
|
import { normalizeKey } from "unstorage";
|
|
7
|
+
import { prerenderOptionsCache } from "#og-image-cache";
|
|
8
|
+
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
9
|
+
import { createSitePathResolver } from "#site-config/server/composables/utils";
|
|
10
10
|
import { logger } from "../../logger.js";
|
|
11
11
|
import { decodeOgImageParams, extractEncodedSegment, sanitizeProps, separateProps, verifyOgImageSignature } from "../../shared.js";
|
|
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
15
|
import { createTimings, TIMING_CTX_KEY } from "../util/timings.js";
|
|
16
|
+
import { withTimeout } from "../util/withTimeout.js";
|
|
16
17
|
import { useOgImageRuntimeConfig } from "../utils.js";
|
|
17
18
|
import { getBrowserRenderer, getSatoriRenderer, getTakumiRenderer } from "./instances.js";
|
|
18
19
|
const RE_HASH_MODE = /^o_([a-z0-9]+)$/i;
|
|
@@ -34,7 +35,7 @@ export function resolvePathCacheKey(e, path, resolvedOptions) {
|
|
|
34
35
|
].join(":");
|
|
35
36
|
}
|
|
36
37
|
export async function resolveContext(e) {
|
|
37
|
-
const runtimeConfig = useOgImageRuntimeConfig();
|
|
38
|
+
const runtimeConfig = useOgImageRuntimeConfig(e);
|
|
38
39
|
const resolvePathWithBase = createSitePathResolver(e, {
|
|
39
40
|
absolute: false,
|
|
40
41
|
withBase: true
|
|
@@ -200,8 +201,14 @@ export async function resolveContext(e) {
|
|
|
200
201
|
basePath,
|
|
201
202
|
options: normalised.options,
|
|
202
203
|
timings,
|
|
204
|
+
// @ts-expect-error hookable v6
|
|
203
205
|
_nitro: useNitroApp()
|
|
204
206
|
};
|
|
205
|
-
|
|
207
|
+
const hookTimeout = runtimeConfig.security?.renderTimeout ?? 15e3;
|
|
208
|
+
await withTimeout(
|
|
209
|
+
ctx._nitro.hooks.callHook("nuxt-og-image:context", ctx),
|
|
210
|
+
hookTimeout,
|
|
211
|
+
"nuxt-og-image:context hook"
|
|
212
|
+
);
|
|
206
213
|
return ctx;
|
|
207
214
|
}
|
|
@@ -1,63 +1,14 @@
|
|
|
1
|
-
import { getNitroOrigin } from "#site-config/server/composables/getNitroOrigin";
|
|
2
1
|
import { useStorage } from "nitropack/runtime";
|
|
3
2
|
import { withBase, withoutLeadingSlash } from "ufo";
|
|
3
|
+
import { getNitroOrigin } from "#site-config/server/composables/getNitroOrigin";
|
|
4
4
|
import { toBase64Image } from "../../../../shared.js";
|
|
5
5
|
import { decodeHtml } from "../../../util/encoding.js";
|
|
6
6
|
import { fetchLocalAsset } from "../../../util/fetchLocalAsset.js";
|
|
7
7
|
import { getFetchTimeout } from "../../../util/fetchTimeout.js";
|
|
8
8
|
import { logger } from "../../../util/logger.js";
|
|
9
|
+
import { fetchWithRedirectValidation, isBlockedUrl } from "../../../util/ssrf.js";
|
|
9
10
|
import { getImageDimensions } from "../../utils/image-detector.js";
|
|
10
11
|
import { defineTransformer } from "../plugins.js";
|
|
11
|
-
const RE_IPV6_BRACKETS = /^\[|\]$/g;
|
|
12
|
-
const RE_MAPPED_V4 = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/;
|
|
13
|
-
const RE_DIGIT_ONLY = /^\d+$/;
|
|
14
|
-
const RE_INT_IP = /^(?:0x[\da-f]+|\d+)$/i;
|
|
15
|
-
function isPrivateIPv4(a, b) {
|
|
16
|
-
if (a === 127)
|
|
17
|
-
return true;
|
|
18
|
-
if (a === 10)
|
|
19
|
-
return true;
|
|
20
|
-
if (a === 172 && b >= 16 && b <= 31)
|
|
21
|
-
return true;
|
|
22
|
-
if (a === 192 && b === 168)
|
|
23
|
-
return true;
|
|
24
|
-
if (a === 169 && b === 254)
|
|
25
|
-
return true;
|
|
26
|
-
if (a === 0)
|
|
27
|
-
return true;
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
function isBlockedUrl(url) {
|
|
31
|
-
let parsed;
|
|
32
|
-
try {
|
|
33
|
-
parsed = new URL(url);
|
|
34
|
-
} catch {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
38
|
-
return true;
|
|
39
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
40
|
-
const bare = hostname.replace(RE_IPV6_BRACKETS, "");
|
|
41
|
-
if (bare === "localhost" || bare.endsWith(".localhost"))
|
|
42
|
-
return true;
|
|
43
|
-
const mappedV4 = bare.match(RE_MAPPED_V4);
|
|
44
|
-
const ip = mappedV4 ? mappedV4[1] : bare;
|
|
45
|
-
const parts = ip.split(".");
|
|
46
|
-
if (parts.length === 4 && parts.every((p) => RE_DIGIT_ONLY.test(p))) {
|
|
47
|
-
const octets = parts.map(Number);
|
|
48
|
-
if (octets.some((o) => o > 255))
|
|
49
|
-
return true;
|
|
50
|
-
return isPrivateIPv4(octets[0], octets[1]);
|
|
51
|
-
}
|
|
52
|
-
if (RE_INT_IP.test(ip)) {
|
|
53
|
-
const num = Number(ip);
|
|
54
|
-
if (!Number.isNaN(num) && num >= 0 && num <= 4294967295)
|
|
55
|
-
return isPrivateIPv4(num >> 24 & 255, num >> 16 & 255);
|
|
56
|
-
}
|
|
57
|
-
if (bare === "::1" || bare.startsWith("fc") || bare.startsWith("fd") || bare.startsWith("fe80"))
|
|
58
|
-
return true;
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
12
|
const RE_URL_LEADING = /^url\(['"]?/;
|
|
62
13
|
const RE_URL_TRAILING = /['"]?\)$/;
|
|
63
14
|
const SUBREQUEST_HEADERS = { "x-nuxt-og-image": "1" };
|
|
@@ -120,12 +71,24 @@ async function doResolveSrcToBuffer(src, kind, { e, publicStoragePath, runtimeCo
|
|
|
120
71
|
return { blocked: true };
|
|
121
72
|
}
|
|
122
73
|
const end = timings.start("image-fetch");
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
74
|
+
let buffer;
|
|
75
|
+
if (import.meta.dev) {
|
|
76
|
+
buffer = await $fetch(decodedSrc, {
|
|
77
|
+
responseType: "arrayBuffer",
|
|
78
|
+
timeout: fetchTimeout
|
|
79
|
+
}).catch((err) => {
|
|
80
|
+
logFailure(decodedSrc, err);
|
|
81
|
+
}).finally(end);
|
|
82
|
+
} else {
|
|
83
|
+
const ab = await fetchWithRedirectValidation(decodedSrc, {
|
|
84
|
+
timeout: fetchTimeout
|
|
85
|
+
}).catch((err) => {
|
|
86
|
+
logFailure(decodedSrc, err);
|
|
87
|
+
return null;
|
|
88
|
+
}).finally(end);
|
|
89
|
+
if (ab)
|
|
90
|
+
buffer = new Uint8Array(ab);
|
|
91
|
+
}
|
|
129
92
|
return buffer ? { buffer } : {};
|
|
130
93
|
}
|
|
131
94
|
function applyImageDimensions(node, buffer) {
|
|
@@ -173,8 +136,12 @@ export default defineTransformer([
|
|
|
173
136
|
applyImageDimensions(node, result.buffer);
|
|
174
137
|
return;
|
|
175
138
|
}
|
|
176
|
-
if (isRelative)
|
|
139
|
+
if (isRelative) {
|
|
177
140
|
node.props.src = withBase(src, `${getNitroOrigin(ctx.e)}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!import.meta.dev)
|
|
144
|
+
delete node.props.src;
|
|
178
145
|
}
|
|
179
146
|
},
|
|
180
147
|
// fix style="background-image: url('')"
|
|
@@ -190,8 +157,12 @@ export default defineTransformer([
|
|
|
190
157
|
delete node.props.style.backgroundImage;
|
|
191
158
|
return;
|
|
192
159
|
}
|
|
193
|
-
if (result.buffer)
|
|
160
|
+
if (result.buffer) {
|
|
194
161
|
node.props.style.backgroundImage = `url(${toBufferSourceAsBase64(result.buffer)})`;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!import.meta.dev && !src.startsWith("/"))
|
|
165
|
+
delete node.props.style.backgroundImage;
|
|
195
166
|
}
|
|
196
167
|
}
|
|
197
168
|
]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { emojiCache } from "#og-image-cache";
|
|
2
1
|
import { $fetch } from "ofetch";
|
|
2
|
+
import { emojiCache } from "#og-image-cache";
|
|
3
3
|
import { getFetchTimeout } from "../../../../util/fetchTimeout.js";
|
|
4
4
|
import { getEmojiCodePoint, getEmojiIconNames } from "./emoji-utils.js";
|
|
5
5
|
export async function getEmojiSvg(ctx, emojiChar) {
|
|
@@ -178,7 +178,8 @@ export async function createVNodes(ctx, options) {
|
|
|
178
178
|
logger.warn("The `html` option is deprecated and will be removed in the next major version. Use a Vue component instead.");
|
|
179
179
|
}
|
|
180
180
|
if (!html) {
|
|
181
|
-
const
|
|
181
|
+
const islandTimeout = ctx.runtimeConfig.security?.renderTimeout ?? 15e3;
|
|
182
|
+
const island = await ctx.timings.measure("island-fetch", () => fetchIsland(ctx.e, ctx.options.component, typeof ctx.options.props !== "undefined" ? ctx.options.props : ctx.options, islandTimeout));
|
|
182
183
|
island.html = htmlDecodeQuotes(island.html);
|
|
183
184
|
await applyEmojis(ctx, island);
|
|
184
185
|
html = island.html;
|
|
@@ -195,6 +196,6 @@ export async function createVNodes(ctx, options) {
|
|
|
195
196
|
rootChild.props.style = { width: "100%", height: "100%", ...rootChild.props.style };
|
|
196
197
|
}
|
|
197
198
|
warnUnsupportedSvgElements(vnodeTree, ctx.options.component);
|
|
198
|
-
await Promise.all(walkTree(ctx, vnodeTree, [encoding, styleDirectives, imageSrc]));
|
|
199
|
+
await ctx.timings.measure("vnode-walk", () => Promise.all(walkTree(ctx, vnodeTree, [encoding, styleDirectives, imageSrc])));
|
|
199
200
|
return vnodeTree;
|
|
200
201
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { htmlPayloadCache } from "#og-image-cache";
|
|
2
1
|
import { parse } from "devalue";
|
|
3
2
|
import { createError } from "h3";
|
|
3
|
+
import { htmlPayloadCache } from "#og-image-cache";
|
|
4
4
|
import { extractSocialPreviewTags } from "../../pure.js";
|
|
5
5
|
import { logger } from "../util/logger.js";
|
|
6
6
|
const PAYLOAD_REGEX = /<script.+id="nuxt-og-image-options"[^>]*>(.+?)<\/script>/;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type FontFormat = 'ttf' | 'otf' | 'woff' | 'woff2';
|
|
2
|
+
export declare function fontFormat(src: string): FontFormat;
|
|
3
|
+
/**
|
|
4
|
+
* Pick the src to actually load for a parsed font entry.
|
|
5
|
+
*
|
|
6
|
+
* - When `preferStatic` is set (takumi), prefers the full static satoriSrc over a subset WOFF2
|
|
7
|
+
* primary src. @nuxt/fonts CSS often ships only the latin subset for a family, so using the
|
|
8
|
+
* subset would hide non-latin glyphs (devanagari, CJK) the static file covers.
|
|
9
|
+
* - Otherwise, uses the primary src when the renderer can parse its format, falling back to
|
|
10
|
+
* satoriSrc as a static alternative when the primary format is unsupported (satori + WOFF2).
|
|
11
|
+
*
|
|
12
|
+
* Returns null when no src on this entry can be parsed by the renderer.
|
|
13
|
+
*/
|
|
14
|
+
export declare function selectFontSource(f: {
|
|
15
|
+
src: string;
|
|
16
|
+
satoriSrc?: string;
|
|
17
|
+
}, supportedFormats: Set<FontFormat>, preferStatic: boolean): {
|
|
18
|
+
src: string;
|
|
19
|
+
isStaticFallback: boolean;
|
|
20
|
+
} | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function fontFormat(src) {
|
|
2
|
+
if (src.endsWith(".woff2"))
|
|
3
|
+
return "woff2";
|
|
4
|
+
if (src.endsWith(".woff"))
|
|
5
|
+
return "woff";
|
|
6
|
+
if (src.endsWith(".otf"))
|
|
7
|
+
return "otf";
|
|
8
|
+
return "ttf";
|
|
9
|
+
}
|
|
10
|
+
export function selectFontSource(f, supportedFormats, preferStatic) {
|
|
11
|
+
const primarySupported = supportedFormats.has(fontFormat(f.src));
|
|
12
|
+
const satoriSupported = !!(f.satoriSrc && supportedFormats.has(fontFormat(f.satoriSrc)));
|
|
13
|
+
if (preferStatic && satoriSupported && f.satoriSrc !== f.src)
|
|
14
|
+
return { src: f.satoriSrc, isStaticFallback: true };
|
|
15
|
+
if (primarySupported)
|
|
16
|
+
return { src: f.src, isStaticFallback: false };
|
|
17
|
+
if (satoriSupported)
|
|
18
|
+
return { src: f.satoriSrc, isStaticFallback: true };
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { H3Event } from 'h3';
|
|
2
2
|
import type { OgImageRenderEventContext, RuntimeFontConfig } from '../../types.js';
|
|
3
|
+
import type { FontFormat } from './font-source.js';
|
|
3
4
|
export { buildSubsetFamilyChain, renameSubsetFonts, resolveSubsetChain } from './font-subsets.js';
|
|
4
5
|
export { codepointsIntersectRanges, extractCodepoints, parseUnicodeRange } from './unicode-range.js';
|
|
5
|
-
type FontFormat = 'ttf' | 'otf' | 'woff' | 'woff2';
|
|
6
6
|
export interface LoadFontsOptions {
|
|
7
7
|
/**
|
|
8
8
|
* Font formats the renderer can parse.
|
|
@@ -16,6 +16,13 @@ export interface LoadFontsOptions {
|
|
|
16
16
|
fontFamilyOverride?: string;
|
|
17
17
|
/** Codepoints present in the template — fonts whose unicodeRange doesn't intersect are skipped */
|
|
18
18
|
codepoints?: Set<number>;
|
|
19
|
+
/**
|
|
20
|
+
* Prefer the static satoriSrc (full TTF/WOFF from fontless) over the primary src
|
|
21
|
+
* (subset WOFF2 from @nuxt/fonts) when both are usable. Takumi uses this so non-Latin
|
|
22
|
+
* glyphs (devanagari, CJK, etc.) render — @nuxt/fonts CSS often only ships latin
|
|
23
|
+
* subsets for a family, but the fontless static file contains the full glyph set.
|
|
24
|
+
*/
|
|
25
|
+
preferStatic?: boolean;
|
|
19
26
|
}
|
|
20
27
|
export declare function loadAllFontsDebug(component?: string): {
|
|
21
28
|
component: string | undefined;
|
|
@@ -4,19 +4,11 @@ import resolvedFonts from "#og-image/fonts";
|
|
|
4
4
|
import availableFonts from "#og-image/fonts-available";
|
|
5
5
|
import { logger } from "../../logger.js";
|
|
6
6
|
import { fontArrayCache, fontCache } from "./cache/lru.js";
|
|
7
|
+
import { fontFormat, selectFontSource } from "./font-source.js";
|
|
7
8
|
import { renameSubsetFonts } from "./font-subsets.js";
|
|
8
9
|
import { codepointsIntersectRanges, parseUnicodeRange } from "./unicode-range.js";
|
|
9
10
|
export { buildSubsetFamilyChain, renameSubsetFonts, resolveSubsetChain } from "./font-subsets.js";
|
|
10
11
|
export { codepointsIntersectRanges, extractCodepoints, parseUnicodeRange } from "./unicode-range.js";
|
|
11
|
-
function fontFormat(src) {
|
|
12
|
-
if (src.endsWith(".woff2"))
|
|
13
|
-
return "woff2";
|
|
14
|
-
if (src.endsWith(".woff"))
|
|
15
|
-
return "woff";
|
|
16
|
-
if (src.endsWith(".otf"))
|
|
17
|
-
return "otf";
|
|
18
|
-
return "ttf";
|
|
19
|
-
}
|
|
20
12
|
async function loadFont(event, font, src) {
|
|
21
13
|
const cacheKey = `${font.family}-${font.weight}-${font.style}-${src}`;
|
|
22
14
|
const cached = fontCache.get(cacheKey);
|
|
@@ -103,39 +95,37 @@ export async function loadAllFonts(event, options) {
|
|
|
103
95
|
}
|
|
104
96
|
}
|
|
105
97
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const ranges = parseUnicodeRange(f.unicodeRange);
|
|
112
|
-
if (!ranges)
|
|
113
|
-
continue;
|
|
114
|
-
if (!codepointsIntersectRanges(options.codepoints, ranges))
|
|
115
|
-
fonts.splice(i, 1);
|
|
116
|
-
}
|
|
98
|
+
const resolved = [];
|
|
99
|
+
for (const f of fonts) {
|
|
100
|
+
const selection = selectFontSource(f, options.supportedFormats, options.preferStatic ?? false);
|
|
101
|
+
if (selection)
|
|
102
|
+
resolved.push({ font: f, ...selection });
|
|
117
103
|
}
|
|
104
|
+
const filtered = options.codepoints && options.codepoints.size > 0 ? resolved.filter(({ font: f, isStaticFallback }) => {
|
|
105
|
+
if (isStaticFallback || !f.unicodeRange)
|
|
106
|
+
return true;
|
|
107
|
+
const ranges = parseUnicodeRange(f.unicodeRange);
|
|
108
|
+
if (!ranges)
|
|
109
|
+
return true;
|
|
110
|
+
return codepointsIntersectRanges(options.codepoints, ranges);
|
|
111
|
+
}) : resolved;
|
|
118
112
|
const results = await Promise.all(
|
|
119
|
-
|
|
120
|
-
let src =
|
|
121
|
-
const srcFormat = fontFormat(f.src);
|
|
122
|
-
if (!options.supportedFormats.has(srcFormat)) {
|
|
123
|
-
if (f.satoriSrc && options.supportedFormats.has(fontFormat(f.satoriSrc))) {
|
|
124
|
-
src = f.satoriSrc;
|
|
125
|
-
} else {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
113
|
+
filtered.map(async ({ font: f, src: initialSrc, isStaticFallback }) => {
|
|
114
|
+
let src = initialSrc;
|
|
129
115
|
let data = await loadFont(event, f, src);
|
|
130
|
-
if (!data && src !== f.src && options.supportedFormats.has(
|
|
116
|
+
if (!data && src !== f.src && options.supportedFormats.has(fontFormat(f.src))) {
|
|
131
117
|
data = await loadFont(event, f, f.src);
|
|
132
|
-
if (data)
|
|
118
|
+
if (data) {
|
|
133
119
|
src = f.src;
|
|
120
|
+
isStaticFallback = false;
|
|
121
|
+
}
|
|
134
122
|
}
|
|
135
123
|
if (!data)
|
|
136
124
|
return null;
|
|
137
125
|
return {
|
|
138
126
|
...f,
|
|
127
|
+
src,
|
|
128
|
+
...isStaticFallback ? { unicodeRange: void 0 } : {},
|
|
139
129
|
cacheKey: `${f.family}-${f.weight}-${f.style}-${src}`,
|
|
140
130
|
data
|
|
141
131
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getEmojiSvg } from "#og-image/emoji-transform";
|
|
2
1
|
import { parse } from "ultrahtml";
|
|
3
2
|
import { querySelector } from "ultrahtml/selector";
|
|
3
|
+
import { getEmojiSvg } from "#og-image/emoji-transform";
|
|
4
4
|
import { defineTransformer } from "../../core/plugins.js";
|
|
5
5
|
import { RE_MATCH_EMOJIS } from "../../core/transforms/emojis/emoji-utils.js";
|
|
6
6
|
import { elementToVNode } from "../../core/vnodes.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { defu } from "defu";
|
|
1
2
|
import { tw4FontVars } from "#og-image-virtual/tw4-theme.mjs";
|
|
2
3
|
import compatibility from "#og-image/compatibility";
|
|
3
|
-
import {
|
|
4
|
+
import { withTimeout } from "../../util/withTimeout.js";
|
|
4
5
|
import { useOgImageRuntimeConfig } from "../../utils.js";
|
|
5
6
|
import { buildSubsetFamilyChain, extractCodepoints, getDefaultFontFamily, loadAllFontsDebug, loadFontsForRenderer, resolveSubsetChain } from "../fonts.js";
|
|
6
7
|
import { getResvg, getSatori, getSharp } from "./instances.js";
|
|
@@ -27,7 +28,7 @@ export async function createSvg(event) {
|
|
|
27
28
|
const { satoriOptions: _satoriOptions } = useOgImageRuntimeConfig();
|
|
28
29
|
const { fontFamilyOverride, defaultFont } = getDefaultFontFamily(options);
|
|
29
30
|
const [satori, vnodes] = await Promise.all([
|
|
30
|
-
getSatori(),
|
|
31
|
+
timings.measure("satori-init", () => getSatori()),
|
|
31
32
|
createVNodes(event)
|
|
32
33
|
]);
|
|
33
34
|
const codepoints = extractCodepoints(vnodes);
|
|
@@ -39,7 +40,12 @@ export async function createSvg(event) {
|
|
|
39
40
|
codepoints,
|
|
40
41
|
fontDefs: options.fonts
|
|
41
42
|
}));
|
|
42
|
-
|
|
43
|
+
const hookTimeout = event.runtimeConfig.security?.renderTimeout ?? 15e3;
|
|
44
|
+
await withTimeout(
|
|
45
|
+
event._nitro.hooks.callHook("nuxt-og-image:satori:vnodes", vnodes, event),
|
|
46
|
+
hookTimeout,
|
|
47
|
+
"nuxt-og-image:satori:vnodes hook"
|
|
48
|
+
);
|
|
43
49
|
const satoriFonts = !hasCustomFonts && _satoriFontCache.get(fonts) || fonts.map((f) => ({ ...f, name: f.family }));
|
|
44
50
|
if (!hasCustomFonts)
|
|
45
51
|
_satoriFontCache.set(fonts, satoriFonts);
|
|
@@ -10,7 +10,7 @@ const RE_LT = /</g;
|
|
|
10
10
|
const RE_GT = />/g;
|
|
11
11
|
export async function createTakumiNodes(ctx) {
|
|
12
12
|
const vnodeTree = await createVNodes(ctx);
|
|
13
|
-
return await vnodeToTakumiNode(vnodeTree, DEFAULT_FONT_SIZE);
|
|
13
|
+
return await ctx.timings.measure("takumi-nodes", () => vnodeToTakumiNode(vnodeTree, DEFAULT_FONT_SIZE));
|
|
14
14
|
}
|
|
15
15
|
function pickNumericDimension(props, key) {
|
|
16
16
|
const v = props[key];
|