nuxt-seo-utils 8.3.3 → 8.4.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.
Files changed (29) hide show
  1. package/dist/cli.mjs +11 -32
  2. package/dist/devtools/lib/seo-utils/state.ts +1 -1
  3. package/dist/devtools/lib/seo-utils/tools.test.ts +31 -0
  4. package/dist/devtools/lib/seo-utils/tools.ts +42 -3
  5. package/dist/devtools/pages/seo-utils/identity.vue +17 -14
  6. package/dist/module.d.mts +1 -1
  7. package/dist/module.d.ts +1 -1
  8. package/dist/module.json +1 -1
  9. package/dist/module.mjs +181 -110
  10. package/dist/runtime/app/composables/useBreadcrumbItems.js +6 -3
  11. package/dist/runtime/app/logic/colorModeIcons.d.ts +3 -0
  12. package/dist/runtime/app/logic/colorModeIcons.js +10 -0
  13. package/dist/runtime/app/plugins/0.routeRules.js +11 -13
  14. package/dist/runtime/app/plugins/colorModeIcons.d.ts +2 -0
  15. package/dist/runtime/app/plugins/colorModeIcons.js +17 -0
  16. package/dist/runtime/server/middleware/redirectCanonical.d.ts +1 -1
  17. package/dist/runtime/server/middleware/redirectCanonical.js +1 -1
  18. package/dist/runtime/server/middleware/resolveImagesInPagesDir.d.ts +1 -1
  19. package/dist/runtime/server/middleware/resolveImagesInPagesDir.js +1 -1
  20. package/dist/runtime/server/plugins/minifyHtml.d.ts +1 -1
  21. package/dist/runtime/server/plugins/minifyHtml.js +1 -1
  22. package/dist/runtime/server/routes/__nuxt-seo-utils/debug.d.ts +1 -17
  23. package/dist/runtime/server/routes/__nuxt-seo-utils/debug.js +3 -2
  24. package/dist/runtime/shared/breadcrumbs.d.ts +8 -0
  25. package/dist/runtime/shared/breadcrumbs.js +10 -0
  26. package/dist/runtime/shared/minify.js +47 -34
  27. package/dist/runtime/types.d.ts +5 -1
  28. package/dist/shared/nuxt-seo-utils.NAV2YNQn.mjs +94 -0
  29. package/package.json +20 -24
package/dist/module.mjs CHANGED
@@ -1,20 +1,22 @@
1
1
  import fs, { existsSync } from 'node:fs';
2
2
  import { pathToFileURL } from 'node:url';
3
- import { useNuxt, addTypeTemplate, extendRouteRules, useLogger, defineNuxtModule, createResolver, addServerHandler, hasNuxtModule, addPlugin, addImports, hasNuxtCompatibility, directoryToURL, addVitePlugin } from '@nuxt/kit';
3
+ import { useNuxt, addTypeTemplate, useLogger, defineNuxtModule, createResolver, addServerHandler, hasNuxtModule, addPlugin, addImports, hasNuxtCompatibility, directoryToURL, addVitePlugin } from '@nuxt/kit';
4
4
  import { defu } from 'defu';
5
5
  import { resolveModulePath } from 'exsolve';
6
6
  import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
7
- import { useModuleLogger, resolveHostUnheadMajor } from 'nuxtseo-shared/kit';
7
+ import { useModuleLogger, setupNitroRuntimeCompatibility, renderNitroTypeAugmentations, resolveHostUnheadMajor } from 'nuxtseo-shared/kit';
8
8
  import { resolve, extname, relative, dirname, basename } from 'pathe';
9
- import { readPackageJSON } from 'pkg-types';
9
+ import { readPackageJSON, resolvePackageJSON } from 'pkg-types';
10
10
  import { unpackMeta } from '@unhead/vue/utils';
11
11
  import { glob } from 'tinyglobby';
12
12
  import { encodePath, joinURL } from 'ufo';
13
+ import { c as classifyIconFilename, g as getIconRel, n as normalizeIconSizes, a as partitionColorModeIconLinks } from './shared/nuxt-seo-utils.NAV2YNQn.mjs';
13
14
  import escapeRE from 'escape-string-regexp';
14
15
  import { readFile, readdir } from 'node:fs/promises';
15
16
  import imageSize from 'image-size';
16
17
  import { JSON_TYPES, SKIP_JS_TYPES } from '../dist/runtime/shared/minify.js';
17
18
  import { setupDevToolsUI as setupDevToolsUI$1 } from 'nuxtseo-shared/devtools';
19
+ import 'node:buffer';
18
20
 
19
21
  function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
20
22
  const seoMeta = nuxt.options.app?.seoMeta || nuxt.options.app?.head?.seoMeta || {};
