nuxt-og-image 6.10.4 → 7.0.1
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/README.md +10 -14
- package/dist/chunks/tw4.mjs +6 -6
- package/dist/chunks/uno.mjs +7 -7
- package/dist/cli.mjs +4 -4
- package/dist/devtools/components/og-image/RendererSelectModal.vue +1 -1
- package/dist/devtools/lib/og-image/fonts.ts +38 -0
- package/dist/devtools/lib/og-image/types.ts +7 -0
- package/dist/devtools/pages/og-image/index.vue +19 -14
- package/dist/module.d.mts +14 -10
- package/dist/module.d.ts +14 -10
- package/dist/module.json +2 -2
- package/dist/module.mjs +6 -6
- package/dist/runtime/app/client-utils.js +7 -4
- package/dist/runtime/app/components/Templates/Community/SaaS.satori.vue +1 -1
- package/dist/runtime/app/components/Templates/Community/SimpleBlog.satori.vue +1 -1
- package/dist/runtime/app/disabled/components.d.ts +1 -0
- package/dist/runtime/app/disabled/components.js +0 -0
- package/dist/runtime/app/disabled/composables/defineOgImage.d.ts +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImage.js +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImageComponent.d.ts +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImageComponent.js +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.d.ts +1 -0
- package/dist/runtime/app/disabled/composables/defineOgImageScreenshot.js +1 -0
- package/dist/runtime/app/disabled/composables/mock.d.ts +1 -0
- package/dist/runtime/app/disabled/composables/mock.js +1 -0
- package/dist/runtime/app/disabled/index.d.ts +3 -0
- package/dist/runtime/app/disabled/index.js +3 -0
- package/dist/runtime/app/index.d.ts +4 -0
- package/dist/runtime/app/index.js +4 -0
- package/dist/runtime/app/utils/plugins.js +1 -1
- package/dist/runtime/app/utils.js +2 -2
- package/dist/runtime/server/index.d.ts +1 -0
- package/dist/runtime/server/index.js +1 -0
- package/dist/runtime/server/middleware/signing.d.ts +2 -0
- package/dist/runtime/server/middleware/signing.js +19 -0
- package/dist/runtime/server/nitro.d.ts +3 -3
- package/dist/runtime/server/nitro.js +4 -4
- package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +8 -49
- package/dist/runtime/server/og-image/bindings/font-assets/node.js +3 -2
- package/dist/runtime/server/og-image/bindings/satori/node.d.ts +1 -2
- package/dist/runtime/server/og-image/bindings/satori/wasm-fs.d.ts +1 -2
- package/dist/runtime/server/og-image/bindings/satori/wasm-fs.js +1 -1
- package/dist/runtime/server/og-image/bindings/satori/wasm.d.ts +1 -2
- package/dist/runtime/server/og-image/bindings/satori/wasm.js +1 -1
- package/dist/runtime/server/og-image/browser/screenshot.js +5 -3
- package/dist/runtime/server/og-image/cache/publish.d.ts +46 -0
- package/dist/runtime/server/og-image/cache/publish.js +91 -0
- package/dist/runtime/server/og-image/cache/published.d.ts +17 -0
- package/dist/runtime/server/og-image/cache/published.js +49 -0
- package/dist/runtime/server/og-image/context.js +1 -1
- package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -5
- package/dist/runtime/server/og-image/core/transforms/emojis/index.d.ts +3 -2
- package/dist/runtime/server/og-image/core/vnodes.js +19 -1
- package/dist/runtime/server/og-image/font-source.d.ts +1 -0
- package/dist/runtime/server/og-image/font-source.js +1 -0
- package/dist/runtime/server/og-image/fonts.d.ts +1 -1
- package/dist/runtime/server/og-image/fonts.js +1 -1
- package/dist/runtime/server/og-image/satori/filters.d.ts +1 -0
- package/dist/runtime/server/og-image/satori/filters.js +14 -0
- package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
- package/dist/runtime/server/og-image/satori/plugins/classes.js +0 -7
- package/dist/runtime/server/og-image/satori/renderer.js +2 -1
- package/dist/runtime/server/og-image/satori/vnodes.js +1 -4
- package/dist/runtime/server/og-image/takumi/renderer.js +11 -47
- package/dist/runtime/server/og-image/utils/css.d.ts +0 -5
- package/dist/runtime/server/og-image/utils/css.js +0 -5
- package/dist/runtime/server/plugins/auto-eject-nitro3.d.ts +2 -0
- package/dist/runtime/server/plugins/auto-eject-nitro3.js +9 -0
- package/dist/runtime/server/plugins/auto-eject.js +3 -31
- package/dist/runtime/server/plugins/prerender.js +67 -2
- package/dist/runtime/server/plugins/publish.d.ts +2 -0
- package/dist/runtime/server/plugins/publish.js +44 -0
- package/dist/runtime/server/routes/debug.json.d.ts +10 -1
- package/dist/runtime/server/routes/debug.json.js +4 -4
- package/dist/runtime/server/routes/static-font.d.ts +2 -0
- package/dist/runtime/server/routes/static-font.js +22 -0
- package/dist/runtime/server/signing-secret.d.ts +8 -0
- package/dist/runtime/server/signing-secret.js +75 -0
- package/dist/runtime/server/util/auto-eject-lifecycle.d.ts +13 -0
- package/dist/runtime/server/util/auto-eject-lifecycle.js +31 -0
- package/dist/runtime/server/util/cache.d.ts +1 -0
- package/dist/runtime/server/util/cache.js +12 -6
- package/dist/runtime/server/util/cloudflare.d.ts +2 -2
- package/dist/runtime/server/util/eventHandlers.js +68 -3
- package/dist/runtime/server/util/fetchLocalAsset.js +2 -2
- package/dist/runtime/server/util/origin.d.ts +3 -0
- package/dist/runtime/server/util/origin.js +4 -0
- package/dist/runtime/server/util/publishWarnings.d.ts +6 -0
- package/dist/runtime/server/util/publishWarnings.js +6 -0
- package/dist/runtime/server/utils.d.ts +3 -3
- package/dist/runtime/server/utils.js +5 -10
- package/dist/runtime/shared/publish.d.ts +13 -0
- package/dist/runtime/shared/publish.js +17 -0
- package/dist/runtime/shared/urlEncoding.js +9 -4
- package/dist/runtime/types.d.ts +11 -16
- package/dist/shared/{nuxt-og-image.Bcegsloy.mjs → nuxt-og-image.DR_SxdDv.mjs} +1224 -1109
- package/package.json +60 -62
- package/skills/nuxt-og-image/SKILL.md +31 -14
- package/types/virtual.d.ts +8 -0
- package/dist/runtime/server/og-image/satori/plugins/emojis.d.ts +0 -2
- package/dist/runtime/server/og-image/satori/plugins/emojis.js +0 -112
- package/dist/runtime/server/og-image/satori/plugins/flex.d.ts +0 -2
- package/dist/runtime/server/og-image/satori/plugins/flex.js +0 -48
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RequestEvent } from 'nuxt/server';
|
|
2
2
|
import type { OgImageOptionsInternal } from '../types.js';
|
|
3
3
|
type Options = Partial<OgImageOptionsInternal>;
|
|
4
4
|
/**
|
|
5
5
|
* Build the absolute OG image URL for a page. Without a page path, uses the current request path.
|
|
6
6
|
*/
|
|
7
|
-
export declare function getOgImageUrl(event:
|
|
8
|
-
export declare function getOgImageUrl(event:
|
|
7
|
+
export declare function getOgImageUrl(event: RequestEvent, options?: Options): string;
|
|
8
|
+
export declare function getOgImageUrl(event: RequestEvent, pagePath: string, options?: Options): string;
|
|
9
9
|
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getRequestURL, useRuntimeConfig } from "nuxt/server";
|
|
2
|
+
import { withoutBase } from "ufo";
|
|
3
3
|
import { withSiteUrl } from "#site-config/server/composables/utils";
|
|
4
4
|
import { getOgImagePath } from "./utils.js";
|
|
5
5
|
export function getOgImageUrl(event, pathOrOptions, maybeOptions) {
|
|
6
|
-
const pagePath = typeof pathOrOptions === "string" ? pathOrOptions : withoutBase(
|
|
6
|
+
const pagePath = typeof pathOrOptions === "string" ? pathOrOptions : withoutBase(getRequestURL(event).pathname, useRuntimeConfig().app.baseURL) || "/";
|
|
7
7
|
const options = typeof pathOrOptions === "string" ? maybeOptions : pathOrOptions;
|
|
8
8
|
const { path } = getOgImagePath(event, pagePath, options);
|
|
9
|
-
return withSiteUrl(event, path, { canonical: !import.meta.dev });
|
|
9
|
+
return withSiteUrl(event, path, { canonical: !import.meta.dev, withBase: true });
|
|
10
10
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { join } from "pathe";
|
|
3
3
|
import { withBase } from "ufo";
|
|
4
4
|
import { getRequestURL } from "#nuxtseo/h3";
|
|
5
5
|
import { fetchWithEvent, useRuntimeConfig } from "#nuxtseo/nitro";
|
|
6
|
-
import {
|
|
6
|
+
import { resolvedFontPaths, rootDir, staticFontCacheDir } from "#og-image-virtual/build-dir.mjs";
|
|
7
7
|
import { getSiteConfig } from "#site-config/server/composables";
|
|
8
8
|
import { getFetchTimeout } from "../../../util/fetchTimeout.js";
|
|
9
9
|
import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
|
|
10
10
|
import { useOgImageRuntimeConfig } from "../../../utils.js";
|
|
11
11
|
import { fetchSpecialFontUrl, isDataFontUrl, isExternalFontUrl } from "./external-url.js";
|
|
12
|
-
let fontUrlMapping;
|
|
13
12
|
async function readOptionalFile(path) {
|
|
14
13
|
return readFile(path).catch((error) => {
|
|
15
14
|
if (error.code === "ENOENT")
|
|
@@ -17,18 +16,6 @@ async function readOptionalFile(path) {
|
|
|
17
16
|
throw error;
|
|
18
17
|
});
|
|
19
18
|
}
|
|
20
|
-
async function fetchOptionalFont(url, timeout) {
|
|
21
|
-
return fetch(url, { signal: AbortSignal.timeout(timeout) }).catch(() => {
|
|
22
|
-
return null;
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
async function loadFontUrlMapping() {
|
|
26
|
-
if (fontUrlMapping)
|
|
27
|
-
return fontUrlMapping;
|
|
28
|
-
const content = await readOptionalFile(join(buildDir, "cache", "og-image", "font-urls.json"));
|
|
29
|
-
fontUrlMapping = content ? JSON.parse(content.toString("utf-8")) : {};
|
|
30
|
-
return fontUrlMapping;
|
|
31
|
-
}
|
|
32
19
|
export async function resolve(event, font) {
|
|
33
20
|
const path = font.src || font.localPath;
|
|
34
21
|
const runtimeConfig = useOgImageRuntimeConfig();
|
|
@@ -40,36 +27,17 @@ export async function resolve(event, font) {
|
|
|
40
27
|
}
|
|
41
28
|
if (path && (isDataFontUrl(path) || isExternalFontUrl(path)))
|
|
42
29
|
return fetchSpecialFontUrl(path, getSiteConfig(event).url, timeout);
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
if (filename && basename(filename) === filename) {
|
|
46
|
-
const cached = await readOptionalFile(join(buildDir, "cache", "fonts", filename));
|
|
47
|
-
if (cached?.length)
|
|
48
|
-
return cached;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
30
|
+
if (resolvedFontPaths[path])
|
|
31
|
+
return readFile(resolvedFontPaths[path]);
|
|
51
32
|
if (import.meta.prerender) {
|
|
52
33
|
if (path.startsWith("/_og-static-fonts/")) {
|
|
53
|
-
const
|
|
54
|
-
const cached = await readOptionalFile(join(staticFontCacheDir,
|
|
34
|
+
const filename = path.slice("/_og-static-fonts/".length);
|
|
35
|
+
const cached = await readOptionalFile(join(staticFontCacheDir, filename));
|
|
55
36
|
if (cached?.length)
|
|
56
37
|
return cached;
|
|
57
38
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const cached = await readOptionalFile(join(rootDir, ".output", "public", "_fonts", filename2));
|
|
61
|
-
if (cached?.length)
|
|
62
|
-
return cached;
|
|
63
|
-
const mapping = await loadFontUrlMapping();
|
|
64
|
-
if (mapping[filename2]) {
|
|
65
|
-
const res = await fetchOptionalFont(mapping[filename2], timeout);
|
|
66
|
-
if (res?.ok)
|
|
67
|
-
return Buffer.from(await res.arrayBuffer());
|
|
68
|
-
}
|
|
69
|
-
throw new Error(`Font ${filename2} not found in mapping or cache`);
|
|
70
|
-
}
|
|
71
|
-
const filename = path.slice(1);
|
|
72
|
-
const data = await readOptionalFile(join(rootDir, "public", filename)) || await readOptionalFile(join(rootDir, ".output", "public", filename));
|
|
39
|
+
const publicPath = path.slice(1);
|
|
40
|
+
const data = await readOptionalFile(join(rootDir, "public", publicPath));
|
|
73
41
|
if (data?.length)
|
|
74
42
|
return data;
|
|
75
43
|
}
|
|
@@ -79,15 +47,6 @@ export async function resolve(event, font) {
|
|
|
79
47
|
if (cached?.length)
|
|
80
48
|
return cached;
|
|
81
49
|
}
|
|
82
|
-
if (import.meta.dev && path.startsWith("/_fonts/")) {
|
|
83
|
-
const filename = path.slice("/_fonts/".length);
|
|
84
|
-
const mapping = await loadFontUrlMapping();
|
|
85
|
-
if (mapping[filename]) {
|
|
86
|
-
const res = await fetchOptionalFont(mapping[filename], timeout);
|
|
87
|
-
if (res?.ok)
|
|
88
|
-
return Buffer.from(await res.arrayBuffer());
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
50
|
if (import.meta.dev) {
|
|
92
51
|
const filename = path.slice(1);
|
|
93
52
|
const data = await readOptionalFile(join(rootDir, "public", filename));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { withBase } from "ufo";
|
|
2
2
|
import { fetchWithEvent, useRuntimeConfig } from "#nuxtseo/nitro";
|
|
3
|
-
import {
|
|
3
|
+
import { getSiteConfig } from "#site-config/server/composables";
|
|
4
4
|
import { getFetchTimeout } from "../../../util/fetchTimeout.js";
|
|
5
|
+
import { getOgImageOrigin } from "../../../util/origin.js";
|
|
5
6
|
import { fetchWithRedirectValidation } from "../../../util/ssrf.js";
|
|
6
7
|
import { useOgImageRuntimeConfig } from "../../../utils.js";
|
|
7
8
|
import { fetchSpecialFontUrl, isDataFontUrl, isExternalFontUrl } from "./external-url.js";
|
|
@@ -10,7 +11,7 @@ export async function resolve(event, font) {
|
|
|
10
11
|
const runtimeConfig = useOgImageRuntimeConfig();
|
|
11
12
|
const timeout = getFetchTimeout(runtimeConfig);
|
|
12
13
|
const { app } = useRuntimeConfig();
|
|
13
|
-
const origin =
|
|
14
|
+
const origin = getOgImageOrigin(event);
|
|
14
15
|
const fullPath = withBase(path, app.baseURL);
|
|
15
16
|
if (path && (isDataFontUrl(path) || isExternalFontUrl(path)))
|
|
16
17
|
return fetchSpecialFontUrl(path, getSiteConfig(event).url, timeout);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import _satori, { init } from "satori/standalone";
|
|
2
2
|
import { readWasmFile } from "../../../util/wasm.js";
|
|
3
|
-
const wasmBinary = readWasmFile("satori/
|
|
3
|
+
const wasmBinary = readWasmFile("satori/layout.wasm");
|
|
4
4
|
export default {
|
|
5
5
|
initWasmPromise: wasmBinary.then(async (wasm) => {
|
|
6
6
|
await init(wasm);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import _satori, { init } from "satori/standalone";
|
|
2
2
|
import { importWasm } from "../../../util/wasm.js";
|
|
3
3
|
export default {
|
|
4
|
-
initWasmPromise: importWasm(import("#og-image/
|
|
4
|
+
initWasmPromise: importWasm(import("#og-image/layout-wasm")).then((wasm) => init(wasm)),
|
|
5
5
|
satori: _satori
|
|
6
6
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { withQuery } from "ufo";
|
|
2
2
|
import { toValue } from "vue";
|
|
3
3
|
import { fetchWithEvent } from "#nuxtseo/nitro";
|
|
4
|
-
import { getNitroOrigin } from "#site-config/server/composables";
|
|
5
4
|
import { buildOgImageUrl } from "../../../shared.js";
|
|
6
5
|
import { getFetchTimeout } from "../../util/fetchTimeout.js";
|
|
7
6
|
import { logger } from "../../util/logger.js";
|
|
7
|
+
import { getOgImageOrigin } from "../../util/origin.js";
|
|
8
8
|
import { resolveSameOriginUrl } from "../../util/ssrf.js";
|
|
9
9
|
import { useOgImageRuntimeConfig } from "../../utils.js";
|
|
10
10
|
function isPlaywrightPage(page) {
|
|
@@ -71,13 +71,15 @@ export async function createScreenshot({ basePath, e, options, extension, timing
|
|
|
71
71
|
} else {
|
|
72
72
|
page = await browser.newPage({
|
|
73
73
|
colorScheme: colorPreference || "no-preference",
|
|
74
|
-
baseURL:
|
|
74
|
+
baseURL: getOgImageOrigin(e)
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
try {
|
|
78
78
|
if (options.html) {
|
|
79
79
|
logger.warn("The `html` option is deprecated and will be removed in the next major version. Use a Vue component instead.");
|
|
80
80
|
}
|
|
81
|
+
if (import.meta.prerender && options._prerenderHtml)
|
|
82
|
+
options.html = options._prerenderHtml;
|
|
81
83
|
if (import.meta.prerender && !options.html) {
|
|
82
84
|
options.html = await timings.measure("html-fetch", () => fetchWithEvent(e, path, { timeout: getFetchTimeout(runtimeConfig) }).catch((err) => {
|
|
83
85
|
void err;
|
|
@@ -102,7 +104,7 @@ export async function createScreenshot({ basePath, e, options, extension, timing
|
|
|
102
104
|
await page.setContent(html, { waitUntil: "networkidle0" });
|
|
103
105
|
}
|
|
104
106
|
} else {
|
|
105
|
-
const url = resolveSameOriginUrl(withQuery(path, options.props || {}),
|
|
107
|
+
const url = resolveSameOriginUrl(withQuery(path, options.props || {}), getOgImageOrigin(e));
|
|
106
108
|
if (!url)
|
|
107
109
|
throw new Error("[Nuxt OG Image] The browser renderer can only navigate to a same-origin path.");
|
|
108
110
|
await gotoWithIdle(page, url, 1e4);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Storage } from 'unstorage';
|
|
2
|
+
interface PublishStorage {
|
|
3
|
+
getMount: Storage['getMount'];
|
|
4
|
+
hasItem: (key: string) => Promise<boolean>;
|
|
5
|
+
getItem: (key: string) => Promise<unknown>;
|
|
6
|
+
setItem: (key: string, value: object, options?: Record<string, unknown>) => Promise<void>;
|
|
7
|
+
setItemRaw: (key: string, value: Uint8Array, options?: Record<string, unknown>) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
interface PublishInput {
|
|
10
|
+
storage: PublishStorage;
|
|
11
|
+
mount: string;
|
|
12
|
+
baseURL: string;
|
|
13
|
+
key: string;
|
|
14
|
+
extension: 'png' | 'jpg' | 'jpeg' | 'webp';
|
|
15
|
+
maxAgeSeconds: number;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
now: () => number;
|
|
18
|
+
}
|
|
19
|
+
export interface Published {
|
|
20
|
+
_tag: 'Published';
|
|
21
|
+
url: string;
|
|
22
|
+
objectKey: string;
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
}
|
|
25
|
+
interface Unavailable {
|
|
26
|
+
_tag: 'Unavailable';
|
|
27
|
+
reason: unknown;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Normalize signed image paths without loading a renderer during page SSR.
|
|
31
|
+
* The key is a full SHA-256 hex digest, so it has one fixed format.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getPublishKey(url: string, defaults: Record<string, unknown>, cacheVersion: string): string;
|
|
34
|
+
/** A page may reference the bucket only while its manifest and object both exist. */
|
|
35
|
+
export declare function getPublishedImage(input: PublishInput): Promise<Published | Unavailable | {
|
|
36
|
+
_tag: 'Miss';
|
|
37
|
+
}>;
|
|
38
|
+
/** Publish immutable bytes. Manifest expiry limits reuse, never object retention. */
|
|
39
|
+
export declare function publishImage(input: PublishInput & {
|
|
40
|
+
render: () => Promise<Uint8Array>;
|
|
41
|
+
/** Original buffer expiry, preserved across uploads and cache hits. */
|
|
42
|
+
expiresAt?: number;
|
|
43
|
+
/** Fresh runtime bytes replace the manifest even if an older object is still reusable. */
|
|
44
|
+
force?: boolean;
|
|
45
|
+
}): Promise<Published | Unavailable>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { parseURL } from "ufo";
|
|
2
|
+
import { parsePublishConfig } from "../../../shared/publish.js";
|
|
3
|
+
import { digestCanonical, extractEncodedSegment } from "../../../shared/urlEncoding.js";
|
|
4
|
+
import { withTimeout } from "../../util/withTimeout.js";
|
|
5
|
+
export function getPublishKey(url, defaults, cacheVersion) {
|
|
6
|
+
const { pathname: path, host, protocol } = parseURL(url);
|
|
7
|
+
const extension = path.split(".").pop();
|
|
8
|
+
const segment = decodeURIComponent(extractEncodedSegment(path, extension)).replace(/,s_[^,]+$/, "");
|
|
9
|
+
return digestCanonical([{ segment, defaults, host, protocol }, cacheVersion]);
|
|
10
|
+
}
|
|
11
|
+
export async function getPublishedImage(input) {
|
|
12
|
+
return withTimeout((async () => {
|
|
13
|
+
const target = resolveTarget(input);
|
|
14
|
+
return target._tag === "Unavailable" ? target : lookupPublishedImage(input, target);
|
|
15
|
+
})(), input.timeoutMs ?? 15e3, "OG image publish lookup").catch((reason) => ({ _tag: "Unavailable", reason }));
|
|
16
|
+
}
|
|
17
|
+
export async function publishImage(input) {
|
|
18
|
+
return withTimeout((async () => {
|
|
19
|
+
const target = resolveTarget(input);
|
|
20
|
+
if (target._tag === "Unavailable")
|
|
21
|
+
return target;
|
|
22
|
+
if (!input.force) {
|
|
23
|
+
const cached = await lookupPublishedImage(input, target);
|
|
24
|
+
if (cached._tag === "Published")
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
const { storage } = target;
|
|
28
|
+
const bytes = await input.render();
|
|
29
|
+
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes));
|
|
30
|
+
const hash = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
31
|
+
const objectKey = `${hash}.${input.extension}`;
|
|
32
|
+
if (!await storage.hasItem(objectKey)) {
|
|
33
|
+
await storage.setItemRaw(objectKey, bytes, {
|
|
34
|
+
httpMetadata: {
|
|
35
|
+
contentType: `image/${input.extension === "jpg" ? "jpeg" : input.extension}`,
|
|
36
|
+
cacheControl: "public, max-age=31536000, immutable"
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (!await storage.hasItem(objectKey))
|
|
40
|
+
return { _tag: "Unavailable", reason: "Publish storage did not retain the image." };
|
|
41
|
+
}
|
|
42
|
+
const expiresAt = input.expiresAt ?? input.now() + (Number.isFinite(input.maxAgeSeconds) ? Math.max(0, input.maxAgeSeconds) : 0) * 1e3;
|
|
43
|
+
await storage.setItem(manifestKey(input), {
|
|
44
|
+
objectKey,
|
|
45
|
+
expiresAt
|
|
46
|
+
}, { ttl: Math.max(60, Number.isFinite(input.maxAgeSeconds) ? Math.ceil(input.maxAgeSeconds) : 60) });
|
|
47
|
+
return published(target, objectKey, expiresAt);
|
|
48
|
+
})(), input.timeoutMs ?? 15e3, "OG image publication").catch((reason) => ({ _tag: "Unavailable", reason }));
|
|
49
|
+
}
|
|
50
|
+
function resolveTarget(input) {
|
|
51
|
+
const parsed = parsePublishConfig({ storage: input.mount, baseURL: input.baseURL });
|
|
52
|
+
if (parsed._tag === "Err")
|
|
53
|
+
return { _tag: "Unavailable", reason: parsed.reason };
|
|
54
|
+
const { storage: mount, baseURL } = parsed.value;
|
|
55
|
+
const mounted = input.storage.getMount(`${mount}:`);
|
|
56
|
+
if (!mount || mounted.base !== `${mount}:` || !mounted.driver.setItemRaw)
|
|
57
|
+
return { _tag: "Unavailable", reason: "Publish storage needs a mounted driver with raw writes." };
|
|
58
|
+
const prefix = `${mount}:`;
|
|
59
|
+
const storage = {
|
|
60
|
+
hasItem: (key) => input.storage.hasItem(prefix + key),
|
|
61
|
+
getItem: (key) => input.storage.getItem(prefix + key),
|
|
62
|
+
setItem: (key, value, options) => input.storage.setItem(prefix + key, value, options),
|
|
63
|
+
setItemRaw: (key, value, options) => input.storage.setItemRaw(prefix + key, value, options)
|
|
64
|
+
};
|
|
65
|
+
return { _tag: "Target", storage, baseURL };
|
|
66
|
+
}
|
|
67
|
+
async function lookupPublishedImage(input, target) {
|
|
68
|
+
if (!(input.maxAgeSeconds > 0))
|
|
69
|
+
return { _tag: "Miss" };
|
|
70
|
+
const raw = await target.storage.getItem(manifestKey(input));
|
|
71
|
+
const manifest = parseManifest(raw, input.extension);
|
|
72
|
+
if (manifest && manifest.expiresAt > input.now() && await target.storage.hasItem(manifest.objectKey))
|
|
73
|
+
return published(target, manifest.objectKey, manifest.expiresAt);
|
|
74
|
+
return { _tag: "Miss" };
|
|
75
|
+
}
|
|
76
|
+
function manifestKey(input) {
|
|
77
|
+
return `manifest:${input.key}.${input.extension}.json`;
|
|
78
|
+
}
|
|
79
|
+
function published(target, objectKey, expiresAt) {
|
|
80
|
+
return { _tag: "Published", objectKey, url: `${target.baseURL}/${objectKey}`, expiresAt };
|
|
81
|
+
}
|
|
82
|
+
function parseManifest(value, extension) {
|
|
83
|
+
if (!value || typeof value !== "object")
|
|
84
|
+
return;
|
|
85
|
+
const record = value;
|
|
86
|
+
if (typeof record.objectKey !== "string" || !new RegExp(`^[a-f0-9]{64}\\.${extension}$`).test(record.objectKey))
|
|
87
|
+
return;
|
|
88
|
+
if (typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt))
|
|
89
|
+
return;
|
|
90
|
+
return { objectKey: record.objectKey, expiresAt: record.expiresAt };
|
|
91
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OgImageRenderEventContext } from '../../../types.js';
|
|
2
|
+
import type { Published } from './publish.js';
|
|
3
|
+
export declare function createPublishedImageCache(max?: number): {
|
|
4
|
+
get(key: string, now: number): Published | undefined;
|
|
5
|
+
isPublishing: (key: string, now: number) => boolean;
|
|
6
|
+
remember: (key: string, image: Published, now: number) => void;
|
|
7
|
+
forget: (key: string) => void;
|
|
8
|
+
begin(key: string, now: number, timeoutMs: number): symbol | undefined;
|
|
9
|
+
complete(key: string, token: symbol, result: Published | {
|
|
10
|
+
_tag: "Unavailable";
|
|
11
|
+
}, now: number): void;
|
|
12
|
+
};
|
|
13
|
+
export declare function getPublishedImageCache(app: OgImageRenderEventContext['_nitro']): any;
|
|
14
|
+
export declare function getPublishedImageCacheKey(publish: {
|
|
15
|
+
storage: string;
|
|
16
|
+
baseURL: string;
|
|
17
|
+
}, key: string, extension: string): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { LRUCache } from "lru-cache";
|
|
2
|
+
export function createPublishedImageCache(max = 1e3) {
|
|
3
|
+
const entries = new LRUCache({ max });
|
|
4
|
+
function read(key, now) {
|
|
5
|
+
const entry = entries.get(key);
|
|
6
|
+
if (entry && entry.validUntil <= now) {
|
|
7
|
+
entries.delete(key);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
return entry;
|
|
11
|
+
}
|
|
12
|
+
function remember(key, image, now) {
|
|
13
|
+
if (image.expiresAt > now)
|
|
14
|
+
entries.set(key, { _tag: "Published", image, validUntil: Math.min(image.expiresAt, now + 6e4) });
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
get(key, now) {
|
|
18
|
+
const entry = read(key, now);
|
|
19
|
+
return entry?._tag === "Published" ? entry.image : void 0;
|
|
20
|
+
},
|
|
21
|
+
isPublishing: (key, now) => read(key, now)?._tag === "Publishing",
|
|
22
|
+
remember,
|
|
23
|
+
forget: (key) => {
|
|
24
|
+
entries.delete(key);
|
|
25
|
+
},
|
|
26
|
+
begin(key, now, timeoutMs) {
|
|
27
|
+
if (read(key, now)?._tag === "Publishing")
|
|
28
|
+
return;
|
|
29
|
+
const token = Symbol(key);
|
|
30
|
+
entries.set(key, { _tag: "Publishing", token, validUntil: now + timeoutMs });
|
|
31
|
+
return token;
|
|
32
|
+
},
|
|
33
|
+
complete(key, token, result, now) {
|
|
34
|
+
const entry = entries.peek(key);
|
|
35
|
+
if (entry?._tag !== "Publishing" || entry.token !== token)
|
|
36
|
+
return;
|
|
37
|
+
entries.delete(key);
|
|
38
|
+
if (result._tag === "Published")
|
|
39
|
+
remember(key, result, now);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function getPublishedImageCache(app) {
|
|
44
|
+
const state = app;
|
|
45
|
+
return state._ogImagePublishedImages ??= createPublishedImageCache();
|
|
46
|
+
}
|
|
47
|
+
export function getPublishedImageCacheKey(publish, key, extension) {
|
|
48
|
+
return JSON.stringify([publish.storage, publish.baseURL, key, extension]);
|
|
49
|
+
}
|
|
@@ -38,7 +38,7 @@ export async function resolveContext(e) {
|
|
|
38
38
|
if (!sigMatch) {
|
|
39
39
|
return createError({
|
|
40
40
|
statusCode: 403,
|
|
41
|
-
statusMessage: "[Nuxt OG Image] Missing URL signature.
|
|
41
|
+
statusMessage: "[Nuxt OG Image] Missing URL signature. Set NUXT_APP_SECRET to sign URLs."
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
const signature = sigMatch[1];
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { parseURL, withBase, withoutLeadingSlash } from "ufo";
|
|
2
2
|
import { useStorage } from "#nuxtseo/nitro";
|
|
3
|
-
import { getNitroOrigin } from "#site-config/server/composables/getNitroOrigin";
|
|
4
3
|
import { toBase64Image } from "../../../../shared.js";
|
|
5
4
|
import { decodeHtml } from "../../../util/encoding.js";
|
|
6
5
|
import { fetchLocalAsset } from "../../../util/fetchLocalAsset.js";
|
|
7
6
|
import { getFetchTimeout } from "../../../util/fetchTimeout.js";
|
|
8
7
|
import { logger } from "../../../util/logger.js";
|
|
8
|
+
import { getOgImageOrigin } from "../../../util/origin.js";
|
|
9
9
|
import { fetchWithRedirectValidation, isBlockedUrl, parseSameOriginPath } from "../../../util/ssrf.js";
|
|
10
10
|
import { getImageDimensions } from "../../utils/image-detector.js";
|
|
11
11
|
import { defineTransformer } from "../plugins.js";
|
|
@@ -123,9 +123,6 @@ export default defineTransformer([
|
|
|
123
123
|
let src = node.props.src;
|
|
124
124
|
if (src.startsWith("data:"))
|
|
125
125
|
return;
|
|
126
|
-
if (ctx.renderer.name === "satori" && src.endsWith(".webp")) {
|
|
127
|
-
logger.warn("Using WebP images with Satori is not supported. Please consider switching image format or use the takumi or browser renderer.", src);
|
|
128
|
-
}
|
|
129
126
|
const isRelative = src.startsWith("/");
|
|
130
127
|
if (!isRelative)
|
|
131
128
|
src = node.props.src = decodeHtml(src);
|
|
@@ -140,7 +137,7 @@ export default defineTransformer([
|
|
|
140
137
|
return;
|
|
141
138
|
}
|
|
142
139
|
if (isRelative) {
|
|
143
|
-
node.props.src = withBase(src, `${
|
|
140
|
+
node.props.src = withBase(src, `${getOgImageOrigin(ctx.e)}`);
|
|
144
141
|
return;
|
|
145
142
|
}
|
|
146
143
|
if (!import.meta.dev)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { NuxtIslandResponse } from 'nuxt/app';
|
|
2
1
|
import type { OgImageRenderEventContext } from '../../../../../types.js';
|
|
3
2
|
/**
|
|
4
3
|
* Apply emoji transformations to island HTML content.
|
|
5
4
|
* Replaces emoji characters with inline SVG elements.
|
|
6
5
|
*/
|
|
7
|
-
export declare function applyEmojis(ctx: OgImageRenderEventContext, island:
|
|
6
|
+
export declare function applyEmojis(ctx: OgImageRenderEventContext, island: {
|
|
7
|
+
html: string;
|
|
8
|
+
}): Promise<void>;
|
|
@@ -119,6 +119,21 @@ export function htmlToVNode(html) {
|
|
|
119
119
|
throw new Error("Failed to parse HTML - no root div found");
|
|
120
120
|
return elementToVNode(root);
|
|
121
121
|
}
|
|
122
|
+
function omitImagesWithoutSource(node) {
|
|
123
|
+
const children = node.props.children;
|
|
124
|
+
if (!Array.isArray(children))
|
|
125
|
+
return;
|
|
126
|
+
node.props.children = children.filter((child) => {
|
|
127
|
+
if (!child || typeof child !== "object")
|
|
128
|
+
return true;
|
|
129
|
+
if (child.type === "img" && !child.props.src)
|
|
130
|
+
return false;
|
|
131
|
+
omitImagesWithoutSource(child);
|
|
132
|
+
return true;
|
|
133
|
+
});
|
|
134
|
+
if (node.props.children.length === 0)
|
|
135
|
+
delete node.props.children;
|
|
136
|
+
}
|
|
122
137
|
const UNSUPPORTED_SVG_ELEMENTS = /* @__PURE__ */ new Set(["text", "tspan", "textPath", "foreignObject", "switch", "a"]);
|
|
123
138
|
export function warnUnsupportedSvgElements(vnode, component) {
|
|
124
139
|
const unsupported = /* @__PURE__ */ new Set();
|
|
@@ -157,12 +172,14 @@ export async function createVNodes(ctx, options) {
|
|
|
157
172
|
if (!html) {
|
|
158
173
|
const islandTimeout = ctx.runtimeConfig.security?.renderTimeout ?? 15e3;
|
|
159
174
|
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));
|
|
160
|
-
await applyEmojis(ctx, island);
|
|
161
175
|
html = island.html;
|
|
162
176
|
if (html?.includes("<body>")) {
|
|
163
177
|
html = html.match(RE_BODY_CONTENT)?.[1] || "";
|
|
164
178
|
}
|
|
165
179
|
}
|
|
180
|
+
const content = { html };
|
|
181
|
+
await applyEmojis(ctx, content);
|
|
182
|
+
html = content.html;
|
|
166
183
|
const baseStyle = `position: relative; display: flex; margin: 0 auto; width: ${ctx.options.width}px; height: ${ctx.options.height}px; overflow: hidden;`;
|
|
167
184
|
const fullStyle = options?.wrapperStyle ? `${baseStyle} ${options.wrapperStyle}` : baseStyle;
|
|
168
185
|
const template = `<div style="${fullStyle}">${html}</div>`;
|
|
@@ -173,5 +190,6 @@ export async function createVNodes(ctx, options) {
|
|
|
173
190
|
}
|
|
174
191
|
warnUnsupportedSvgElements(vnodeTree, ctx.options.component);
|
|
175
192
|
await ctx.timings.measure("vnode-walk", () => Promise.all(walkTree(ctx, vnodeTree, [encoding, styleDirectives, imageSrc])));
|
|
193
|
+
omitImagesWithoutSource(vnodeTree);
|
|
176
194
|
return vnodeTree;
|
|
177
195
|
}
|
|
@@ -7,7 +7,7 @@ export interface LoadFontsOptions {
|
|
|
7
7
|
/**
|
|
8
8
|
* Font formats the renderer can parse.
|
|
9
9
|
* Satori: ttf, otf, woff
|
|
10
|
-
* Takumi: ttf, woff2
|
|
10
|
+
* Takumi: ttf, otf, woff, woff2
|
|
11
11
|
*/
|
|
12
12
|
supportedFormats: Set<FontFormat>;
|
|
13
13
|
/** Component pascalName — filters fonts to only what this component needs */
|
|
@@ -14,7 +14,7 @@ async function loadFont(event, font, src) {
|
|
|
14
14
|
const cached = fontCache.get(cacheKey);
|
|
15
15
|
if (cached)
|
|
16
16
|
return cached;
|
|
17
|
-
const data = await resolve(event, { ...font, src }).catch((err) => {
|
|
17
|
+
const data = await resolve(event, { ...font, src, absolutePath: src === font.src ? font.absolutePath : void 0 }).catch((err) => {
|
|
18
18
|
logger.warn(`Failed to load font ${font.family}: ${err.message}`);
|
|
19
19
|
return null;
|
|
20
20
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveSvgFilters(svg: string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function resolveSvgFilters(svg) {
|
|
2
|
+
return svg.replace(/<feColorMatrix\s[^>]*type="saturate"\s+values="([^"]+)"[^>]*>/g, (filter, value) => {
|
|
3
|
+
const saturation = Number(value);
|
|
4
|
+
if (!Number.isFinite(saturation) || saturation <= 1)
|
|
5
|
+
return filter;
|
|
6
|
+
const matrix = [
|
|
7
|
+
[0.213 + 0.787 * saturation, 0.715 - 0.715 * saturation, 0.072 - 0.072 * saturation, 0, 0],
|
|
8
|
+
[0.213 - 0.213 * saturation, 0.715 + 0.285 * saturation, 0.072 - 0.072 * saturation, 0, 0],
|
|
9
|
+
[0.213 - 0.213 * saturation, 0.715 - 0.715 * saturation, 0.072 + 0.928 * saturation, 0, 0],
|
|
10
|
+
[0, 0, 0, 1, 0]
|
|
11
|
+
].flat();
|
|
12
|
+
return filter.replace(`type="saturate" values="${value}"`, `type="matrix" values="${matrix.join(" ")}"`);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type _satori from 'satori';
|
|
2
1
|
export declare function getResvg(): Promise<any>;
|
|
3
|
-
export declare function getSatori(): Promise<
|
|
2
|
+
export declare function getSatori(): Promise<(element: ReactNode, options: import("satori").SatoriOptions) => Promise<string>>;
|
|
4
3
|
export declare function getSharp(): Promise<import("sharp").SharpConstructor>;
|
|
@@ -7,12 +7,5 @@ export default defineTransformer([
|
|
|
7
7
|
node.props.tw = node.props.class;
|
|
8
8
|
node.props.tw = node.props.tw.replace(RE_ICON_CLASSES, "");
|
|
9
9
|
}
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
filter: (node) => !!node.props?.style?.display,
|
|
13
|
-
transform(node) {
|
|
14
|
-
if (["inline-block", "inline"].includes(node.props.style.display))
|
|
15
|
-
delete node.props.style.display;
|
|
16
|
-
}
|
|
17
10
|
}
|
|
18
11
|
]);
|
|
@@ -4,6 +4,7 @@ import compatibility from "#og-image/compatibility";
|
|
|
4
4
|
import { withTimeout } from "../../util/withTimeout.js";
|
|
5
5
|
import { useOgImageRuntimeConfig } from "../../utils.js";
|
|
6
6
|
import { buildSubsetFamilyChain, extractCodepoints, getDefaultFontFamily, loadAllFontsDebug, loadFontsForRenderer, resolveSubsetChain } from "../fonts.js";
|
|
7
|
+
import { resolveSvgFilters } from "./filters.js";
|
|
7
8
|
import { getResvg, getSatori, getSharp } from "./instances.js";
|
|
8
9
|
import { createVNodes } from "./vnodes.js";
|
|
9
10
|
const _satoriFontCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -89,7 +90,7 @@ export async function createSvg(event) {
|
|
|
89
90
|
const { result, warnings } = await timings.measure("render-satori", () => withWarningCapture(
|
|
90
91
|
() => satori(vnodes, satoriOptions)
|
|
91
92
|
));
|
|
92
|
-
return { svg: result, warnings, fonts };
|
|
93
|
+
return { svg: result && resolveSvgFilters(result), warnings, fonts };
|
|
93
94
|
}
|
|
94
95
|
async function createPng(event) {
|
|
95
96
|
const { resvgOptions } = useOgImageRuntimeConfig();
|
|
@@ -2,8 +2,6 @@ import resolvedFonts from "#og-image/fonts";
|
|
|
2
2
|
import { walkTree } from "../core/plugins.js";
|
|
3
3
|
import { createVNodes as coreCreateVNodes, resolveSvgDimension } from "../core/vnodes.js";
|
|
4
4
|
import classes from "./plugins/classes.js";
|
|
5
|
-
import emojis from "./plugins/emojis.js";
|
|
6
|
-
import flex from "./plugins/flex.js";
|
|
7
5
|
import nuxtIcon from "./plugins/nuxt-icon.js";
|
|
8
6
|
export { htmlToVNode, resolveSvgDimension, SVG_CAMEL_ATTR_VALUES, warnUnsupportedSvgElements } from "../core/vnodes.js";
|
|
9
7
|
function getDefaultFontFamily() {
|
|
@@ -50,8 +48,7 @@ export async function createVNodes(ctx) {
|
|
|
50
48
|
const fontFamilyOverride = ctx.options.props?.fontFamily;
|
|
51
49
|
const font = fontFamilyOverride ? `'${fontFamilyOverride}', sans-serif` : getDefaultFontFamily();
|
|
52
50
|
const vnodes = await coreCreateVNodes(ctx, { wrapperStyle: `font-family: ${font};` });
|
|
53
|
-
walkTree(ctx, vnodes, [classes,
|
|
54
|
-
await Promise.all(walkTree(ctx, vnodes, [emojis]));
|
|
51
|
+
walkTree(ctx, vnodes, [classes, nuxtIcon]);
|
|
55
52
|
resolveSvgPercentDimensions(vnodes);
|
|
56
53
|
return vnodes;
|
|
57
54
|
}
|