nuxt-og-image 6.9.1 → 6.10.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 CHANGED
@@ -7,6 +7,14 @@
7
7
  [![License][license-src]][license-href]
8
8
  [![Nuxt][nuxt-src]][nuxt-href]
9
9
 
10
+ <a href="https://skilld.dev/gh/nuxt-modules/og-image">
11
+ <picture>
12
+ <source media="(prefers-color-scheme: dark)" srcset="https://skilld.dev/b/nuxt-modules/og-image?theme=dark">
13
+ <source media="(prefers-color-scheme: light)" srcset="https://skilld.dev/b/nuxt-modules/og-image?theme=light">
14
+ <img alt="Skill repository on skilld.dev" src="https://skilld.dev/b/nuxt-modules/og-image?theme=light">
15
+ </picture>
16
+ </a>
17
+
10
18
  Nuxt OG Image allows you to easily generate OG Images using either Vue components or screenshots of your pages.
11
19
 
12
20
  OG Images are known to improve click-through rates on social media.
@@ -42,10 +50,7 @@ npx nuxi@latest module add og-image
42
50
  ```
43
51
 
44
52
  > [!TIP]
45
- > Generate an Agent Skill for this package using [skilld](https://github.com/harlan-zw/skilld):
46
- > ```bash
47
- > npx skilld add nuxt-og-image
48
- > ```
53
+ > Using an AI agent? Get the nuxt-og-image Skill on [skilld.dev/gh/nuxt-modules/og-image](https://skilld.dev/gh/nuxt-modules/og-image).
49
54
 
50
55
  💡 Built your OG images? Check how they render across Twitter, Facebook, LinkedIn and Slack with the free [Social Share Debugger](https://nuxtseo.com/tools/social-share-debugger), or monitor them site-wide with [Nuxt SEO Pro](https://nuxtseo.com/pro).
51
56
 
@@ -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.CNS1IVAi.mjs';
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.Bcegsloy.mjs';
6
6
  import 'node:crypto';
7
7
  import 'node:fs';
8
8
  import '@nuxt/kit';
@@ -11,18 +11,17 @@ import 'fnv1a-64';
11
11
  import 'nuxt-site-config/kit';
12
12
  import 'nuxtseo-shared/kit';
13
13
  import 'pkg-types';
14
- import 'std-env';
15
14
  import 'magic-string';
16
15
  import 'oxc-walker';
17
16
  import '../../dist/runtime/logger.js';
18
17
  import 'chrome-launcher';
18
+ import 'std-env';
19
19
  import 'nypm';
20
20
  import 'nuxtseo-shared/devtools';
21
21
  import 'unstorage';
22
22
  import 'unstorage/drivers/fs-lite';
23
23
  import '../../dist/runtime/server/og-image/utils/css.js';
24
24
  import 'strip-literal';
25
- import 'ufo';
26
25
  import 'unplugin';
27
26
  import '@vue/compiler-sfc';
28
27
  import 'ofetch';
@@ -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.CNS1IVAi.mjs';
5
- export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.CNS1IVAi.mjs';
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.Bcegsloy.mjs';
5
+ export { j as importResolvedModule, k as resolveOptionalModulePath } from '../shared/nuxt-og-image.Bcegsloy.mjs';
6
6
  import 'node:crypto';
7
7
  import 'node:fs';
8
8
  import 'node:url';
@@ -12,18 +12,17 @@ import 'nuxt-site-config/kit';
12
12
  import 'nuxtseo-shared/kit';
13
13
  import 'pathe';
14
14
  import 'pkg-types';
15
- import 'std-env';
16
15
  import 'magic-string';
17
16
  import 'oxc-walker';
18
17
  import 'exsolve';
19
18
  import 'chrome-launcher';
19
+ import 'std-env';
20
20
  import 'nypm';
21
21
  import 'nuxtseo-shared/devtools';
22
22
  import 'unstorage';
23
23
  import 'unstorage/drivers/fs-lite';
24
24
  import '../../dist/runtime/server/og-image/utils/css.js';
25
25
  import 'strip-literal';
26
- import 'ufo';
27
26
  import 'unplugin';
28
27
  import '@vue/compiler-sfc';
29
28
  import 'ofetch';
package/dist/cli.mjs CHANGED
@@ -13,6 +13,67 @@ import 'magicast';
13
13
  import 'magicast/helpers';
14
14
  import '../dist/runtime/logger.js';
15
15
 
16
+ const RENDERER_PREFERENCE = ["takumi", "satori", "browser"];
17
+ const RE_TEMPLATE_FILE = /^(.+)\.(takumi|satori|browser)\.vue$/;
18
+ const RENDERER_PACKAGES = {
19
+ takumi: ["@takumi-rs/core", "@takumi-rs/wasm"],
20
+ satori: ["satori"],
21
+ browser: ["playwright-core"]
22
+ };
23
+ function parseTemplateFile(file) {
24
+ const match = RE_TEMPLATE_FILE.exec(file);
25
+ if (!match)
26
+ return null;
27
+ return { baseName: match[1], renderer: match[2] };
28
+ }
29
+ function listEjectableNames(templateFiles) {
30
+ const names = /* @__PURE__ */ new Set();
31
+ for (const file of templateFiles) {
32
+ const parsed = parseTemplateFile(file);
33
+ if (!parsed)
34
+ continue;
35
+ names.add(parsed.baseName);
36
+ names.add(`${parsed.baseName}.${parsed.renderer}`);
37
+ }
38
+ return [...names].sort();
39
+ }
40
+ function resolveEjectTemplate(name, ctx) {
41
+ const requested = name.replace(/\.vue$/, "");
42
+ const variants = ctx.templateFiles.map((file) => ({ file, parsed: parseTemplateFile(file) })).filter((v) => !!v.parsed);
43
+ const exact = variants.find((v) => `${v.parsed.baseName}.${v.parsed.renderer}` === requested);
44
+ if (exact)
45
+ return { _tag: "Ok", file: exact.file, renderer: exact.parsed.renderer, reason: "exact" };
46
+ const candidates = variants.filter((v) => v.parsed.baseName === requested);
47
+ if (candidates.length === 0)
48
+ return { _tag: "NotFound", name, available: listEjectableNames(ctx.templateFiles) };
49
+ if (candidates.length === 1)
50
+ return { _tag: "Ok", file: candidates[0].file, renderer: candidates[0].parsed.renderer, reason: "only-variant" };
51
+ const pick = (renderers, reason) => {
52
+ for (const renderer of RENDERER_PREFERENCE) {
53
+ if (!renderers.includes(renderer))
54
+ continue;
55
+ const match = candidates.find((c) => c.parsed.renderer === renderer);
56
+ if (match)
57
+ return { _tag: "Ok", file: match.file, renderer, reason };
58
+ }
59
+ return null;
60
+ };
61
+ return pick(ctx.appRenderers, "app-templates") ?? pick(ctx.installedRenderers, "installed") ?? pick(RENDERER_PREFERENCE, "default");
62
+ }
63
+ function detectInstalledRenderers(pkg) {
64
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
65
+ return RENDERER_PREFERENCE.filter((r) => RENDERER_PACKAGES[r].some((p) => p in deps));
66
+ }
67
+ function detectAppRenderers(files) {
68
+ const found = /* @__PURE__ */ new Set();
69
+ for (const file of files) {
70
+ const parsed = parseTemplateFile(file.split("/").pop() || file);
71
+ if (parsed)
72
+ found.add(parsed.renderer);
73
+ }
74
+ return RENDERER_PREFERENCE.filter((r) => found.has(r));
75
+ }
76
+
16
77
  const __dirname$1 = dirname(fileURLToPath(import.meta.url));
17
78
  const communityDir = resolve(__dirname$1, "runtime/app/components/Templates/Community");
18
79
  const RE_RENDERER_SUFFIX = /\.(satori|browser|takumi)\.vue$/;
@@ -195,28 +256,30 @@ function listTemplates() {
195
256
  const templates = [...new Set(
196
257
  readdirSync(communityDir).filter((f) => f.endsWith(".vue")).map(getBaseName)
197
258
  )];
259
+ const variants = listEjectableNames(readdirSync(communityDir)).filter((n) => n.includes("."));
198
260
  p.intro("Community Templates");
199
261
  p.note(templates.map((t) => `\u2022 ${t}`).join("\n"), "Available");
200
262
  p.log.info("Usage: npx nuxt-og-image eject <template-name>");
263
+ p.log.info(`To pick a renderer, add its suffix: ${variants.slice(0, 2).join(", ")}`);
201
264
  p.outro("");
202
265
  }
203
- function findTemplateFile(name) {
204
- const files = readdirSync(communityDir).filter((f) => f.endsWith(".vue"));
205
- const match = files.find((f) => getBaseName(f) === name);
206
- return match || null;
207
- }
208
- function ejectTemplate(name, targetDir) {
209
- const templateFile = findTemplateFile(name);
210
- if (!templateFile) {
266
+ function ejectTemplate(name, targetDir, cwd = process.cwd()) {
267
+ const appComponents = [.../* @__PURE__ */ new Set([cwd, targetDir])].flatMap(findOgImageComponents);
268
+ const resolution = resolveEjectTemplate(name, {
269
+ templateFiles: readdirSync(communityDir).filter((f) => f.endsWith(".vue")),
270
+ appRenderers: detectAppRenderers(appComponents),
271
+ installedRenderers: detectInstalledRenderers(readPackageJson(cwd))
272
+ });
273
+ if (resolution._tag === "NotFound") {
211
274
  p.log.error(`Template "${name}" not found.`);
212
275
  listTemplates();
213
276
  process.exit(1);
214
277
  }
215
- const templatePath = join(communityDir, templateFile);
278
+ const templatePath = join(communityDir, resolution.file);
216
279
  const outputDir = resolve(targetDir, "components", "OgImage");
217
280
  if (!existsSync(outputDir))
218
281
  mkdirSync(outputDir, { recursive: true });
219
- const outputPath = join(outputDir, templateFile);
282
+ const outputPath = join(outputDir, resolution.file);
220
283
  if (existsSync(outputPath)) {
221
284
  p.log.error(`File already exists: ${outputPath}`);
222
285
  process.exit(1);
@@ -224,6 +287,17 @@ function ejectTemplate(name, targetDir) {
224
287
  const content = readFileSync(templatePath, "utf-8");
225
288
  writeFileSync(outputPath, content, "utf-8");
226
289
  p.log.success(`Ejected "${name}" to ${outputPath}`);
290
+ const why = {
291
+ "exact": null,
292
+ "only-variant": null,
293
+ "app-templates": `your templates use ${resolution.renderer}`,
294
+ "installed": `${resolution.renderer} is in package.json`,
295
+ "default": `${resolution.renderer} is the default renderer`
296
+ }[resolution.reason];
297
+ if (why) {
298
+ const others = listEjectableNames(readdirSync(communityDir)).filter((n) => n.startsWith(`${getBaseName(resolution.file)}.`) && n !== resolution.file.replace(".vue", ""));
299
+ p.log.info(`Picked the ${resolution.renderer} variant because ${why}.${others.length ? ` For another renderer, run: npx nuxt-og-image eject ${others[0]}` : ""}`);
300
+ }
227
301
  }
228
302
  function findOgImageComponents(dir) {
229
303
  const components = [];
@@ -445,11 +519,19 @@ function collectTemplateReplacements(templateHtml) {
445
519
  if (el.name !== "OgImage" && el.name !== "OgImageScreenshot")
446
520
  return;
447
521
  const isScreenshot = el.name === "OgImageScreenshot";
448
- const composable = isScreenshot ? "defineOgImageScreenshot" : "defineOgImage";
449
- const propsStr = attrsToProps(el.attributes);
450
522
  const start = el.loc[0].start;
451
523
  const end = el.loc[1].end;
452
- const text = propsStr ? `<!-- Migrated: use ${composable}(${propsStr}) in <script setup> -->` : `<!-- Migrated: use ${composable}() in <script setup> -->`;
524
+ let call;
525
+ if (isScreenshot) {
526
+ const propsStr = attrsToProps(el.attributes);
527
+ call = `defineOgImageScreenshot(${propsStr})`;
528
+ } else {
529
+ const { component, ":component": dynamicComponent, ...props } = el.attributes;
530
+ const componentArg = dynamicComponent || (component ? `'${component}'` : `'NuxtSeo'`);
531
+ const propsStr = attrsToProps(props);
532
+ call = `defineOgImage(${componentArg}${propsStr ? `, ${propsStr}` : ""})`;
533
+ }
534
+ const text = `<!-- Migrated: use ${call} in <script setup> -->`;
453
535
  replacements.push({ start, end, text });
454
536
  });
455
537
  return replacements;
@@ -755,7 +837,7 @@ async function runMigrate(args2) {
755
837
  if (shouldEject) {
756
838
  const targetDir = existsSync(join(cwd, "app")) ? join(cwd, "app") : cwd;
757
839
  for (const name of migrationCheck.usedCommunityTemplates) {
758
- ejectTemplate(name, targetDir);
840
+ ejectTemplate(name, targetDir, cwd);
759
841
  }
760
842
  } else {
761
843
  p.log.info("Run manually before building for production:");
@@ -1273,6 +1355,7 @@ function showHelp() {
1273
1355
  " Options: --renderer <renderer>, --path <dir>",
1274
1356
  "list List available community templates",
1275
1357
  "eject <name> Eject a community template to your project",
1358
+ " Picks the variant for your renderer; add .takumi or .satori to choose",
1276
1359
  "switch Switch components between renderers",
1277
1360
  " Options: --from <renderer>, --to <renderer>, --dry-run, --yes",
1278
1361
  "enable <renderer> Install dependencies for a renderer (satori, browser, takumi)",
@@ -1299,7 +1382,7 @@ if (hasHelp || !command) {
1299
1382
  }
1300
1383
  const cwd = process.cwd();
1301
1384
  const targetDir = existsSync(join(cwd, "app")) ? join(cwd, "app") : cwd;
1302
- ejectTemplate(templateName, targetDir);
1385
+ ejectTemplate(templateName, targetDir, cwd);
1303
1386
  } else if (command === "list") {
1304
1387
  listTemplates();
1305
1388
  } else if (command === "migrate") {
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "nuxt": ">=3.16.0"
5
5
  },
6
6
  "configKey": "ogImage",
7
- "version": "6.9.1",
7
+ "version": "6.10.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -9,20 +9,19 @@ import 'nuxt-site-config/kit';
9
9
  import 'nuxtseo-shared/kit';
10
10
  import 'pathe';
11
11
  import 'pkg-types';
12
- import 'std-env';
13
- export { n as default } from './shared/nuxt-og-image.CNS1IVAi.mjs';
12
+ export { n as default } from './shared/nuxt-og-image.Bcegsloy.mjs';
14
13
  import '../dist/runtime/logger.js';
15
14
  import 'magic-string';
16
15
  import 'oxc-walker';
17
16
  import 'exsolve';
18
17
  import 'chrome-launcher';
18
+ import 'std-env';
19
19
  import 'nypm';
20
20
  import 'nuxtseo-shared/devtools';
21
21
  import 'unstorage';
22
22
  import 'unstorage/drivers/fs-lite';
23
23
  import '../dist/runtime/server/og-image/utils/css.js';
24
24
  import 'strip-literal';
25
- import 'ufo';
26
25
  import 'unplugin';
27
26
  import '@vue/compiler-sfc';
28
27
  import 'ofetch';
@@ -3,6 +3,7 @@ import { injectHead, useHead, useRuntimeConfig } from "nuxt/app";
3
3
  import { joinURL, withQuery } from "ufo";
4
4
  import { toValue } from "vue";
5
5
  import { componentNames } from "#build/nuxt-og-image/components.mjs";
6
+ import { createSitePathResolver } from "#site-config/app/composables/utils";
6
7
  import { buildOgImageUrl, generateMeta, separateProps } from "../shared.js";
7
8
  const clientEntriesByHead = /* @__PURE__ */ new WeakMap();
8
9
  function registerClientOgHead(ogKey, input, options) {
@@ -61,6 +62,7 @@ export function clientProcessOgImageOptions(input, route, basePath) {
61
62
  const inputs = Array.isArray(input) ? input : [input];
62
63
  const rc = useRuntimeConfig();
63
64
  const baseURL = rc.app.baseURL;
65
+ const resolveImageUrl = createSitePathResolver({ absolute: true, withBase: true, canonical: false });
64
66
  const publicCfg = rc.public?.["nuxt-og-image"] || {};
65
67
  const defaults = publicCfg.defaults || {};
66
68
  const metaOptions = { includeTwitter: publicCfg.includeTwitter };
@@ -79,13 +81,14 @@ export function clientProcessOgImageOptions(input, route, basePath) {
79
81
  const ogKey = validOptions.key || "og";
80
82
  if (validOptions.url) {
81
83
  const url = validOptions.url;
82
- registerClientOgHead(ogKey, { meta: generateMeta(url, validOptions, metaOptions) }, { tagPriority: "high" });
84
+ const absoluteUrl = /^https?:\/\//.test(url) ? url : toValue(resolveImageUrl(url));
85
+ registerClientOgHead(ogKey, { meta: generateMeta(absoluteUrl, validOptions, metaOptions) }, { tagPriority: "high" });
83
86
  paths.push(url);
84
87
  continue;
85
88
  }
86
89
  if (publicCfg.hasServerRuntime) {
87
90
  const finalUrl2 = buildResolverUrl(baseURL, basePath, ogKey, route.query);
88
- registerClientOgHead(ogKey, { meta: generateMeta(finalUrl2, validOptions, metaOptions) }, { tagPriority: 35 });
91
+ registerClientOgHead(ogKey, { meta: generateMeta(toValue(resolveImageUrl(finalUrl2)), validOptions, metaOptions) }, { tagPriority: "high" });
89
92
  paths.push(finalUrl2);
90
93
  continue;
91
94
  }
@@ -99,7 +102,7 @@ export function clientProcessOgImageOptions(input, route, basePath) {
99
102
  const result = buildOgImageUrl(urlOpts, extension, true, defaults, void 0);
100
103
  const resolvedUrl = joinURL("/", baseURL, result.url);
101
104
  const finalUrl = opts._query && Object.keys(opts._query).length ? withQuery(resolvedUrl, { _query: opts._query }) : resolvedUrl;
102
- registerClientOgHead(ogKey, { meta: generateMeta(finalUrl, opts, metaOptions) }, { processTemplateParams: true, tagPriority: 35 });
105
+ registerClientOgHead(ogKey, { meta: generateMeta(toValue(resolveImageUrl(finalUrl)), opts, metaOptions) }, { processTemplateParams: true, tagPriority: "high" });
103
106
  paths.push(finalUrl);
104
107
  }
105
108
  return paths;
@@ -0,0 +1,9 @@
1
+ import { defineOgImage as _defineOgImage } from './defineOgImage.js';
2
+ import { defineOgImageComponent as _defineOgImageComponent } from './defineOgImageComponent.js';
3
+ import { defineOgImageScreenshot as _defineOgImageScreenshot } from './defineOgImageScreenshot.js';
4
+ export declare const defineOgImage: typeof _defineOgImage;
5
+ /**
6
+ * @deprecated Use `defineOgImage()` instead.
7
+ */
8
+ export declare const defineOgImageComponent: typeof _defineOgImageComponent;
9
+ export declare const defineOgImageScreenshot: typeof _defineOgImageScreenshot;
@@ -0,0 +1,6 @@
1
+ import { defineOgImage as _defineOgImage } from "./defineOgImage.js";
2
+ import { defineOgImageComponent as _defineOgImageComponent } from "./defineOgImageComponent.js";
3
+ import { defineOgImageScreenshot as _defineOgImageScreenshot } from "./defineOgImageScreenshot.js";
4
+ export const defineOgImage = (...args) => import.meta.prerender ? _defineOgImage(...args) : [];
5
+ export const defineOgImageComponent = (...args) => import.meta.prerender ? _defineOgImageComponent(...args) : [];
6
+ export const defineOgImageScreenshot = (...args) => import.meta.prerender ? _defineOgImageScreenshot(...args) : [];
@@ -32,13 +32,13 @@ export const ogImageCanonicalUrls = defineNuxtPlugin((nuxtApp) => {
32
32
  break;
33
33
  }
34
34
  for (const tag of ctx2.tags) {
35
- if (tag.tag === "meta" && (tag.props.property === "og:image" || ["twitter:image:src", "twitter:image"].includes(tag.props.name || ""))) {
35
+ if (tag.tag === "meta" && (tag.props.property === "og:image" || tag.props.name === "twitter:image")) {
36
36
  if (!tag.props.content) {
37
37
  tag.props = {};
38
38
  continue;
39
39
  }
40
40
  tag.props.content = tag.props.content.replaceAll("%title", title).replaceAll("%description", description).replaceAll(" ", "+");
41
- if (!tag.props.content?.startsWith("https")) {
41
+ if (!/^https?:\/\//.test(tag.props.content || "")) {
42
42
  nuxtApp.runWithContext(() => {
43
43
  tag.props.content = toValue(withSiteUrl(tag.props.content || "", {
44
44
  withBase: true,
@@ -4,6 +4,7 @@ import { useHead, useRequestEvent, useRuntimeConfig } from "nuxt/app";
4
4
  import { joinURL, withQuery } from "ufo";
5
5
  import { isRef, toValue } from "vue";
6
6
  import { componentNames } from "#build/nuxt-og-image/components.mjs";
7
+ import { createSitePathResolver, withSiteUrl } from "#site-config/app/composables/utils";
7
8
  import { buildOgImageUrl, generateMeta, separateProps } from "../shared.js";
8
9
  function resolveUnrefHeadInput(input) {
9
10
  if (input == null)
@@ -54,7 +55,9 @@ export function setHeadOgImagePrebuilt(input) {
54
55
  if (!url)
55
56
  return;
56
57
  const { includeTwitter } = useOgImageRuntimeConfig();
57
- const meta = generateMeta(url, input, { includeTwitter });
58
+ const urlValue = toValue(url);
59
+ const absoluteUrl = /^https?:\/\//.test(urlValue) ? urlValue : toValue(withSiteUrl(urlValue, { withBase: true, canonical: !import.meta.dev }));
60
+ const meta = generateMeta(absoluteUrl, input, { includeTwitter });
58
61
  useHead({ meta }, { tagPriority: "high" });
59
62
  }
60
63
  export function createOgImageMeta(src, input, ssrContext, pagePath, head) {
@@ -77,6 +80,7 @@ export function createOgImageMeta(src, input, ssrContext, pagePath, head) {
77
80
  payloads[currentPayloadIdx] = [resolvedOptions.key, _input, basePath];
78
81
  }
79
82
  const baseURL = useRuntimeConfig().app.baseURL;
83
+ const resolveImageUrl = createSitePathResolver({ absolute: true, withBase: true, canonical: !import.meta.dev });
80
84
  ssrContext._ogImageInstance?.dispose();
81
85
  ssrContext._ogImageInstance = useHead({
82
86
  // Meta is generated lazily so that title/description from useSeoMeta / useHead
@@ -118,12 +122,12 @@ export function createOgImageMeta(src, input, ssrContext, pagePath, head) {
118
122
  prerenderPaths.set(ogKey, (finalUrl.split("?")[0] || finalUrl).replace(/,/g, "%2C"));
119
123
  }
120
124
  }
121
- return generateMeta(finalUrl, opts, { includeTwitter: ogImageConfig.includeTwitter });
125
+ return generateMeta(toValue(resolveImageUrl(finalUrl)), opts, { includeTwitter: ogImageConfig.includeTwitter });
122
126
  });
123
127
  }
124
128
  }, {
125
129
  processTemplateParams: true,
126
- tagPriority: 35
130
+ tagPriority: "high"
127
131
  });
128
132
  if (import.meta.dev || import.meta.prerender) {
129
133
  ssrContext._ogImageDevtoolsInstance?.dispose();
@@ -184,9 +188,12 @@ export function resolveComponentName(component) {
184
188
  const originalName = component;
185
189
  const normalizedName = originalName.split(".").map((s, i) => i === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1)).join("");
186
190
  const inputBase = normalizedName.replace(RE_RENDERER_SUFFIX, "");
191
+ const requestedRenderer = normalizedName.match(RE_RENDERER_SUFFIX)?.[1]?.toLowerCase();
187
192
  for (const component2 of componentNames) {
188
193
  if (component2.pascalName === normalizedName)
189
194
  return component2.pascalName;
195
+ if (requestedRenderer && component2.renderer !== requestedRenderer)
196
+ continue;
190
197
  const basePascalName = component2.pascalName.replace(RE_RENDERER_SUFFIX, "");
191
198
  if (basePascalName === originalName || basePascalName === inputBase)
192
199
  return component2.pascalName;
@@ -1,2 +1 @@
1
1
  export declare const logger: import("consola").ConsolaInstance;
2
- export declare const gray: (s: string) => string;
@@ -1,4 +1,2 @@
1
- import { colorize } from "consola/utils";
2
1
  import { createModuleLogger } from "nuxtseo-shared/utils";
3
2
  export const logger = createModuleLogger("@nuxtjs/og-image");
4
- export const gray = (s) => colorize("gray", s);
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { join } from "pathe";
2
+ import { basename, join } from "pathe";
3
3
  import { withBase } from "ufo";
4
4
  import { getRequestURL } from "#nuxtseo/h3";
5
5
  import { fetchWithEvent, useRuntimeConfig } from "#nuxtseo/nitro";
@@ -40,6 +40,14 @@ export async function resolve(event, font) {
40
40
  }
41
41
  if (path && (isDataFontUrl(path) || isExternalFontUrl(path)))
42
42
  return fetchSpecialFontUrl(path, getSiteConfig(event).url, timeout);
43
+ if ((import.meta.dev || import.meta.prerender) && path.startsWith("/_fonts/")) {
44
+ const filename = path.slice("/_fonts/".length);
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
+ }
43
51
  if (import.meta.prerender) {
44
52
  if (path.startsWith("/_og-static-fonts/")) {
45
53
  const filename2 = path.slice("/_og-static-fonts/".length);
@@ -9,7 +9,6 @@ import { createSitePathResolver } from "#site-config/server/composables/utils";
9
9
  import { logger } from "../../logger.js";
10
10
  import { decodeOgImageParams, extractEncodedSegment, sanitizeProps, separateProps, verifyOgImageSignature } from "../../shared.js";
11
11
  import { hashKey } from "../../shared/hash.js";
12
- import { autoEjectCommunityTemplate } from "../util/auto-eject.js";
13
12
  import { createNitroRouteRuleMatcher } from "../util/kit.js";
14
13
  import { normaliseOptions } from "../util/options.js";
15
14
  import { getEventQuery } from "../util/query.js";
@@ -174,8 +173,6 @@ export async function resolveContext(e) {
174
173
  }
175
174
  normalised.options.props = filtered;
176
175
  }
177
- if (normalised.component?.category === "community")
178
- autoEjectCommunityTemplate(normalised.component, runtimeConfig, { requestPath: e.path });
179
176
  const rendererType = normalised.renderer;
180
177
  const baseCacheKey = normalised.options.cacheKey || (hashMatch ? `hash:${hashMatch[1]}` : resolvePathCacheKey(e, basePathWithQuery, normalised.options));
181
178
  const key = componentHash ? `${baseCacheKey}:${componentHash}` : baseCacheKey;
@@ -1,4 +1,4 @@
1
1
  import type { Node } from '@takumi-rs/helpers';
2
2
  import type { OgImageRenderEventContext, VNode } from '../../../types.js';
3
3
  export declare function createTakumiNodes(ctx: OgImageRenderEventContext): Promise<Node>;
4
- export declare function vnodeToTakumiNode(vnode: VNode, inheritedFontSize: number, inheritedColor?: string): Promise<Node>;
4
+ export declare function vnodeToTakumiNode(vnode: VNode, inheritedColor?: string): Promise<Node>;
@@ -1,8 +1,7 @@
1
1
  import { createVNodes, resolveSvgDimension, SVG_CAMEL_ATTR_VALUES } from "../core/vnodes.js";
2
- const RE_RELATIVE_UNIT = /^([\d.]+)(em|rem)$/;
3
- const RE_TW_TEXT_ARBITRARY = /(?:^|\s)text-\[(\d+(?:\.\d+)?)(px|rem|em)\]/;
4
- const RE_FONT_SIZE_PX = /^(\d+(?:\.\d+)?)(px)?$/;
5
- const DEFAULT_FONT_SIZE = 16;
2
+ const RE_RELATIVE_UNIT = /^[\d.]+r?em$/;
3
+ const RE_LEADING_SPACE = /^\s/;
4
+ const RE_TRAILING_SPACE = /\s$/;
6
5
  const RE_UPPERCASE = /[A-Z]/g;
7
6
  const RE_DQUOTE = /"/g;
8
7
  const RE_AMP = /&/g;
@@ -10,7 +9,7 @@ const RE_LT = /</g;
10
9
  const RE_GT = />/g;
11
10
  export async function createTakumiNodes(ctx) {
12
11
  const vnodeTree = await createVNodes(ctx);
13
- return await ctx.timings.measure("takumi-nodes", () => vnodeToTakumiNode(vnodeTree, DEFAULT_FONT_SIZE));
12
+ return await ctx.timings.measure("takumi-nodes", () => vnodeToTakumiNode(vnodeTree));
14
13
  }
15
14
  function pickNumericDimension(props, key) {
16
15
  const v = props[key];
@@ -19,35 +18,11 @@ function pickNumericDimension(props, key) {
19
18
  const n = Number(v);
20
19
  return Number.isNaN(n) ? void 0 : n;
21
20
  }
22
- function resolveRelativeUnit(value, inheritedFontSize) {
23
- if (value == null)
24
- return void 0;
25
- const match = String(value).match(RE_RELATIVE_UNIT);
26
- if (!match)
27
- return void 0;
28
- const n = Number.parseFloat(match[1]);
29
- return match[2] === "rem" ? n * DEFAULT_FONT_SIZE : n * inheritedFontSize;
30
- }
31
- function extractFontSize(props, style) {
32
- if (style?.fontSize != null) {
33
- const m = String(style.fontSize).match(RE_FONT_SIZE_PX);
34
- if (m)
35
- return Number.parseFloat(m[1]);
36
- }
37
- const twStr = props.tw || props.class || "";
38
- const twMatch = twStr.match(RE_TW_TEXT_ARBITRARY);
39
- if (twMatch) {
40
- const val = Number.parseFloat(twMatch[1]);
41
- if (twMatch[2] === "px")
42
- return val;
43
- }
44
- return void 0;
45
- }
46
21
  function extractColor(style) {
47
22
  const color = style?.color;
48
23
  return typeof color === "string" && color ? color : void 0;
49
24
  }
50
- export async function vnodeToTakumiNode(vnode, inheritedFontSize, inheritedColor) {
25
+ export async function vnodeToTakumiNode(vnode, inheritedColor) {
51
26
  const { style, children, class: cls, tw, src, ...rest } = vnode.props;
52
27
  const nodeColor = extractColor(style) ?? inheritedColor;
53
28
  const baseMetadata = {
@@ -55,18 +30,16 @@ export async function vnodeToTakumiNode(vnode, inheritedFontSize, inheritedColor
55
30
  style
56
31
  };
57
32
  if (vnode.type === "svg") {
58
- const hasExplicitFontSize = inheritedFontSize !== DEFAULT_FONT_SIZE;
59
- const isRelativeW = RE_RELATIVE_UNIT.test(String(rest.width ?? ""));
60
- const isRelativeH = RE_RELATIVE_UNIT.test(String(rest.height ?? ""));
33
+ const relativeW = RE_RELATIVE_UNIT.test(String(rest.width ?? "")) ? String(rest.width) : void 0;
34
+ const relativeH = RE_RELATIVE_UNIT.test(String(rest.height ?? "")) ? String(rest.height) : void 0;
35
+ const imageStyle = relativeW || relativeH ? { ...style, ...relativeW && style?.width == null ? { width: relativeW } : {}, ...relativeH && style?.height == null ? { height: relativeH } : {} } : style;
61
36
  return {
62
37
  ...baseMetadata,
38
+ style: imageStyle,
63
39
  type: "image",
64
40
  src: vnodeToHtmlString(vnode, nodeColor),
65
- // When em/rem + explicit parent font size → resolve to px.
66
- // When em/rem + default font size → leave undefined (takumi handles natively).
67
- // Otherwise → use standard resolution chain (numeric attrs → style → viewBox).
68
- width: isRelativeW ? hasExplicitFontSize ? resolveRelativeUnit(rest.width, inheritedFontSize) : void 0 : resolveSvgDimension(rest, style, "width"),
69
- height: isRelativeH ? hasExplicitFontSize ? resolveRelativeUnit(rest.height, inheritedFontSize) : void 0 : resolveSvgDimension(rest, style, "height")
41
+ width: relativeW ? void 0 : resolveSvgDimension(rest, style, "width"),
42
+ height: relativeH ? void 0 : resolveSvgDimension(rest, style, "height")
70
43
  };
71
44
  }
72
45
  if (vnode.type === "img") {
@@ -78,8 +51,6 @@ export async function vnodeToTakumiNode(vnode, inheritedFontSize, inheritedColor
78
51
  height: pickNumericDimension(rest, "height")
79
52
  };
80
53
  }
81
- const nodeFontSize = extractFontSize(vnode.props, style);
82
- const childFontSize = nodeFontSize ?? inheritedFontSize;
83
54
  const containerStyle = { ...style };
84
55
  const w = pickNumericDimension(rest, "width");
85
56
  const h = pickNumericDimension(rest, "height");
@@ -102,11 +73,15 @@ export async function vnodeToTakumiNode(vnode, inheritedFontSize, inheritedColor
102
73
  }
103
74
  if (Array.isArray(children)) {
104
75
  const takumiChildren = [];
105
- for (const child of children) {
106
- if (child && typeof child === "object")
107
- takumiChildren.push(await vnodeToTakumiNode(child, childFontSize, nodeColor));
108
- else if (typeof child === "string" && child.trim())
109
- takumiChildren.push({ type: "text", text: child.trim() });
76
+ const isInlineImage = (c) => !!c && typeof c === "object" && ["svg", "img"].includes(c.type);
77
+ for (const [i, child] of children.entries()) {
78
+ if (child && typeof child === "object") {
79
+ takumiChildren.push(await vnodeToTakumiNode(child, nodeColor));
80
+ } else if (typeof child === "string" && child.trim()) {
81
+ const start = isInlineImage(children[i - 1]) && RE_LEADING_SPACE.test(child) ? " " : "";
82
+ const end = isInlineImage(children[i + 1]) && RE_TRAILING_SPACE.test(child) ? " " : "";
83
+ takumiChildren.push({ type: "text", text: `${start}${child.trim()}${end}` });
84
+ }
110
85
  }
111
86
  return {
112
87
  ...containerMetadata,
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,36 @@
1
+ import { parse } from "devalue";
2
+ import { defineNitroPlugin } from "#nuxtseo/nitro";
3
+ import { autoEjectCommunityTemplate } from "../util/auto-eject.js";
4
+ import { normaliseOptions } from "../util/options.js";
5
+ import { useOgImageRuntimeConfig } from "../utils.js";
6
+ const PAYLOAD_REGEX = /<script.+id="nuxt-og-image-options"[^>]*>(.+?)<\/script>/;
7
+ export default defineNitroPlugin((nitro) => {
8
+ if (!import.meta.dev)
9
+ return;
10
+ nitro.hooks.hook("render:html", (html, ctx) => {
11
+ const payload = [...html.head, ...html.bodyAppend].join("\n").match(PAYLOAD_REGEX)?.[1];
12
+ if (!payload)
13
+ return;
14
+ for (const options of parse(payload)) {
15
+ if (!options?.component)
16
+ continue;
17
+ const normalised = (() => {
18
+ try {
19
+ return normaliseOptions({ ...options });
20
+ } catch {
21
+ return null;
22
+ }
23
+ })();
24
+ if (normalised?.component?.category === "community")
25
+ (ctx.event.context._ogImageAutoEject ||= []).push(normalised.component);
26
+ }
27
+ });
28
+ nitro.hooks.hook("afterResponse", (event) => {
29
+ const components = event.context._ogImageAutoEject;
30
+ if (!components?.length)
31
+ return;
32
+ const runtimeConfig = useOgImageRuntimeConfig(event);
33
+ for (const component of components)
34
+ autoEjectCommunityTemplate(component, runtimeConfig);
35
+ });
36
+ });