nuxt-og-image 6.7.8 → 6.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/tw4.mjs +2 -2
- package/dist/chunks/uno.mjs +4 -4
- package/dist/cli.mjs +1 -1
- package/dist/devtools/nuxt.config.ts +4 -1
- package/dist/module.d.mts +10 -0
- package/dist/module.d.ts +10 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +3 -3
- package/dist/runtime/app/client-utils.js +4 -3
- package/dist/runtime/app/utils.js +3 -2
- package/dist/runtime/server/nitro.d.ts +9 -0
- package/dist/runtime/server/nitro.js +10 -0
- package/dist/runtime/server/og-image/bindings/fetch/node.d.ts +2 -0
- package/dist/runtime/server/og-image/bindings/fetch/node.js +42 -0
- package/dist/runtime/server/og-image/bindings/fetch/web.d.ts +7 -0
- package/dist/runtime/server/og-image/bindings/fetch/web.js +3 -0
- package/dist/runtime/server/og-image/bindings/font-assets/external-url.d.ts +0 -11
- package/dist/runtime/server/og-image/bindings/font-assets/external-url.js +2 -19
- package/dist/runtime/server/og-image/bindings/satori/harfbuzz.d.ts +4 -0
- package/dist/runtime/server/og-image/bindings/satori/harfbuzz.js +33 -0
- package/dist/runtime/server/og-image/browser/screenshot.js +4 -1
- package/dist/runtime/server/og-image/context.js +9 -1
- package/dist/runtime/server/og-image/font-subsets.d.ts +3 -11
- package/dist/runtime/server/og-image/font-subsets.js +50 -27
- package/dist/runtime/server/og-image/fonts.d.ts +1 -1
- package/dist/runtime/server/routes/resolve.js +2 -2
- package/dist/runtime/server/util/cloudflare.js +2 -2
- package/dist/runtime/server/util/eventHandlers.js +3 -3
- package/dist/runtime/server/util/ssrf.d.ts +31 -0
- package/dist/runtime/server/util/ssrf.js +64 -5
- package/dist/runtime/server/utils.d.ts +1 -1
- package/dist/runtime/server/utils.js +3 -3
- package/dist/runtime/shared.d.ts +10 -1
- package/dist/runtime/shared.js +2 -2
- package/dist/runtime/types.d.ts +5 -1
- package/dist/shared/{nuxt-og-image.BMHzg1Vz.mjs → nuxt-og-image.CTD0ZPNd.mjs} +634 -468
- package/package.json +70 -66
- package/types/virtual.d.ts +26 -0
package/dist/chunks/tw4.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
import { resolveModulePath } from 'exsolve';
|
|
4
4
|
import { dirname, join } from 'pathe';
|
|
5
|
-
import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.
|
|
5
|
+
import { e as extractVariantBaseClasses, r as resolveVariantPrefixes, h as resolveExtractedVars, l as resolveVarsDeep, s as simplifyCss, p as postProcessStyles, m as loadLightningCss, a as extractCssVars, c as extractUniversalVars, d as extractPerClassVars, g as extractVarsFromCss, f as extractClassStyles } from '../shared/nuxt-og-image.CTD0ZPNd.mjs';
|
|
6
6
|
import 'node:crypto';
|
|
7
7
|
import 'node:fs';
|
|
8
8
|
import '@nuxt/kit';
|
|
@@ -12,6 +12,7 @@ import 'nuxt-site-config/kit';
|
|
|
12
12
|
import 'nuxtseo-shared/kit';
|
|
13
13
|
import 'pkg-types';
|
|
14
14
|
import 'std-env';
|
|
15
|
+
import 'magic-string';
|
|
15
16
|
import 'oxc-walker';
|
|
16
17
|
import '../../dist/runtime/logger.js';
|
|
17
18
|
import 'chrome-launcher';
|
|
@@ -20,7 +21,6 @@ import 'nuxtseo-shared/devtools';
|
|
|
20
21
|
import 'unstorage';
|
|
21
22
|
import 'unstorage/drivers/fs-lite';
|
|
22
23
|
import '../../dist/runtime/server/og-image/utils/css.js';
|
|
23
|
-
import 'magic-string';
|
|
24
24
|
import 'strip-literal';
|
|
25
25
|
import 'ufo';
|
|
26
26
|
import 'unplugin';
|
package/dist/chunks/uno.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { defu } from 'defu';
|
|
3
3
|
import { logger } from '../../dist/runtime/logger.js';
|
|
4
|
-
import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.
|
|
5
|
-
export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.
|
|
4
|
+
import { e as extractVariantBaseClasses, s as simplifyCss, a as extractCssVars, b as extractPropertyInitialValues, c as extractUniversalVars, d as extractPerClassVars, f as extractClassStyles, p as postProcessStyles, r as resolveVariantPrefixes, i as importOptionalPeer, g as extractVarsFromCss, h as resolveExtractedVars } from '../shared/nuxt-og-image.CTD0ZPNd.mjs';
|
|
5
|
+
export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.CTD0ZPNd.mjs';
|
|
6
6
|
import 'node:crypto';
|
|
7
7
|
import 'node:fs';
|
|
8
8
|
import 'node:url';
|
|
@@ -13,14 +13,15 @@ import 'nuxtseo-shared/kit';
|
|
|
13
13
|
import 'pathe';
|
|
14
14
|
import 'pkg-types';
|
|
15
15
|
import 'std-env';
|
|
16
|
+
import 'magic-string';
|
|
16
17
|
import 'oxc-walker';
|
|
18
|
+
import 'exsolve';
|
|
17
19
|
import 'chrome-launcher';
|
|
18
20
|
import 'nypm';
|
|
19
21
|
import 'nuxtseo-shared/devtools';
|
|
20
22
|
import 'unstorage';
|
|
21
23
|
import 'unstorage/drivers/fs-lite';
|
|
22
24
|
import '../../dist/runtime/server/og-image/utils/css.js';
|
|
23
|
-
import 'magic-string';
|
|
24
25
|
import 'strip-literal';
|
|
25
26
|
import 'ufo';
|
|
26
27
|
import 'unplugin';
|
|
@@ -28,7 +29,6 @@ import '@vue/compiler-sfc';
|
|
|
28
29
|
import 'ofetch';
|
|
29
30
|
import 'ultrahtml';
|
|
30
31
|
import '../../dist/runtime/server/og-image/core/transforms/emojis/emoji-utils.js';
|
|
31
|
-
import 'exsolve';
|
|
32
32
|
import '../shared/nuxt-og-image.CJa2KCie.mjs';
|
|
33
33
|
import 'magicast';
|
|
34
34
|
import 'magicast/helpers';
|
package/dist/cli.mjs
CHANGED
|
@@ -1211,7 +1211,7 @@ async function runCreate(name, args2) {
|
|
|
1211
1211
|
message: "Add defineOgImage to a page? (relative path from pages/, leave empty to skip)",
|
|
1212
1212
|
placeholder: "index.vue"
|
|
1213
1213
|
});
|
|
1214
|
-
if (
|
|
1214
|
+
if (typeof pageInput === "string" && pageInput.trim()) {
|
|
1215
1215
|
let pagePath = pageInput.trim();
|
|
1216
1216
|
if (!pagePath.endsWith(".vue"))
|
|
1217
1217
|
pagePath += ".vue";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
1
2
|
import { resolve } from 'pathe'
|
|
2
3
|
|
|
4
|
+
const currentDir = fileURLToPath(new URL('.', import.meta.url))
|
|
5
|
+
|
|
3
6
|
// Nuxt SEO devtools panel, shipped as a layer (Model C). Components flat-registered.
|
|
4
7
|
export default defineNuxtConfig({
|
|
5
|
-
components: [{ path: resolve(
|
|
8
|
+
components: [{ path: resolve(currentDir, './components'), pathPrefix: false }],
|
|
6
9
|
})
|
package/dist/module.d.mts
CHANGED
|
@@ -118,6 +118,16 @@ interface ModuleOptions {
|
|
|
118
118
|
* @default 60 * 60 * 24 * 3 (3 days)
|
|
119
119
|
*/
|
|
120
120
|
cacheMaxAgeSeconds?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Include Twitter card meta tags (`twitter:card`, `twitter:image`, etc)
|
|
123
|
+
* alongside the Open Graph image tags.
|
|
124
|
+
*
|
|
125
|
+
* Set to `false` to only emit `og:image` tags. When unset, the module respects
|
|
126
|
+
* `seo.automaticTwitterTags` (nuxt-seo-kit) before defaulting to `true`.
|
|
127
|
+
*
|
|
128
|
+
* @default true
|
|
129
|
+
*/
|
|
130
|
+
includeTwitter?: boolean;
|
|
121
131
|
/**
|
|
122
132
|
* Font subsets to download when resolving missing font families via fontless.
|
|
123
133
|
*
|
package/dist/module.d.ts
CHANGED
|
@@ -118,6 +118,16 @@ interface ModuleOptions {
|
|
|
118
118
|
* @default 60 * 60 * 24 * 3 (3 days)
|
|
119
119
|
*/
|
|
120
120
|
cacheMaxAgeSeconds?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Include Twitter card meta tags (`twitter:card`, `twitter:image`, etc)
|
|
123
|
+
* alongside the Open Graph image tags.
|
|
124
|
+
*
|
|
125
|
+
* Set to `false` to only emit `og:image` tags. When unset, the module respects
|
|
126
|
+
* `seo.automaticTwitterTags` (nuxt-seo-kit) before defaulting to `true`.
|
|
127
|
+
*
|
|
128
|
+
* @default true
|
|
129
|
+
*/
|
|
130
|
+
includeTwitter?: boolean;
|
|
121
131
|
/**
|
|
122
132
|
* Font subsets to download when resolving missing font families via fontless.
|
|
123
133
|
*
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -10,16 +10,17 @@ import 'nuxtseo-shared/kit';
|
|
|
10
10
|
import 'pathe';
|
|
11
11
|
import 'pkg-types';
|
|
12
12
|
import 'std-env';
|
|
13
|
-
export { n as default } from './shared/nuxt-og-image.
|
|
13
|
+
export { n as default } from './shared/nuxt-og-image.CTD0ZPNd.mjs';
|
|
14
14
|
import '../dist/runtime/logger.js';
|
|
15
|
+
import 'magic-string';
|
|
15
16
|
import 'oxc-walker';
|
|
17
|
+
import 'exsolve';
|
|
16
18
|
import 'chrome-launcher';
|
|
17
19
|
import 'nypm';
|
|
18
20
|
import 'nuxtseo-shared/devtools';
|
|
19
21
|
import 'unstorage';
|
|
20
22
|
import 'unstorage/drivers/fs-lite';
|
|
21
23
|
import '../dist/runtime/server/og-image/utils/css.js';
|
|
22
|
-
import 'magic-string';
|
|
23
24
|
import 'strip-literal';
|
|
24
25
|
import 'ufo';
|
|
25
26
|
import 'unplugin';
|
|
@@ -27,7 +28,6 @@ import '@vue/compiler-sfc';
|
|
|
27
28
|
import 'ofetch';
|
|
28
29
|
import 'ultrahtml';
|
|
29
30
|
import '../dist/runtime/server/og-image/core/transforms/emojis/emoji-utils.js';
|
|
30
|
-
import 'exsolve';
|
|
31
31
|
import './shared/nuxt-og-image.CJa2KCie.mjs';
|
|
32
32
|
import 'magicast';
|
|
33
33
|
import 'magicast/helpers';
|
|
@@ -63,6 +63,7 @@ export function clientProcessOgImageOptions(input, route, basePath) {
|
|
|
63
63
|
const baseURL = rc.app.baseURL;
|
|
64
64
|
const publicCfg = rc.public?.["nuxt-og-image"] || {};
|
|
65
65
|
const defaults = publicCfg.defaults || {};
|
|
66
|
+
const metaOptions = { includeTwitter: publicCfg.includeTwitter };
|
|
66
67
|
const paths = [];
|
|
67
68
|
for (const rawInput of inputs) {
|
|
68
69
|
const resolved = resolveReactiveOptions(rawInput);
|
|
@@ -78,13 +79,13 @@ export function clientProcessOgImageOptions(input, route, basePath) {
|
|
|
78
79
|
const ogKey = validOptions.key || "og";
|
|
79
80
|
if (validOptions.url) {
|
|
80
81
|
const url = validOptions.url;
|
|
81
|
-
registerClientOgHead(ogKey, { meta: generateMeta(url, validOptions) }, { tagPriority: "high" });
|
|
82
|
+
registerClientOgHead(ogKey, { meta: generateMeta(url, validOptions, metaOptions) }, { tagPriority: "high" });
|
|
82
83
|
paths.push(url);
|
|
83
84
|
continue;
|
|
84
85
|
}
|
|
85
86
|
if (publicCfg.hasServerRuntime) {
|
|
86
87
|
const finalUrl2 = buildResolverUrl(baseURL, basePath, ogKey, route.query);
|
|
87
|
-
registerClientOgHead(ogKey, { meta: generateMeta(finalUrl2, validOptions) }, { tagPriority: 35 });
|
|
88
|
+
registerClientOgHead(ogKey, { meta: generateMeta(finalUrl2, validOptions, metaOptions) }, { tagPriority: 35 });
|
|
88
89
|
paths.push(finalUrl2);
|
|
89
90
|
continue;
|
|
90
91
|
}
|
|
@@ -98,7 +99,7 @@ export function clientProcessOgImageOptions(input, route, basePath) {
|
|
|
98
99
|
const result = buildOgImageUrl(urlOpts, extension, true, defaults, void 0);
|
|
99
100
|
const resolvedUrl = joinURL("/", baseURL, result.url);
|
|
100
101
|
const finalUrl = opts._query && Object.keys(opts._query).length ? withQuery(resolvedUrl, { _query: opts._query }) : resolvedUrl;
|
|
101
|
-
registerClientOgHead(ogKey, { meta: generateMeta(finalUrl, opts) }, { processTemplateParams: true, tagPriority: 35 });
|
|
102
|
+
registerClientOgHead(ogKey, { meta: generateMeta(finalUrl, opts, metaOptions) }, { processTemplateParams: true, tagPriority: 35 });
|
|
102
103
|
paths.push(finalUrl);
|
|
103
104
|
}
|
|
104
105
|
return paths;
|
|
@@ -53,7 +53,8 @@ export function setHeadOgImagePrebuilt(input) {
|
|
|
53
53
|
const url = input.url;
|
|
54
54
|
if (!url)
|
|
55
55
|
return;
|
|
56
|
-
const
|
|
56
|
+
const { includeTwitter } = useOgImageRuntimeConfig();
|
|
57
|
+
const meta = generateMeta(url, input, { includeTwitter });
|
|
57
58
|
useHead({ meta }, { tagPriority: "high" });
|
|
58
59
|
}
|
|
59
60
|
export function createOgImageMeta(src, input, ssrContext, pagePath, head) {
|
|
@@ -117,7 +118,7 @@ export function createOgImageMeta(src, input, ssrContext, pagePath, head) {
|
|
|
117
118
|
prerenderPaths.set(ogKey, (finalUrl.split("?")[0] || finalUrl).replace(/,/g, "%2C"));
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
|
-
return generateMeta(finalUrl, opts);
|
|
121
|
+
return generateMeta(finalUrl, opts, { includeTwitter: ogImageConfig.includeTwitter });
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
124
|
}, {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { H3Event } from '#nuxtseo/h3';
|
|
2
|
+
import type { OgImageOptionsInternal } from '../types.js';
|
|
3
|
+
type Options = Partial<OgImageOptionsInternal>;
|
|
4
|
+
/**
|
|
5
|
+
* Build the absolute OG image URL for a page. Without a page path, uses the current request path.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getOgImageUrl(event: H3Event, options?: Options): string;
|
|
8
|
+
export declare function getOgImageUrl(event: H3Event, pagePath: string, options?: Options): string;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parseURL, withoutBase } from "ufo";
|
|
2
|
+
import { useRuntimeConfig } from "#nuxtseo/nitro";
|
|
3
|
+
import { withSiteUrl } from "#site-config/server/composables/utils";
|
|
4
|
+
import { getOgImagePath } from "./utils.js";
|
|
5
|
+
export function getOgImageUrl(event, pathOrOptions, maybeOptions) {
|
|
6
|
+
const pagePath = typeof pathOrOptions === "string" ? pathOrOptions : withoutBase(parseURL(event.path).pathname, useRuntimeConfig(event).app.baseURL) || "/";
|
|
7
|
+
const options = typeof pathOrOptions === "string" ? maybeOptions : pathOrOptions;
|
|
8
|
+
const { path } = getOgImagePath(event, pagePath, options);
|
|
9
|
+
return withSiteUrl(event, path, { canonical: !import.meta.dev });
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { lookup as dnsLookup } from "node:dns";
|
|
2
|
+
import { request as httpRequest } from "node:http";
|
|
3
|
+
import { request as httpsRequest } from "node:https";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
function guardedLookup(isBlocked) {
|
|
6
|
+
return (hostname, options, callback) => {
|
|
7
|
+
dnsLookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
8
|
+
if (err)
|
|
9
|
+
return callback(err, "", 0);
|
|
10
|
+
const blocked = addresses.find((a) => isBlocked(a.address));
|
|
11
|
+
if (blocked)
|
|
12
|
+
return callback(new Error(`[Nuxt OG Image] ${hostname} resolves to blocked address ${blocked.address}.`), "", 0);
|
|
13
|
+
if (options.all)
|
|
14
|
+
return callback(null, addresses);
|
|
15
|
+
callback(null, addresses[0].address, addresses[0].family);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function guardedFetch(url, { headers, signal, isBlockedAddress }) {
|
|
20
|
+
const request = url.startsWith("https:") ? httpsRequest : httpRequest;
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const req = request(url, {
|
|
23
|
+
// Match the defaults that `fetch` sent before this transport existed.
|
|
24
|
+
headers: { "accept": "*/*", "user-agent": "node", ...headers },
|
|
25
|
+
signal,
|
|
26
|
+
lookup: guardedLookup(isBlockedAddress ?? (() => false)),
|
|
27
|
+
// A fresh socket per hop: never reuse a connection nobody checked.
|
|
28
|
+
agent: false
|
|
29
|
+
}, (res) => {
|
|
30
|
+
const status = res.statusCode ?? 0;
|
|
31
|
+
const responseHeaders = new Headers();
|
|
32
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
33
|
+
for (const v of [value ?? []].flat())
|
|
34
|
+
responseHeaders.append(name, v);
|
|
35
|
+
}
|
|
36
|
+
const body = status === 204 || status === 304 ? null : Readable.toWeb(res);
|
|
37
|
+
resolve(new Response(body, { status, headers: responseHeaders }));
|
|
38
|
+
});
|
|
39
|
+
req.on("error", reject);
|
|
40
|
+
req.end();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface GuardedFetchInit {
|
|
2
|
+
headers?: Record<string, string>;
|
|
3
|
+
signal: AbortSignal;
|
|
4
|
+
/** Rejects the connection when a resolved address matches. Node only. */
|
|
5
|
+
isBlockedAddress?: (address: string) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function guardedFetch(url: string, { headers, signal }: GuardedFetchInit): Promise<Response>;
|
|
@@ -12,17 +12,6 @@ export declare function isDataFontUrl(path: string): boolean;
|
|
|
12
12
|
* Check `isDataFontUrl` first — `data:` is non-http and would report true here.
|
|
13
13
|
*/
|
|
14
14
|
export declare function isExternalFontUrl(path: string): boolean;
|
|
15
|
-
/**
|
|
16
|
-
* Resolve an authority-bearing font URL to the href to fetch, but only when it
|
|
17
|
-
* is same-origin with the configured site URL. Returns null otherwise.
|
|
18
|
-
*
|
|
19
|
-
* Runtime external font URLs are unsupported — `@nuxt/fonts` is the only
|
|
20
|
-
* supported way to load custom fonts, and it serves them same-origin. The single
|
|
21
|
-
* allowed exception is the site's own origin, gated on an explicitly configured
|
|
22
|
-
* site URL. Resolving against that origin also collapses canonicalization
|
|
23
|
-
* bypasses (a crafted " //127.0.0.1" resolves cross-origin → rejected).
|
|
24
|
-
*/
|
|
25
|
-
export declare function resolveSameOriginFontUrl(path: string, siteUrl: string | undefined): string | null;
|
|
26
15
|
/**
|
|
27
16
|
* Fetch a `data:` or authority-bearing font URL. `data:` is decoded directly
|
|
28
17
|
* (no network). An external URL is fetched only when same-origin with the site
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
|
|
1
|
+
import { fetchWithRedirectValidation, resolveSameOriginUrl } from "../../../util/ssrf.js";
|
|
2
2
|
const SENTINEL_ORIGIN = "http://font-asset.invalid/";
|
|
3
3
|
const SENTINEL_HOST = "font-asset.invalid";
|
|
4
4
|
export function isDataFontUrl(path) {
|
|
@@ -13,23 +13,6 @@ export function isExternalFontUrl(path) {
|
|
|
13
13
|
}
|
|
14
14
|
return resolved.protocol !== "http:" || resolved.host !== SENTINEL_HOST;
|
|
15
15
|
}
|
|
16
|
-
export function resolveSameOriginFontUrl(path, siteUrl) {
|
|
17
|
-
if (!siteUrl)
|
|
18
|
-
return null;
|
|
19
|
-
let siteOrigin;
|
|
20
|
-
try {
|
|
21
|
-
siteOrigin = new URL(siteUrl).origin;
|
|
22
|
-
} catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
let target;
|
|
26
|
-
try {
|
|
27
|
-
target = new URL(path, siteOrigin);
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
return target.origin === siteOrigin ? target.href : null;
|
|
32
|
-
}
|
|
33
16
|
export async function fetchSpecialFontUrl(path, siteUrl, timeout) {
|
|
34
17
|
if (isDataFontUrl(path)) {
|
|
35
18
|
const res = await fetch(path.trimStart(), { signal: AbortSignal.timeout(timeout) }).catch(() => {
|
|
@@ -39,7 +22,7 @@ export async function fetchSpecialFontUrl(path, siteUrl, timeout) {
|
|
|
39
22
|
return Buffer.from(await res.arrayBuffer());
|
|
40
23
|
throw new Error("[Nuxt OG Image] Invalid data: font URL.");
|
|
41
24
|
}
|
|
42
|
-
const href =
|
|
25
|
+
const href = resolveSameOriginUrl(path, siteUrl);
|
|
43
26
|
if (!href)
|
|
44
27
|
throw new Error("[Nuxt OG Image] External font URLs are not supported. Load custom fonts via @nuxt/fonts.");
|
|
45
28
|
const ab = await fetchWithRedirectValidation(href, { timeout });
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
readonly then: <TResult1 = unknown, TResult2 = never>(onfulfilled?: ((value: unknown) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined) => Promise<TResult1 | TResult2>;
|
|
3
|
+
};
|
|
4
|
+
export default _default;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import wrapHarfBuzz from "#og-image/harfbuzz-adapter";
|
|
2
|
+
import callbackModules from "#og-image/harfbuzz-callbacks";
|
|
3
|
+
import createHarfBuzz from "#og-image/harfbuzz-factory";
|
|
4
|
+
function loadHarfBuzz() {
|
|
5
|
+
return import("#og-image/harfbuzz-wasm").then(async ({ default: importedWasm }) => {
|
|
6
|
+
const wasm = await importedWasm;
|
|
7
|
+
const callbacks = callbackModules && Object.fromEntries(await Promise.all(
|
|
8
|
+
Object.entries(callbackModules).map(async ([signature, module]) => [signature, await module])
|
|
9
|
+
));
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
createHarfBuzz({
|
|
12
|
+
...callbacks && {
|
|
13
|
+
convertJsFunctionToWasm(callback, signature) {
|
|
14
|
+
return new WebAssembly.Instance(callbacks[signature], { e: { f: callback } }).exports.f;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
instantiateWasm(imports, receiveInstance) {
|
|
18
|
+
Promise.resolve().then(() => typeof wasm === "function" ? wasm(imports) : new WebAssembly.Instance(wasm, imports)).then((result) => {
|
|
19
|
+
receiveInstance("instance" in result ? result.instance : result);
|
|
20
|
+
}).catch(reject);
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}).then(wrapHarfBuzz).then(resolve, reject);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
let runtime;
|
|
28
|
+
export default {
|
|
29
|
+
get then() {
|
|
30
|
+
const promise = runtime ||= loadHarfBuzz();
|
|
31
|
+
return promise.then.bind(promise);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -5,6 +5,7 @@ import { getNitroOrigin } from "#site-config/server/composables";
|
|
|
5
5
|
import { buildOgImageUrl } from "../../../shared.js";
|
|
6
6
|
import { getFetchTimeout } from "../../util/fetchTimeout.js";
|
|
7
7
|
import { logger } from "../../util/logger.js";
|
|
8
|
+
import { resolveSameOriginUrl } from "../../util/ssrf.js";
|
|
8
9
|
import { useOgImageRuntimeConfig } from "../../utils.js";
|
|
9
10
|
function isPlaywrightPage(page) {
|
|
10
11
|
return typeof page.setViewportSize === "function";
|
|
@@ -101,7 +102,9 @@ export async function createScreenshot({ basePath, e, options, extension, timing
|
|
|
101
102
|
await page.setContent(html, { waitUntil: "networkidle0" });
|
|
102
103
|
}
|
|
103
104
|
} else {
|
|
104
|
-
const url =
|
|
105
|
+
const url = resolveSameOriginUrl(withQuery(path, options.props || {}), getNitroOrigin(e));
|
|
106
|
+
if (!url)
|
|
107
|
+
throw new Error("[Nuxt OG Image] The browser renderer can only navigate to a same-origin path.");
|
|
105
108
|
await gotoWithIdle(page, url, 1e4);
|
|
106
109
|
}
|
|
107
110
|
const screenshotOptions = {
|
|
@@ -13,6 +13,7 @@ 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 { getEventQuery } from "../util/query.js";
|
|
16
|
+
import { parseSameOriginPath } from "../util/ssrf.js";
|
|
16
17
|
import { createTimings, TIMING_CTX_KEY } from "../util/timings.js";
|
|
17
18
|
import { withTimeout } from "../util/withTimeout.js";
|
|
18
19
|
import { useOgImageRuntimeConfig } from "../utils.js";
|
|
@@ -120,7 +121,14 @@ export async function resolveContext(e) {
|
|
|
120
121
|
delete queryParams.html;
|
|
121
122
|
queryParams = separateProps(queryParams);
|
|
122
123
|
}
|
|
123
|
-
const
|
|
124
|
+
const rawPath = urlOptions._path ?? "/";
|
|
125
|
+
const basePath = parseSameOriginPath(typeof rawPath === "string" ? withoutTrailingSlash(rawPath) : rawPath);
|
|
126
|
+
if (!basePath) {
|
|
127
|
+
return createError({
|
|
128
|
+
statusCode: 400,
|
|
129
|
+
statusMessage: "[Nuxt OG Image] The _path option must be a same-origin path."
|
|
130
|
+
});
|
|
131
|
+
}
|
|
124
132
|
const componentHash = urlOptions._componentHash || "";
|
|
125
133
|
delete urlOptions._path;
|
|
126
134
|
delete urlOptions._hash;
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import type { RuntimeFontConfig } from '../../types.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Both Satori and Takumi pick the first loaded font file for a given family
|
|
6
|
-
* name and don't fall back to other font files for missing glyphs. When CJK
|
|
7
|
-
* fonts like "Noto Sans SC" are split into 100+ unicode-range subsets by
|
|
8
|
-
* fontsource, each covering ~200 characters, this means only one subset's
|
|
9
|
-
* glyphs render while the rest show as .notdef boxes.
|
|
10
|
-
*
|
|
11
|
-
* Fix: rename each subset to "Family__N" and use font-family fallback chains
|
|
12
|
-
* so renderers try each subset in order per character.
|
|
3
|
+
* Give subsets stable names so renderers can fall back between their glyphs.
|
|
4
|
+
* Renderers cache registrations by name across renders with different filtered subsets.
|
|
13
5
|
*/
|
|
14
6
|
export declare function renameSubsetFonts(fonts: RuntimeFontConfig[]): RuntimeFontConfig[];
|
|
15
7
|
/**
|
|
16
8
|
* Build a mapping from original family names to their renamed subset chain.
|
|
17
|
-
* E.g., "Noto Sans SC" → ["Noto Sans
|
|
9
|
+
* E.g., "Noto Sans SC" → ["Noto Sans SC__<hash-a>", "Noto Sans SC__<hash-b>", ...]
|
|
18
10
|
*/
|
|
19
11
|
export declare function buildSubsetFamilyChain(fonts: RuntimeFontConfig[]): Map<string, string[]>;
|
|
20
12
|
/**
|
|
@@ -1,44 +1,67 @@
|
|
|
1
|
+
import { fnv1a64Base36 } from "fnv1a-64";
|
|
1
2
|
export function renameSubsetFonts(fonts) {
|
|
2
3
|
const groups = /* @__PURE__ */ new Map();
|
|
3
|
-
for (const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
arr.push(f);
|
|
4
|
+
for (const font of fonts) {
|
|
5
|
+
const members = groups.get(font.family);
|
|
6
|
+
if (members)
|
|
7
|
+
members.push(font);
|
|
8
8
|
else
|
|
9
|
-
groups.set(
|
|
9
|
+
groups.set(font.family, [font]);
|
|
10
10
|
}
|
|
11
|
-
const
|
|
12
|
-
let changed = false;
|
|
11
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
13
12
|
for (const members of groups.values()) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const faces = /* @__PURE__ */ new Map();
|
|
14
|
+
for (const font of members) {
|
|
15
|
+
const face = `${font.weight}\0${font.style}`;
|
|
16
|
+
const subsets = faces.get(face);
|
|
17
|
+
if (subsets)
|
|
18
|
+
subsets.push(font);
|
|
19
|
+
else
|
|
20
|
+
faces.set(face, [font]);
|
|
18
21
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const needsRename = members.some((font) => font.unicodeRange) || [...faces.values()].some((subsets) => new Set(subsets.map((font) => font.cacheKey)).size > 1);
|
|
23
|
+
if (!needsRename)
|
|
24
|
+
continue;
|
|
25
|
+
const slots = [];
|
|
26
|
+
for (const subsets of faces.values()) {
|
|
27
|
+
const binaries = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const font of subsets) {
|
|
29
|
+
const source = font.src || font.localPath || font.cacheKey;
|
|
30
|
+
const entries = binaries.get(source);
|
|
31
|
+
if (entries)
|
|
32
|
+
entries.push(font);
|
|
33
|
+
else
|
|
34
|
+
binaries.set(source, [font]);
|
|
35
|
+
}
|
|
36
|
+
const ordered = [...binaries.values()].sort((a, b) => {
|
|
37
|
+
const left = `${a[0].unicodeRange || ""}\0${a[0].src || a[0].localPath || a[0].cacheKey}`;
|
|
38
|
+
const right = `${b[0].unicodeRange || ""}\0${b[0].src || b[0].localPath || b[0].cacheKey}`;
|
|
39
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
26
40
|
});
|
|
41
|
+
for (const [index, entries] of ordered.entries())
|
|
42
|
+
(slots[index] ||= []).push(...entries);
|
|
43
|
+
}
|
|
44
|
+
for (const slot of slots) {
|
|
45
|
+
const identities = [...new Set(slot.map((font) => JSON.stringify([
|
|
46
|
+
font.weight,
|
|
47
|
+
font.style,
|
|
48
|
+
font.src || font.localPath || font.cacheKey
|
|
49
|
+
])))].sort();
|
|
50
|
+
const alias = `${slot[0].family}__${fnv1a64Base36(JSON.stringify(identities))}`;
|
|
51
|
+
for (const font of slot)
|
|
52
|
+
aliases.set(font, alias);
|
|
27
53
|
}
|
|
28
54
|
}
|
|
29
|
-
return
|
|
55
|
+
return aliases.size ? fonts.map((font) => aliases.has(font) ? { ...font, originalFamily: font.originalFamily || font.family, family: aliases.get(font) } : font) : fonts;
|
|
30
56
|
}
|
|
31
57
|
export function buildSubsetFamilyChain(fonts) {
|
|
32
58
|
const chains = /* @__PURE__ */ new Map();
|
|
33
59
|
for (const f of fonts) {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
const arr = chains.get(f.originalFamily);
|
|
37
|
-
if (arr)
|
|
38
|
-
arr.push(f.family);
|
|
39
|
-
else
|
|
40
|
-
chains.set(f.originalFamily, [f.family]);
|
|
60
|
+
if (f.originalFamily)
|
|
61
|
+
chains.set(f.originalFamily, []);
|
|
41
62
|
}
|
|
63
|
+
for (const f of fonts)
|
|
64
|
+
chains.get(f.originalFamily || f.family)?.push(f.family);
|
|
42
65
|
return chains;
|
|
43
66
|
}
|
|
44
67
|
export function resolveSubsetChain(family, chains) {
|
|
@@ -51,7 +51,7 @@ export interface LoadFontsForRendererOptions extends LoadFontsOptions {
|
|
|
51
51
|
*
|
|
52
52
|
* When a font family has multiple unicode-range subsets loaded (e.g., CJK fonts
|
|
53
53
|
* split into many small chunks), renames each subset to a unique family name
|
|
54
|
-
* (e.g., "Noto Sans
|
|
54
|
+
* (e.g., "Noto Sans SC__<stable hash per binary>") so renderers use font-family
|
|
55
55
|
* fallback chains for per-character glyph coverage. Without this, both Satori
|
|
56
56
|
* and Takumi pick the first font file and show .notdef for characters in other subsets.
|
|
57
57
|
*/
|
|
@@ -4,6 +4,7 @@ import { fetchWithEvent } from "#nuxtseo/nitro";
|
|
|
4
4
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
5
5
|
import { isInternalRoute } from "../../shared.js";
|
|
6
6
|
import { getEventQuery } from "../util/query.js";
|
|
7
|
+
import { parseSameOriginPath } from "../util/ssrf.js";
|
|
7
8
|
import { useOgImageRuntimeConfig } from "../utils.js";
|
|
8
9
|
const RE_META_TAG = /<meta\b[^>]*>/gi;
|
|
9
10
|
const RE_META_KEY = /\b(?:property|name)\s*=\s*(?:"([^"]+)"|'([^']+)')/i;
|
|
@@ -11,7 +12,6 @@ const RE_META_CONTENT = /\bcontent\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
|
|
|
11
12
|
const MAX_RESOLVE_PATH_LENGTH = 2048;
|
|
12
13
|
const RE_STRIP_PREFIX = /^.*?\/_og\/r/;
|
|
13
14
|
const RE_IMAGE_EXT = /\.(?:png|jpe?g|webp|svg)$/i;
|
|
14
|
-
const RE_DOUBLE_LEADING_SLASH = /^\/{2,}/;
|
|
15
15
|
function extractMeta(html, key) {
|
|
16
16
|
for (const tagMatch of html.matchAll(RE_META_TAG)) {
|
|
17
17
|
const tag = tagMatch[0];
|
|
@@ -78,7 +78,7 @@ export default defineEventHandler(async (event) => {
|
|
|
78
78
|
statusMessage: `[Nuxt OG Image] Target path exceeds ${MAX_RESOLVE_PATH_LENGTH} characters.`
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
-
if (targetPath.includes("://") ||
|
|
81
|
+
if (targetPath.includes("://") || !parseSameOriginPath(targetPath)) {
|
|
82
82
|
throw createError({
|
|
83
83
|
statusCode: 400,
|
|
84
84
|
statusMessage: "[Nuxt OG Image] Target path must be a same-origin path."
|
|
@@ -7,11 +7,11 @@ export function getCloudflareEnv(event) {
|
|
|
7
7
|
const runtimeEnv = toRecord(runtimeCloudflare?.env);
|
|
8
8
|
if (runtimeEnv)
|
|
9
9
|
return runtimeEnv;
|
|
10
|
-
const contextCloudflare = toRecord(event?.context
|
|
10
|
+
const contextCloudflare = toRecord(event?.context?.cloudflare);
|
|
11
11
|
const contextEnv = toRecord(contextCloudflare?.env);
|
|
12
12
|
if (contextEnv)
|
|
13
13
|
return contextEnv;
|
|
14
|
-
const platform = toRecord(event?.context
|
|
14
|
+
const platform = toRecord(event?.context?._platform);
|
|
15
15
|
const platformCloudflare = toRecord(platform?.cloudflare);
|
|
16
16
|
return toRecord(platformCloudflare?.env);
|
|
17
17
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createError, getRequestHost, H3Error, setHeader } from "#nuxtseo/h3";
|
|
1
|
+
import { appendResponseHeader, createError, getRequestHost, H3Error, setHeader } from "#nuxtseo/h3";
|
|
2
2
|
import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
|
|
3
3
|
import { logger } from "../../logger.js";
|
|
4
4
|
import { getBuildCachedImage, setBuildCachedImage } from "../og-image/cache/buildCache.js";
|
|
@@ -22,8 +22,8 @@ export async function imageEventHandler(e) {
|
|
|
22
22
|
} finally {
|
|
23
23
|
timings.record("total", performance.now() - reqStart);
|
|
24
24
|
const header = timings.header();
|
|
25
|
-
if (header)
|
|
26
|
-
|
|
25
|
+
if (header && !e.node?.res?.headersSent)
|
|
26
|
+
appendResponseHeader(e, "Server-Timing", header);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
async function renderOgImage(e, ctx) {
|