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.
Files changed (94) 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/Axr0GNgb.js +23 -0
  8. package/dist/devtools/_nuxt/BFnUFv7a.js +6 -0
  9. package/dist/devtools/_nuxt/BLdiDWsd.js +3 -0
  10. package/dist/devtools/_nuxt/BPuj2Ioi.js +1 -0
  11. package/dist/devtools/_nuxt/BhUO5EEy.js +4 -0
  12. package/dist/devtools/_nuxt/{C2qZkcnS.js → Bu77Nt5h.js} +1 -1
  13. package/dist/devtools/_nuxt/CbxDj8dt.js +2 -0
  14. package/dist/devtools/_nuxt/CeQOmnUG.js +1 -0
  15. package/dist/devtools/_nuxt/Ceo32Wod.js +1 -0
  16. package/dist/devtools/_nuxt/{B75jVnAo.js → D-SbM8XJ.js} +1 -1
  17. package/dist/devtools/_nuxt/DWbsyffp.js +152 -0
  18. package/dist/devtools/_nuxt/Dd8dEdTA.js +3 -0
  19. package/dist/devtools/_nuxt/DevtoolsSection.BsOFzz12.css +1 -0
  20. package/dist/devtools/_nuxt/DevtoolsSnippet.DTkjQmsJ.css +1 -0
  21. package/dist/devtools/_nuxt/{IFrameLoader.HTWy4e5S.css → IFrameLoader.Bd09J70F.css} +1 -1
  22. package/dist/devtools/_nuxt/builds/latest.json +1 -1
  23. package/dist/devtools/_nuxt/builds/meta/df7e4f86-26df-4aa8-839c-9d7fb31dd16b.json +1 -0
  24. package/dist/devtools/_nuxt/entry.D7nBa7-d.css +2 -0
  25. package/dist/devtools/_nuxt/{D5adIWRn.js → oq4DzcWM.js} +1 -1
  26. package/dist/devtools/_nuxt/{pages.DNfeVIDd.css → pages.D6uci9iO.css} +1 -1
  27. package/dist/devtools/_nuxt/renderer-select.Pd5oWk7V.css +1 -0
  28. package/dist/devtools/_nuxt/{templates.C7e57cTp.css → templates.BHO7w0UG.css} +1 -1
  29. package/dist/devtools/debug/index.html +1 -1
  30. package/dist/devtools/docs/index.html +1 -1
  31. package/dist/devtools/index.html +1 -1
  32. package/dist/devtools/templates/index.html +1 -1
  33. package/dist/module.cjs +1 -1
  34. package/dist/module.json +1 -1
  35. package/dist/module.mjs +1 -1
  36. package/dist/runtime/app/client-utils.js +1 -1
  37. package/dist/runtime/app/components/Templates/Community/SimpleBlog.satori.vue +1 -1
  38. package/dist/runtime/app/utils/plugins.js +4 -4
  39. package/dist/runtime/app/utils.js +1 -1
  40. package/dist/runtime/server/og-image/bindings/browser/chrome-launcher.js +2 -1
  41. package/dist/runtime/server/og-image/bindings/browser/cloudflare.js +4 -2
  42. package/dist/runtime/server/og-image/bindings/browser/on-demand.js +2 -1
  43. package/dist/runtime/server/og-image/bindings/browser/playwright.js +2 -1
  44. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +1 -1
  45. package/dist/runtime/server/og-image/bindings/font-assets/node.js +1 -1
  46. package/dist/runtime/server/og-image/browser/renderer.js +1 -1
  47. package/dist/runtime/server/og-image/browser/screenshot.js +2 -2
  48. package/dist/runtime/server/og-image/cache/buildCache.js +1 -1
  49. package/dist/runtime/server/og-image/context.js +12 -5
  50. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +30 -59
  51. package/dist/runtime/server/og-image/core/transforms/emojis/fetch.js +1 -1
  52. package/dist/runtime/server/og-image/core/vnodes.js +3 -2
  53. package/dist/runtime/server/og-image/devtools.js +1 -1
  54. package/dist/runtime/server/og-image/font-source.d.ts +20 -0
  55. package/dist/runtime/server/og-image/font-source.js +20 -0
  56. package/dist/runtime/server/og-image/fonts.d.ts +8 -1
  57. package/dist/runtime/server/og-image/fonts.js +22 -32
  58. package/dist/runtime/server/og-image/satori/plugins/emojis.js +1 -1
  59. package/dist/runtime/server/og-image/satori/renderer.js +9 -3
  60. package/dist/runtime/server/og-image/takumi/nodes.js +1 -1
  61. package/dist/runtime/server/og-image/takumi/renderer.js +76 -24
  62. package/dist/runtime/server/og-image/templates/html.js +3 -2
  63. package/dist/runtime/server/plugins/prerender.js +2 -2
  64. package/dist/runtime/server/routes/debug.json.js +2 -2
  65. package/dist/runtime/server/routes/resolve.js +2 -2
  66. package/dist/runtime/server/util/eventHandlers.js +3 -2
  67. package/dist/runtime/server/util/fetchLocalAsset.js +1 -1
  68. package/dist/runtime/server/util/kit.d.ts +1 -1
  69. package/dist/runtime/server/util/kit.js +5 -2
  70. package/dist/runtime/server/util/options.js +1 -1
  71. package/dist/runtime/server/util/ssrf.d.ts +27 -0
  72. package/dist/runtime/server/util/ssrf.js +165 -0
  73. package/dist/runtime/server/util/withTimeout.d.ts +1 -0
  74. package/dist/runtime/server/util/withTimeout.js +12 -0
  75. package/dist/runtime/server/utils.js +6 -2
  76. package/dist/shared/{nuxt-og-image.Cr3WHMk1.mjs → nuxt-og-image.CW3PwVsO.mjs} +7 -2
  77. package/dist/shared/{nuxt-og-image.TJuh6pW5.cjs → nuxt-og-image.DnwTVrnM.cjs} +8 -3
  78. package/package.json +18 -17
  79. package/types/virtual.d.ts +1 -1
  80. package/dist/devtools/_nuxt/671cdwR5.js +0 -1
  81. package/dist/devtools/_nuxt/8kkJjsM6.js +0 -2
  82. package/dist/devtools/_nuxt/BJ9iyiD_.js +0 -3
  83. package/dist/devtools/_nuxt/BcGh1UWn.js +0 -23
  84. package/dist/devtools/_nuxt/CHmOAOu6.js +0 -3
  85. package/dist/devtools/_nuxt/Ci8ZvNRB.js +0 -1
  86. package/dist/devtools/_nuxt/DVtbGlya.js +0 -152
  87. package/dist/devtools/_nuxt/DbOyl855.js +0 -1
  88. package/dist/devtools/_nuxt/DevtoolsSection.DTZAmexP.css +0 -1
  89. package/dist/devtools/_nuxt/DevtoolsSnippet.Cd7XR-3f.css +0 -1
  90. package/dist/devtools/_nuxt/DrqfEi04.js +0 -4
  91. package/dist/devtools/_nuxt/builds/meta/d8fa30f4-68dd-4f57-a1c2-1c3a852f7ee7.json +0 -1
  92. package/dist/devtools/_nuxt/cIRSrPLM.js +0 -6
  93. package/dist/devtools/_nuxt/entry.zgUFdiSv.css +0 -2
  94. 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": async (ctx2) => {
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
- await nuxtApp.runWithContext(() => {
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)
@@ -4,6 +4,7 @@ const chromePath = Launcher.getFirstInstallation();
4
4
  export async function createBrowser(_event) {
5
5
  return playwrightCore.chromium.launch({
6
6
  headless: true,
7
- executablePath: chromePath
7
+ executablePath: chromePath,
8
+ timeout: 15e3
8
9
  });
9
10
  }
@@ -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
- browserPromise = (async () => {
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;
@@ -27,6 +27,7 @@ export async function createBrowser(_event) {
27
27
  })();
28
28
  }
29
29
  return await playwrightCore.chromium.launch({
30
- headless: true
30
+ headless: true,
31
+ timeout: 15e3
31
32
  });
32
33
  }