@@ -74,20 +76,15 @@ export {}
74
76
  }
75
77
 
76
78
  const MetaTagFileDeepGlobs = [
77
- "**/og-image.{png,jpg,jpeg,gif}",
78
- "**/opengraph-image.{png,jpg,jpeg,gif}",
79
- "**/twitter-image.{png,jpg,jpeg,gif}",
80
- "**/apple-icon.{jpg,jpeg,png}",
81
- "**/apple-touch.{jpg,jpeg,png}",
82
- "**/apple-touch-*.{jpg,jpeg,png}",
83
- "**/apple-touch-icon.{jpg,jpeg,png}",
84
- "**/apple-touch-icon-*.{jpg,jpeg,png}",
85
- "**/icon.{ico,jpg,jpeg,png,svg}",
86
- "**/icon-*.{ico,jpg,jpeg,png,svg}"
79
+ "**/{og-image,opengraph-image,twitter-image}.{png,jpg,jpeg,gif}",
80
+ "**/{favicon*,icon*}.{ico,jpg,jpeg,png,svg}",
81
+ "**/*.icon*.{ico,jpg,jpeg,png,svg}",
82
+ "**/apple-*.{jpg,jpeg,png}",
83
+ "**/*.apple-*.{jpg,jpeg,png}"
87
84
  ];
88
- const META_TAG_FILE_RE = /^(?:(?:og-image|opengraph-image)\.(?:png|jpe?g|gif)|twitter-image\.(?:png|jpe?g|gif)|apple-(?:icon|touch|touch-icon)(?:-[^.]+)?\.(?:jpe?g|png)|icon(?:-[^.]+)?\.(?:ico|jpe?g|png|svg)|favicon\.ico)$/;
85
+ const SOCIAL_IMAGE_RE = /^(?:og-image|opengraph-image|twitter-image)\.(?:png|jpe?g|gif)$/;
89
86
  function isMetaTagFile(filename) {
90
- return META_TAG_FILE_RE.test(filename);
87
+ return SOCIAL_IMAGE_RE.test(filename) || classifyIconFilename(filename) !== void 0;
91
88
  }
92
89
 
93
90
  const INDEX_SUFFIX_RE = /-index$/;
@@ -190,9 +187,6 @@ function parseSegment(segment) {
190
187
  return tokens;
191
188
  }
192
189
 
193
- function hasLinkRel(input, rel) {
194
- return input.link?.some((link) => link.rel === rel);
195
- }
196
190
  function hasMetaProperty(input, property) {
197
191
  return input.meta?.some((meta) => meta.property === property);
198
192
  }
@@ -201,12 +195,12 @@ async function getImageMeta(base, path, isIcon = false) {
201
195
  const absolutePath = resolve(base, path);
202
196
  const file = absolutePath.split("/").pop();
203
197
  const keyword = file.split(".")[0];
204
- let ext = absolutePath.split(".").pop();
198
+ let ext = absolutePath.split(".").pop()?.toLowerCase();
205
199
  if (ext === "jpg")
206
200
  ext = "jpeg";
207
- const { width, height } = await getImageDimensions(absolutePath);
201
+ const { width, height, images } = await getImageDimensions(absolutePath);
208
202
  const payload = {
209
- type: ext === "svg" ? `image/svg+xml` : `image/${ext}`
203
+ type: ext === "svg" ? "image/svg+xml" : ext === "ico" ? "image/vnd.microsoft.icon" : `image/${ext}`
210
204
  };
211
205
  if (!isIcon) {
212
206
  payload.width = width;
@@ -221,7 +215,9 @@ async function getImageMeta(base, path, isIcon = false) {
221
215
  payload.media = "(prefers-color-scheme: dark)";
222
216
  else if (path.includes(".light") || path.includes("-light"))
223
217
  payload.media = "(prefers-color-scheme: light)";
224
- if (ext !== "svg") {
218
+ if (ext === "ico" && images?.length) {
219
+ payload.sizes = [...new Map(images.map((image) => [`${image.width}x${image.height}`, image])).values()].sort((a, b) => a.width * a.height - b.width * b.height).map((image) => `${image.width}x${image.height}`).join(" ");
220
+ } else if (ext !== "svg") {
225
221
  payload.sizes = `${width}x${height}`;
226
222
  } else {
227
223
  payload.sizes = "any";
@@ -235,47 +231,64 @@ async function getImageDimensions(absolutePath) {
235
231
  }
236
232
 
237
233
  const DIR_SUFFIX_RE = /\/_dir$/;
234
+ function addGeneratedRouteRule(nuxt, route, generatedRule) {
235
+ nuxt.options.routeRules ||= {};
236
+ nuxt.options.nitro.routeRules ||= {};
237
+ const routeRules = nuxt.options.routeRules;
238
+ const rule = structuredClone(generatedRule);
239
+ routeRules[route] = defu(routeRules[route], rule);
240
+ const nitroRouteRules = nuxt.options.nitro.routeRules;
241
+ if (nitroRouteRules !== routeRules)
242
+ nitroRouteRules[route] = defu(nitroRouteRules[route], structuredClone(rule));
243
+ }
238
244
  async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
239
245
  const pagesDirs = nuxt.options._layers.map((layer) => resolve(layer.config.rootDir, layer.config.dir?.pages || "pages")).filter((dir) => fs.existsSync(dir));
240
246
  const appendRouteRules = {};
247
+ const registeredFiles = /* @__PURE__ */ new Set();
241
248
  const devMiddlewareMap = {};
242
249
  const nitroOutputMap = [];
243
250
  for (const pagesDir of pagesDirs) {
244
- const files = await glob(MetaTagFileDeepGlobs, { cwd: pagesDir, onlyFiles: true });
251
+ const files = (await glob(MetaTagFileDeepGlobs, { cwd: pagesDir, onlyFiles: true })).filter((file) => isMetaTagFile(basename(file)));
245
252
  for (const file of files) {
246
253
  const fileName = basename(file);
247
254
  const keyword = fileName.split(".")[0] || "";
248
255
  let { path } = generateNuxtPageFromFile(resolve(pagesDir, dirname(file)), pagesDir);
249
- const meta = await getImageMeta(pagesDir, file);
250
256
  if (path.endsWith("/_dir"))
251
257
  path = path.replace(DIR_SUFFIX_RE, "");
252
- const src = joinURL(path, fileName);
253
- if (appendRouteRules[`${path}/**`])
258
+ const registrationKey = `${path}:${fileName}`;
259
+ if (registeredFiles.has(registrationKey))
254
260
  continue;
255
- if (["icon", "apple-touch-icon", "apple-icon"].includes(keyword) || keyword.startsWith("icon-")) {
256
- appendRouteRules[`${path}/**`] = {
257
- head: {
258
- link: [
259
- { rel: keyword, type: meta.type, href: src, sizes: meta.sizes }
260
- ]
261
- }
262
- };
261
+ registeredFiles.add(registrationKey);
262
+ const routeAssetPath = joinURL(path, fileName);
263
+ const href = joinURL(nuxt.options.app.baseURL, routeAssetPath);
264
+ const iconRel = classifyIconFilename(fileName);
265
+ const meta = await getImageMeta(pagesDir, file, iconRel !== void 0);
266
+ const routeRule = appendRouteRules[path] ||= { head: { link: [] }, seoMeta: {} };
267
+ if (iconRel) {
268
+ routeRule.head.link.push({
269
+ rel: iconRel,
270
+ type: String(meta.type),
271
+ href,
272
+ sizes: String(meta.sizes),
273
+ media: meta.media ? String(meta.media) : void 0
274
+ });
263
275
  } else {
264
- appendRouteRules[`${path}/**`] = {
265
- seoMeta: {
266
- [["opengraph-image", "og-image"].includes(keyword) ? "ogImage" : "twitterImage"]: [{ url: src, ...meta, sizes: void 0 }]
267
- }
268
- };
276
+ const property = ["opengraph-image", "og-image"].includes(keyword) ? "ogImage" : "twitterImage";
277
+ routeRule.seoMeta[property] ||= [];
278
+ routeRule.seoMeta[property].push({ url: href, ...meta, sizes: void 0 });
269
279
  }
270
- devMiddlewareMap[src] = resolve(pagesDir, file);
280
+ devMiddlewareMap[routeAssetPath] = resolve(pagesDir, file);
271
281
  nitroOutputMap.push({
272
282
  src: resolve(pagesDir, file),
273
- dest: src.slice(1)
283
+ dest: routeAssetPath.slice(1)
274
284
  });
275
285
  }
276
286
  }
277
- for (const [route, rule] of Object.entries(appendRouteRules))
278
- extendRouteRules(route, rule, { override: true });
287
+ for (const [route, rule] of Object.entries(appendRouteRules)) {
288
+ addGeneratedRouteRule(nuxt, route, rule);
289
+ if (route !== "/")
290
+ addGeneratedRouteRule(nuxt, `${route}/**`, rule);
291
+ }
279
292
  if (nuxt.options.dev) {
280
293
  nuxt.hooks.hook("nitro:config", async (nitroConfig) => {
281
294
  nitroConfig.virtual["#seo-utils-virtual/pageDirImages"] = `export const fileMapping = ${JSON.stringify(devMiddlewareMap)}`;
@@ -295,9 +308,54 @@ async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
295
308
  }
296
309
 
297
310
  async function listMetaTagFiles(dir) {
298
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
311
+ const entries = await readdir(dir, { withFileTypes: true }).catch((error) => {
312
+ if (error.code === "ENOENT")
313
+ return [];
314
+ throw error;
315
+ });
299
316
  return entries.filter((e) => e.isFile() && isMetaTagFile(e.name)).map((e) => e.name);
300
317
  }
318
+ function findLocalIcon(href, baseURL, iconEntries) {
319
+ if (typeof href !== "string" || /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(href))
320
+ return;
321
+ const path = href.split(/[?#]/, 1)[0] || "";
322
+ const file = basename(path);
323
+ const entry = iconEntries.find((icon) => icon.file === file);
324
+ if (!entry)
325
+ return;
326
+ const validPaths = /* @__PURE__ */ new Set([entry.file, `/${entry.file}`, joinURL(baseURL, entry.file)]);
327
+ return validPaths.has(path) ? { entry, resolvedHref: `${joinURL(baseURL, entry.file)}${href.slice(path.length)}` } : void 0;
328
+ }
329
+ function enrichConfiguredIcon(link, baseURL, iconEntries, diagnostics) {
330
+ const iconLink = link;
331
+ const match = findLocalIcon(iconLink.href, baseURL, iconEntries);
332
+ if (!match)
333
+ return link;
334
+ const { entry, resolvedHref } = match;
335
+ if (iconLink.href !== resolvedHref) {
336
+ diagnostics.push({
337
+ _tag: "IconHrefNormalized",
338
+ configured: String(iconLink.href),
339
+ resolved: resolvedHref
340
+ });
341
+ }
342
+ const normalizedSizes = normalizeIconSizes(iconLink.sizes, entry.sizes);
343
+ if (normalizedSizes.normalized) {
344
+ diagnostics.push({
345
+ _tag: "IconSizesNormalized",
346
+ href: String(iconLink.href),
347
+ configured: String(iconLink.sizes),
348
+ resolved: entry.sizes
349
+ });
350
+ }
351
+ return {
352
+ ...iconLink,
353
+ href: resolvedHref,
354
+ type: iconLink.type || entry.type,
355
+ sizes: normalizedSizes.sizes,
356
+ media: iconLink.media || entry.media
357
+ };
358
+ }
301
359
  async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
302
360
  const publicDirs = nuxt.options._layers.map((layer) => resolve(layer.config.rootDir, layer.config.dir?.public || "public"));
303
361
  const seen = /* @__PURE__ */ new Set();
@@ -316,37 +374,33 @@ async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
316
374
  link: [],
317
375
  meta: []
318
376
  });
319
- if (!hasLinkRel(headConfig, "icon")) {
320
- if (rootPublicFiles.includes("favicon.ico") && nuxt.options.app.baseURL !== "/") {
321
- headConfig.link.push({
322
- rel: "icon",
323
- href: joinURL(nuxt.options.app.baseURL, "favicon.ico"),
324
- sizes: "any"
325
- });
326
- }
327
- const isIcon = (file) => file.includes("icon") && !file.endsWith(".ico");
328
- const isAppleTouchIcon = (file) => file.includes("apple-icon") || file.includes("apple-touch-icon") || file.includes("apple-touch");
329
- const resolveDir = (file) => fileEntries.find((e) => e.file === file).dir;
330
- headConfig.link.push(
331
- ...await Promise.all([
332
- ...rootPublicFiles.filter((file) => isIcon(file) && !isAppleTouchIcon(file)).sort().map(async (iconFile) => {
333
- const meta = await getImageMeta(resolveDir(iconFile), iconFile, true);
334
- return {
335
- rel: "icon",
336
- href: joinURL(nuxt.options.app.baseURL, iconFile),
337
- ...meta
338
- };
339
- }),
340
- ...rootPublicFiles.filter((file) => isAppleTouchIcon(file)).sort().map(async (appleIconFile) => {
341
- const meta = await getImageMeta(resolveDir(appleIconFile), appleIconFile, true);
342
- return {
343
- rel: "apple-touch-icon",
344
- href: joinURL(nuxt.options.app.baseURL, appleIconFile),
345
- ...meta
346
- };
347
- })
348
- ])
349
- );
377
+ const iconEntries = (await Promise.all(fileEntries.map(async ({ file, dir }) => {
378
+ const rel = classifyIconFilename(file);
379
+ if (!rel)
380
+ return;
381
+ const meta = await getImageMeta(dir, file, true);
382
+ return {
383
+ file,
384
+ dir,
385
+ rel,
386
+ type: String(meta.type),
387
+ sizes: String(meta.sizes),
388
+ media: meta.media ? String(meta.media) : void 0
389
+ };
390
+ }))).filter((entry) => entry !== void 0);
391
+ const diagnostics = [];
392
+ headConfig.link = headConfig.link.map((link) => enrichConfiguredIcon(link, nuxt.options.app.baseURL, iconEntries, diagnostics));
393
+ const configuredIconRels = new Set(headConfig.link.map((link) => getIconRel(link.rel)).filter((rel) => rel !== void 0));
394
+ for (const rel of ["icon", "apple-touch-icon"]) {
395
+ if (configuredIconRels.has(rel))
396
+ continue;
397
+ headConfig.link.push(...iconEntries.filter((entry) => entry.rel === rel).sort((a, b) => a.file.localeCompare(b.file)).map((entry) => ({
398
+ rel: entry.rel,
399
+ type: entry.type,
400
+ href: joinURL(nuxt.options.app.baseURL, entry.file),
401
+ sizes: entry.sizes,
402
+ media: entry.media
403
+ })));
350
404
  }
351
405
  let hasTwitterImage = hasMetaProperty(headConfig, "twitter:image");
352
406
  if (!hasTwitterImage) {
@@ -392,8 +446,8 @@ async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
392
446
  }
393
447
  }
394
448
  nuxt.options.app.head = headConfig;
395
- const hasIcons = rootPublicFiles.some((f) => f.includes("icon") || f === "favicon.ico");
396
- return { hasIcons };
449
+ const hasIcons = iconEntries.length > 0 || configuredIconRels.size > 0;
450
+ return { hasIcons, diagnostics };
397
451
  }
398
452
 
399
453
  const logger = useLogger("nuxt-seo-utils");
@@ -490,6 +544,16 @@ function minifyStaticHead() {
490
544
  });
491
545
  }
492
546
 
547
+ function resolveUnheadVitePluginSource(input, resolvePath) {
548
+ if (input.unheadMajor < 3)
549
+ return { _tag: "unsupported-v2" };
550
+ const path = resolvePath("@unhead/vue/vite", {
551
+ try: true,
552
+ from: [...input.hostImportPaths, ...input.importPaths]
553
+ });
554
+ return path ? { _tag: "vue", url: pathToFileURL(path).href } : { _tag: "missing-vue" };
555
+ }
556
+
493
557
  function setupNuxtConfigAppHeadWithMoreDefaults(nuxt = useNuxt()) {
494
558
  const headConfig = nuxt.options.app.head;
495
559
  headConfig.link = headConfig.link || [];
@@ -551,7 +615,7 @@ const module$1 = defineNuxtModule({
551
615
  automaticOgAndTwitterTags: true,
552
616
  canonicalLowercase: true,
553
617
  tagPriority: "low",
554
- minify: true
618
+ minify: { build: true, runtime: false }
555
619
  },
556
620
  async setup(config, nuxt) {
557
621
  const logger = useModuleLogger("nuxt-seo-utils", config, nuxt);
@@ -559,14 +623,22 @@ const module$1 = defineNuxtModule({
559
623
  logger.debug("The module is disabled, skipping setup.");
560
624
  return;
561
625
  }
626
+ const nitroCompatibility = setupNitroRuntimeCompatibility(nuxt);
562
627
  const { resolve } = createResolver(import.meta.url);
563
628
  const { version } = await readPackageJSON(resolve("../package.json"));
564
629
  await installNuxtSiteConfig();
565
630
  const runtimeDir = resolve("./runtime");
566
631
  if (config.metaDataFiles) {
567
- const { hasIcons } = await generateTagsFromPublicFiles();
632
+ const { hasIcons, diagnostics } = await generateTagsFromPublicFiles();
568
633
  await generateTagsFromPageDirImages();
569
634
  if (nuxt.options.dev) {
635
+ diagnostics.forEach((diagnostic) => {
636
+ if (diagnostic._tag === "IconSizesNormalized") {
637
+ logger.warn(`Normalized icon sizes for ${diagnostic.href} from "${diagnostic.configured}" to "${diagnostic.resolved}" to match the local file.`);
638
+ } else if (diagnostic._tag === "IconHrefNormalized") {
639
+ logger.warn(`Normalized local icon href from "${diagnostic.configured}" to "${diagnostic.resolved}" to include app.baseURL.`);
640
+ }
641
+ });
570
642
  if (!hasIcons) {
571
643
  const publicDir = resolve(nuxt.options.rootDir, nuxt.options.dir.public);
572
644
  const logoPatterns = ["logo.svg", "logo.png", "icon.svg", "icon.png", "logo.jpg", "logo.webp"];
@@ -672,25 +744,17 @@ const module$1 = defineNuxtModule({
672
744
  filename: "module/nuxt-seo-utils.d.ts",
673
745
  getContents: (data) => {
674
746
  const typesPath = relative(resolve(data.nuxt.options.rootDir, data.nuxt.options.buildDir, "module"), resolve("runtime/types"));
675
- const types = ` interface NitroRouteRules {
676
- seoMeta?: import('${typesPath}').MetaFlatSerializable
677
- head?: import('${typesPath}').Head
678
- }
679
- interface NitroRouteConfig {
680
- seoMeta?: import('${typesPath}').MetaFlatSerializable
681
- head?: import('${typesPath}').Head
682
- }`;
747
+ const routeRuleTypes = `seoMeta?: import('${typesPath}').MetaFlatSerializable
748
+ head?: import('${typesPath}').Head`;
749
+ const nitroTypes = renderNitroTypeAugmentations(nitroCompatibility, {
750
+ routeRules: routeRuleTypes,
751
+ routeConfig: routeRuleTypes
752
+ });
683
753
  return `// Generated by nuxt-seo-utils
684
754
 
685
755
  import type { RobotsContext } from '#robots/types'
686
756
 
687
- declare module 'nitropack/types' {
688
- ${types}
689
- }
690
-
691
- declare module 'nitropack' {
692
- ${types}
693
- }
757
+ ${nitroTypes}
694
758
 
695
759
 
696
760
  declare module '@nuxt/schema' {
@@ -709,6 +773,18 @@ export {}
709
773
  nuxt: true
710
774
  });
711
775
  const appRuntimeDir = resolve(runtimeDir, "./app");
776
+ nuxt.hook("modules:done", () => {
777
+ if (!hasNuxtModule("@nuxtjs/color-mode", nuxt))
778
+ return;
779
+ const headConfig = nuxt.options.app.head;
780
+ const result = partitionColorModeIconLinks(headConfig.link || []);
781
+ if (result._tag === "StaticIconLinks")
782
+ return;
783
+ headConfig.link = result.links;
784
+ const seoRuntimeConfig = nuxt.options.runtimeConfig.public["seo-utils"];
785
+ seoRuntimeConfig.colorModeIcons = result.icons;
786
+ addPlugin({ src: resolve(appRuntimeDir, "plugins", "colorModeIcons") });
787
+ });
712
788
  nuxt.options.unhead = defu(
713
789
  nuxt.options.unhead,
714
790
  { vite: config.treeShakeUseSeoMeta ? { validate: !nuxt.options.test } : false }
@@ -722,9 +798,12 @@ export {}
722
798
  const lightningcssPath = resolveModulePath("lightningcss", { try: true, from: importPaths });
723
799
  const rolldownURL = rolldownPath ? pathToFileURL(rolldownPath).href : void 0;
724
800
  const lightningcssURL = lightningcssPath ? pathToFileURL(lightningcssPath).href : void 0;
725
- const bundlerPath = unheadMajor < 3 ? resolveModulePath("@unhead/bundler/vite", { try: true, from: importPaths }) : void 0;
726
- const bundlerURL = bundlerPath ? pathToFileURL(bundlerPath).href : void 0;
727
- const vitePluginSource = unheadMajor >= 3 ? { _tag: "vue" } : bundlerURL ? { _tag: "bundler", url: bundlerURL } : { _tag: "missing-bundler" };
801
+ const hostImportPaths = unheadMajor >= 3 ? [directoryToURL(dirname(await resolvePackageJSON("nuxt", { url: directoryToURL(nuxt.options.rootDir).href })))] : [];
802
+ const vitePluginSource = resolveUnheadVitePluginSource({
803
+ unheadMajor,
804
+ hostImportPaths,
805
+ importPaths
806
+ }, resolveModulePath);
728
807
  const minify = {
729
808
  js: rolldownURL ? async (code) => {
730
809
  const { minify: minify2 } = await import(rolldownURL);
@@ -739,22 +818,14 @@ export {}
739
818
  }).code).trim();
740
819
  } : void 0
741
820
  };
742
- if (vitePluginSource._tag === "missing-bundler") {
743
- logger.warn("`treeShakeUseSeoMeta` requires the optional `@unhead/bundler` peer on Unhead v2. Install it or disable `treeShakeUseSeoMeta`.");
821
+ if (vitePluginSource._tag === "unsupported-v2") {
822
+ logger.warn("`treeShakeUseSeoMeta` requires Unhead v3; skipping the transform on Unhead v2.");
823
+ } else if (vitePluginSource._tag === "missing-vue") {
824
+ logger.warn("`treeShakeUseSeoMeta` requires `@unhead/vue` with the `/vite` export. Install Unhead v3 or disable `treeShakeUseSeoMeta`.");
744
825
  } else {
745
826
  addVitePlugin(async () => {
746
- if (vitePluginSource._tag === "vue") {
747
- const v3Vite = "@unhead/vue/vite";
748
- const { Unhead: Unhead2 } = await import(v3Vite);
749
- return Unhead2({ minify, ...viteOpts });
750
- }
751
827
  const { Unhead } = await import(vitePluginSource.url);
752
- return Unhead({
753
- _framework: "@unhead/vue",
754
- minify,
755
- validate: false,
756
- devtools: false
757
- });
828
+ return Unhead({ minify, ...viteOpts });
758
829
  }, {
759
830
  prepend: true
760
831
  });
@@ -17,7 +17,7 @@ import {
17
17
  import { defineBreadcrumb, useI18n, useSchemaOrg } from "#imports";
18
18
  import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
19
19
  import { createSitePathResolver } from "#site-config/app/composables/utils";
20
- import { pathBreadcrumbSegments } from "../../shared/breadcrumbs.js";
20
+ import { pathBreadcrumbSegments, resolveBreadcrumbRoot } from "../../shared/breadcrumbs.js";
21
21
  function withoutQuery(path) {
22
22
  return path.split("?")[0] || "";
23
23
  }
@@ -120,8 +120,11 @@ export function useBreadcrumbItems(_options = {}) {
120
120
  }, {});
121
121
  let rootNode = flatOptions.rootSegment || "/";
122
122
  if (i18n) {
123
- if (i18n.strategy === "prefix" || i18n.strategy !== "no_prefix" && toValue(i18n.defaultLocale) !== toValue(i18n.locale))
124
- rootNode = `${rootNode}${toValue(i18n.locale)}`;
123
+ rootNode = resolveBreadcrumbRoot(rootNode, {
124
+ locale: toValue(i18n.locale),
125
+ defaultLocale: toValue(i18n.defaultLocale),
126
+ strategy: i18n.strategy
127
+ });
125
128
  }
126
129
  const current = withoutQuery(withoutTrailingSlash(flatOptions.path || toRaw(route)?.path || rootNode)) || "";
127
130
  const segments = pathBreadcrumbSegments(current, rootNode).map((path, index) => {
@@ -0,0 +1,3 @@
1
+ import type { Link } from '@unhead/vue/types';
2
+ import type { ColorModeIconLinks } from '../../types.js';
3
+ export declare function selectColorModeIconLinks(icons: ColorModeIconLinks, colorMode: string): Link[];
@@ -0,0 +1,10 @@
1
+ export function selectColorModeIconLinks(icons, colorMode) {
2
+ const selected = colorMode === "dark" ? icons.dark : icons.light;
3
+ return selected.map((link, index) => ({
4
+ ...link,
5
+ key: `nuxt-seo-utils:color-mode-icon:${getLinkKey(link, index)}`
6
+ }));
7
+ }
8
+ function getLinkKey(link, index) {
9
+ return `${link.rel}:${index}`;
10
+ }
@@ -1,27 +1,25 @@
1
- import { injectHead, useSeoMeta } from "@unhead/vue";
2
- import { defineNuxtPlugin, getRouteRules, useRequestEvent, useRuntimeConfig, useState } from "nuxt/app";
1
+ import { defineNuxtPlugin, useHead, useRequestEvent, useRuntimeConfig, useSeoMeta, useState } from "#imports";
2
+ function parseRouteRuleState(context) {
3
+ const rules = context._nitro?.routeRules;
4
+ return {
5
+ head: rules?.head,
6
+ seoMeta: rules?.seoMeta
7
+ };
8
+ }
3
9
  export default defineNuxtPlugin({
4
10
  enforce: "post",
5
11
  env: { islands: false },
6
- async setup() {
7
- const head = injectHead();
8
- if (!head)
9
- return;
12
+ setup() {
10
13
  const { tagPriority } = useRuntimeConfig().public["seo-utils"];
11
14
  const routeRuleState = useState("nuxt-seo-utils:routeRules", () => null);
12
15
  if (import.meta.server) {
13
16
  const event = useRequestEvent();
14
- const routeRules = await getRouteRules(event);
15
- const rules = routeRules;
16
- routeRuleState.value = {
17
- head: rules.head,
18
- seoMeta: rules.seoMeta
19
- };
17
+ routeRuleState.value = parseRouteRuleState(event?.context);
20
18
  }
21
19
  if (routeRuleState.value) {
22
20
  const { head: headInput, seoMeta } = routeRuleState.value;
23
21
  if (headInput)
24
- head.push(headInput);
22
+ useHead(headInput);
25
23
  if (seoMeta)
26
24
  useSeoMeta(seoMeta, { tagPriority });
27
25
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import { defineNuxtPlugin, useHead, useNuxtApp, useRuntimeConfig } from "#imports";
2
+ import { selectColorModeIconLinks } from "../logic/colorModeIcons.js";
3
+ export default defineNuxtPlugin({
4
+ name: "nuxt-seo-utils:color-mode-icons",
5
+ enforce: "post",
6
+ setup() {
7
+ const runtimeConfig = useRuntimeConfig();
8
+ const seoConfig = runtimeConfig.public["seo-utils"];
9
+ const colorMode = useNuxtApp().$colorMode;
10
+ const icons = seoConfig.colorModeIcons;
11
+ if (!icons || !colorMode)
12
+ return;
13
+ useHead(() => ({
14
+ link: selectColorModeIconLinks(icons, colorMode.value)
15
+ }));
16
+ }
17
+ });
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void> | undefined>;
1
+ declare const _default: any;
2
2
  export default _default;
@@ -1,5 +1,5 @@
1
- import { defineEventHandler, getRequestHost, sendRedirect, setHeader } from "h3";
2
1
  import { joinURL } from "ufo";
2
+ import { defineEventHandler, getRequestHost, sendRedirect, setHeader } from "#nuxtseo/h3";
3
3
  import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
4
4
  export default defineEventHandler((e) => {
5
5
  if (import.meta.prerender) {
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<NonSharedBuffer | undefined>>;
1
+ declare const _default: any;
2
2
  export default _default;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
- import { defineEventHandler, setHeader } from "h3";
3
2
  import { parseURL } from "ufo";
3
+ import { defineEventHandler, setHeader } from "#nuxtseo/h3";
4
4
  import { fileMapping } from "#seo-utils-virtual/pageDirImages";
5
5
  export default defineEventHandler(async (e) => {
6
6
  const path = parseURL(e.path).pathname;
@@ -1,2 +1,2 @@
1
- declare const _default: import("nitropack/types").NitroAppPlugin;
1
+ declare const _default: any;
2
2
  export default _default;
@@ -1,4 +1,4 @@
1
- import { defineNitroPlugin } from "nitropack/runtime";
1
+ import { defineNitroPlugin } from "#nuxtseo/nitro";
2
2
  import { JSON_TYPES, minifyCSS, minifyJS, minifyJSON, SKIP_JS_TYPES } from "../../shared/minify.js";
3
3
  const INLINE_SCRIPT_RE = /<script(?![^>]+\bsrc\b)([^>]*)>([\s\S]*?)<\/script\s*>/gi;
4
4
  const INLINE_STYLE_RE = /<style([^>]*)>([\s\S]*?)<\/style\s*>/gi;
@@ -1,18 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, {
2
- runtimeConfig: any;
3
- siteConfig: {
4
- url: string;
5
- name: any;
6
- description: any;
7
- env: any;
8
- indexable: any;
9
- trailingSlash: any;
10
- titleSeparator: any;
11
- defaultLocale: any;
12
- currentLocale: any;
13
- twitter: any;
14
- };
15
- headConfig: any;
16
- path: string | number | true | Record<string, any> | import("ufo").QueryValue[];
17
- }>;
1
+ declare const _default: any;
18
2
  export default _default;
@@ -1,5 +1,5 @@
1
- import { defineEventHandler, getQuery } from "h3";
2
- import { useRuntimeConfig } from "nitropack/runtime";
1
+ import { defineEventHandler, getQuery } from "#nuxtseo/h3";
2
+ import { useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { getSiteConfig } from "#site-config/server/composables";
4
4
  export default defineEventHandler((e) => {
5
5
  const siteConfig = getSiteConfig(e);
@@ -10,6 +10,7 @@ export default defineEventHandler((e) => {
10
10
  return {
11
11
  runtimeConfig: {
12
12
  version: runtimeConfig.public?.["nuxt-seo-utils-version"] || "unknown",
13
+ appBaseURL: runtimeConfig.app.baseURL,
13
14
  ...seoUtils
14
15
  },
15
16
  siteConfig: {
@@ -1 +1,9 @@
1
+ import type { RuntimeI18nConfig } from 'nuxtseo-shared/i18n-runtime';
2
+ interface BreadcrumbLocaleContext {
3
+ locale?: string;
4
+ defaultLocale?: string;
5
+ strategy?: RuntimeI18nConfig['strategy'];
6
+ }
7
+ export declare function resolveBreadcrumbRoot(rootNode: string, context: BreadcrumbLocaleContext): string;
1
8
  export declare function pathBreadcrumbSegments(path: string, rootNode?: string): string[];
9
+ export {};