@@ -1,6 +1,7 @@
1
1
  import playwright from "playwright";
2
2
  export async function createBrowser(_event) {
3
3
  return await playwright.chromium.launch({
4
- headless: true
4
+ headless: true,
5
+ timeout: 15e3
5
6
  });
6
7
  }
@@ -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,5 +1,5 @@
1
- import { createBrowser } from "#og-image/bindings/browser";
2
1
  import { createError } from "h3";
2
+ import { createBrowser } from "#og-image/bindings/browser";
3
3
  import { createScreenshot } from "./screenshot.js";
4
4
  const BrowserRenderer = {
5
5
  name: "browser",
@@ -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
- await ctx._nitro.hooks.callHook("nuxt-og-image:context", ctx);
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
- const buffer = await $fetch(decodedSrc, {
124
- responseType: "arrayBuffer",
125
- timeout: fetchTimeout
126
- }).catch((err) => {
127
- logFailure(decodedSrc, err);
128
- }).finally(end);
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 island = await fetchIsland(ctx.e, ctx.options.component, typeof ctx.options.props !== "undefined" ? ctx.options.props : ctx.options);
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
- if (options.codepoints && options.codepoints.size > 0) {
107
- for (let i = fonts.length - 1; i >= 0; i--) {
108
- const f = fonts[i];
109
- if (!f.unicodeRange)
110
- continue;
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
- fonts.map(async (f) => {
120
- let src = f.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(srcFormat)) {
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 { defu } from "defu";
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
- await event._nitro.hooks.callHook("nuxt-og-image:satori:vnodes", vnodes, event);
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];