nuxt-seo-utils 6.0.11 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md CHANGED
@@ -1,9 +1,9 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Harlan Wilton
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
-
7
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
-
9
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Harlan Wilton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,74 +1,74 @@
1
- <h1>nuxt-seo-utils</h1>
2
-
3
- [![npm version][npm-version-src]][npm-version-href]
4
- [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
- [![License][license-src]][license-href]
6
- [![Nuxt][nuxt-src]][nuxt-href]
7
-
8
- Nuxt SEO Utils is a collection of defaults and utilities to improve your Nuxt site's discoverability and shareability.
9
-
10
- While there are several features covering many aspects of SEO, it covers important defaults such as [automatic canonical URLs](https://nuxtseo.com/learn/controlling-crawlers/canonical-urls) and
11
- [open graph tags](https://nuxtseo.com/learn/mastering-meta/open-graph).
12
-
13
- <p align="center">
14
- <table>
15
- <tbody>
16
- <td align="center">
17
- <sub>Made possible by my <a href="https://github.com/sponsors/harlan-zw">Sponsor Program 💖</a><br> Follow me <a href="https://twitter.com/harlan_zw">@harlan_zw</a> 🐦 • Join <a href="https://discord.gg/275MBUBvgP">Discord</a> for help</sub><br>
18
- </td>
19
- </tbody>
20
- </table>
21
- </p>
22
-
23
- ## Features
24
-
25
- - ▲ Next.js inspired [Metadata Files](https://nextjs.org/docs/app/api-reference/file-conventions/metadata)
26
- - ✨ Use SEO meta in your nuxt.config (`app.seoMeta`) and route rules
27
- - 🤖 Adds meta tags (`og:title`, `og:description`, etc.) from page title and description
28
- - 🧙 Correct tags that need to be absolute (i.e `og:image`)
29
- - 🧩 Site config template params `useHead({ title: '%site.title' })`
30
- - 🍞 Breadcrumb composable
31
- - 🧙 Best practice defaults and enhanced titles
32
-
33
- ## Installation
34
-
35
- Install `nuxt-seo-utils` dependency to your project:
36
-
37
- ```bash
38
- npx nuxi@latest module add nuxt-seo-utils
39
- ```
40
-
41
- 💡 Need a complete SEO solution for Nuxt? Check out [Nuxt SEO](https://nuxtseo.com).
42
-
43
- ## Documentation
44
-
45
- [📖 Read the full documentation](https://nuxtseo.com/docs/utils) for more information.
46
-
47
- ## Demos
48
-
49
- [Basic](https://stackblitz.com/edit/nuxt-starter-vbay3q?file=app.vue)
50
-
51
- ## Sponsors
52
-
53
- <p align="center">
54
- <a href="https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg">
55
- <img src='https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg'/>
56
- </a>
57
- </p>
58
-
59
- ## License
60
-
61
- Licensed under the [MIT license](https://github.com/harlan-zw/nuxt-seo-utils/blob/main/LICENSE.md).
62
-
63
- <!-- Badges -->
64
- [npm-version-src]: https://img.shields.io/npm/v/nuxt-seo-utils/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
65
- [npm-version-href]: https://npmjs.com/package/nuxt-seo-utils
66
-
67
- [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-seo-utils.svg?style=flat&colorA=18181B&colorB=28CF8D
68
- [npm-downloads-href]: https://npmjs.com/package/nuxt-seo-utils
69
-
70
- [license-src]: https://img.shields.io/github/license/harlan-zw/nuxt-seo-utils.svg?style=flat&colorA=18181B&colorB=28CF8D
71
- [license-href]: https://github.com/harlan-zw/nuxt-seo-utils/blob/main/LICENSE.md
72
-
73
- [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
74
- [nuxt-href]: https://nuxt.com
1
+ <h1>nuxt-seo-utils</h1>
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![License][license-src]][license-href]
6
+ [![Nuxt][nuxt-src]][nuxt-href]
7
+
8
+ Nuxt SEO Utils is a collection of defaults and utilities to improve your Nuxt site's discoverability and shareability.
9
+
10
+ While there are several features covering many aspects of SEO, it covers important defaults such as [automatic canonical URLs](https://nuxtseo.com/learn/controlling-crawlers/canonical-urls) and
11
+ [open graph tags](https://nuxtseo.com/learn/mastering-meta/open-graph).
12
+
13
+ <p align="center">
14
+ <table>
15
+ <tbody>
16
+ <td align="center">
17
+ <sub>Made possible by my <a href="https://github.com/sponsors/harlan-zw">Sponsor Program 💖</a><br> Follow me <a href="https://twitter.com/harlan_zw">@harlan_zw</a> 🐦 • Join <a href="https://discord.gg/275MBUBvgP">Discord</a> for help</sub><br>
18
+ </td>
19
+ </tbody>
20
+ </table>
21
+ </p>
22
+
23
+ ## Features
24
+
25
+ - ▲ Next.js inspired [Metadata Files](https://nextjs.org/docs/app/api-reference/file-conventions/metadata)
26
+ - ✨ Use SEO meta in your nuxt.config (`app.seoMeta`) and route rules
27
+ - 🤖 Adds meta tags (`og:title`, `og:description`, etc.) from page title and description
28
+ - 🧙 Correct tags that need to be absolute (i.e `og:image`)
29
+ - 🧩 Site config template params `useHead({ title: '%site.title' })`
30
+ - 🍞 Breadcrumb composable
31
+ - 🧙 Best practice defaults and enhanced titles
32
+
33
+ ## Installation
34
+
35
+ Install `nuxt-seo-utils` dependency to your project:
36
+
37
+ ```bash
38
+ npx nuxi@latest module add nuxt-seo-utils
39
+ ```
40
+
41
+ 💡 Need a complete SEO solution for Nuxt? Check out [Nuxt SEO](https://nuxtseo.com).
42
+
43
+ ## Documentation
44
+
45
+ [📖 Read the full documentation](https://nuxtseo.com/docs/utils) for more information.
46
+
47
+ ## Demos
48
+
49
+ [Basic](https://stackblitz.com/edit/nuxt-starter-vbay3q?file=app.vue)
50
+
51
+ ## Sponsors
52
+
53
+ <p align="center">
54
+ <a href="https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg">
55
+ <img src='https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg'/>
56
+ </a>
57
+ </p>
58
+
59
+ ## License
60
+
61
+ Licensed under the [MIT license](https://github.com/harlan-zw/nuxt-seo-utils/blob/main/LICENSE.md).
62
+
63
+ <!-- Badges -->
64
+ [npm-version-src]: https://img.shields.io/npm/v/nuxt-seo-utils/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
65
+ [npm-version-href]: https://npmjs.com/package/nuxt-seo-utils
66
+
67
+ [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-seo-utils.svg?style=flat&colorA=18181B&colorB=28CF8D
68
+ [npm-downloads-href]: https://npmjs.com/package/nuxt-seo-utils
69
+
70
+ [license-src]: https://img.shields.io/github/license/harlan-zw/nuxt-seo-utils.svg?style=flat&colorA=18181B&colorB=28CF8D
71
+ [license-href]: https://github.com/harlan-zw/nuxt-seo-utils/blob/main/LICENSE.md
72
+
73
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
74
+ [nuxt-href]: https://nuxt.com
package/dist/module.d.mts CHANGED
@@ -81,6 +81,12 @@ interface ModuleOptions {
81
81
  * When enabled, it will whitelist the query parameters that are allowed in the canonical URL.
82
82
  */
83
83
  canonicalQueryWhitelist?: string[];
84
+ /**
85
+ * When enabled, it will lowercase the canonical URL.
86
+ *
87
+ * @default true
88
+ */
89
+ canonicalLowercase?: boolean;
84
90
  /**
85
91
  * When enabled, it will redirect any request to the canonical domain (site url) using a 301 redirect on non-dev environments.
86
92
  *
package/dist/module.d.ts CHANGED
@@ -81,6 +81,12 @@ interface ModuleOptions {
81
81
  * When enabled, it will whitelist the query parameters that are allowed in the canonical URL.
82
82
  */
83
83
  canonicalQueryWhitelist?: string[];
84
+ /**
85
+ * When enabled, it will lowercase the canonical URL.
86
+ *
87
+ * @default true
88
+ */
89
+ canonicalLowercase?: boolean;
84
90
  /**
85
91
  * When enabled, it will redirect any request to the canonical domain (site url) using a 301 redirect on non-dev environments.
86
92
  *
package/dist/module.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "name": "nuxt-seo-utils",
3
3
  "configKey": "seo",
4
4
  "compatibility": {
5
- "nuxt": ">=3.6.1",
5
+ "nuxt": ">=3.16.0",
6
6
  "bridge": false
7
7
  },
8
- "version": "6.0.10",
8
+ "version": "6.0.12",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "0.8.4",
11
11
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { useNuxt, createResolver, addTemplate, defineNuxtModule, useLogger, addServerHandler, hasNuxtModule, addPlugin, addImports, addVitePlugin } from '@nuxt/kit';
1
+ import { useNuxt, createResolver, addTemplate, defineNuxtModule, useLogger, resolvePath, addServerHandler, hasNuxtModule, addPlugin, addImports, addVitePlugin } from '@nuxt/kit';
2
2
  import UnheadVite from '@unhead/addons/vite';
3
3
  import { defu } from 'defu';
4
- import { resolvePath } from 'mlly';
5
4
  import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
6
- import { relative, resolve, extname, dirname, basename } from 'pathe';
7
- import { unpackMeta } from '@unhead/shared';
5
+ import { relative, resolve, extname, dirname, basename, join } from 'pathe';
6
+ import { readPackageJSON } from 'pkg-types';
7
+ import { gte } from 'semver';
8
8
  import fg from 'fast-glob';
9
9
  import fs from 'node:fs';
10
10
  import { encodePath, joinURL } from 'ufo';
@@ -12,9 +12,9 @@ import escapeRE from 'escape-string-regexp';
12
12
  import { readFile } from 'node:fs/promises';
13
13
  import imageSize from 'image-size';
14
14
 
15
- function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
15
+ function extendNuxtConfigAppHeadSeoMeta(unpackMeta, nuxt = useNuxt()) {
16
16
  const seoMeta = nuxt.options.app?.seoMeta || nuxt.options.app?.head?.seoMeta || false;
17
- const configSeoMeta = nuxt.options.seo?.meta || false;
17
+ const configSeoMeta = nuxt.options.seo?.meta || nuxt.options.seo?.seoMeta || false;
18
18
  if (!seoMeta && !configSeoMeta)
19
19
  return;
20
20
  nuxt.options.app.head = nuxt.options.app.head || {};
@@ -55,7 +55,7 @@ function extendNuxtConfigAppHeadTypes(nuxt = useNuxt()) {
55
55
  const jsPaths = paths.filter((p) => p.endsWith(".js") || p.endsWith(".mjs"));
56
56
  return `
57
57
  declare module '#app' {
58
- import { HeadEntry, HeadTag } from '@unhead/schema'
58
+ import { HeadEntry, HeadTag } from '@unhead/vue/types'
59
59
 
60
60
  interface RuntimeNuxtHooks {
61
61
  'head:tags': (tag: HeadTag[]) => Promise<void> | void
@@ -188,6 +188,15 @@ function parseSegment(segment) {
188
188
  return tokens;
189
189
  }
190
190
 
191
+ async function resolveUnpackMeta(unheadV2) {
192
+ try {
193
+ const unhead = await import(String(unheadV2 ? "@unhead/vue/util" : "@unhead/shared"));
194
+ return unhead.unpackMeta;
195
+ } catch {
196
+ const unhead = await import(String(!unheadV2 ? "@unhead/vue/util" : "@unhead/shared"));
197
+ return unhead.unpackMeta;
198
+ }
199
+ }
191
200
  function hasLinkRel(input, rel) {
192
201
  return input.link?.some((link) => link.rel === rel);
193
202
  }
@@ -287,7 +296,7 @@ async function generateTagsFromPageDirImages(nuxt = useNuxt()) {
287
296
  });
288
297
  }
289
298
 
290
- async function generateTagsFromPublicFiles(nuxt = useNuxt()) {
299
+ async function generateTagsFromPublicFiles(unpackMeta, nuxt = useNuxt()) {
291
300
  const publicDirPath = resolve(nuxt.options.rootDir, nuxt.options.dir.public);
292
301
  const rootPublicFiles = (await fg(MetaTagFileGlobs, { cwd: publicDirPath, onlyFiles: true, deep: 1 })).map((file) => basename(file));
293
302
  const headConfig = defu(nuxt.options.app.head, {
@@ -391,7 +400,7 @@ const module = defineNuxtModule({
391
400
  name: "nuxt-seo-utils",
392
401
  configKey: "seo",
393
402
  compatibility: {
394
- nuxt: ">=3.6.1",
403
+ nuxt: ">=3.16.0",
395
404
  bridge: false
396
405
  }
397
406
  },
@@ -409,7 +418,8 @@ const module = defineNuxtModule({
409
418
  treeShakeUseSeoMeta: true,
410
419
  extendNuxtConfigAppHeadTypes: true,
411
420
  setupNuxtConfigAppHeadWithMoreDefaults: true,
412
- automaticOgAndTwitterTags: true
421
+ automaticOgAndTwitterTags: true,
422
+ canonicalLowercase: true
413
423
  },
414
424
  async setup(config, nuxt) {
415
425
  const logger = useLogger("nuxt-seo-utils");
@@ -420,13 +430,23 @@ const module = defineNuxtModule({
420
430
  }
421
431
  const { resolve } = createResolver(import.meta.url);
422
432
  await installNuxtSiteConfig();
433
+ let isUnheadV2 = false;
434
+ const unheadPath = await resolvePath("@unhead/vue").catch(() => void 0).then((p) => p?.endsWith("index.mjs") ? dirname(p) : p);
435
+ if (unheadPath) {
436
+ const { version: unheadVersion } = await readPackageJSON(join(unheadPath, "package.json"));
437
+ if (gte(unheadVersion, "2.0.0-rc.1")) {
438
+ isUnheadV2 = true;
439
+ }
440
+ }
441
+ const runtimeDir = resolve("./runtime");
442
+ const unpackMeta = await resolveUnpackMeta(isUnheadV2);
423
443
  if (config.metaDataFiles) {
424
- await generateTagsFromPublicFiles();
444
+ await generateTagsFromPublicFiles(unpackMeta);
425
445
  await generateTagsFromPageDirImages();
426
446
  if (nuxt.options.dev) {
427
447
  addServerHandler({
428
448
  middleware: true,
429
- handler: resolve("./runtime/server/middleware/resolveImagesInPagesDir")
449
+ handler: resolve(runtimeDir, "server/middleware/resolveImagesInPagesDir")
430
450
  });
431
451
  }
432
452
  }
@@ -434,27 +454,27 @@ const module = defineNuxtModule({
434
454
  if (config.automaticDefaults) {
435
455
  if (hasI18n) {
436
456
  addPlugin({
437
- src: resolve(`./runtime/app/plugins/defaultsWaitI18n`)
457
+ src: resolve(runtimeDir, `./app/plugins/defaultsWaitI18n`)
438
458
  });
439
459
  } else {
440
460
  addPlugin({
441
- src: resolve(`./runtime/app/plugins/defaults`)
461
+ src: resolve(runtimeDir, `./app/plugins/defaults`)
442
462
  });
443
463
  }
444
464
  }
445
465
  if (config.fallbackTitle) {
446
466
  addPlugin({
447
- src: resolve("./runtime/app/plugins/titles")
467
+ src: resolve(runtimeDir, "./app/plugins/titles")
448
468
  });
449
469
  }
450
470
  if (!hasI18n) {
451
471
  addImports({
452
- from: resolve(`./runtime/app/composables/polyfills`),
472
+ from: resolve(runtimeDir, `./app/composables/polyfills`),
453
473
  name: "useI18n"
454
474
  });
455
475
  }
456
476
  addImports({
457
- from: resolve(`./runtime/app/composables/useBreadcrumbItems`),
477
+ from: resolve(runtimeDir, `./app/composables/useBreadcrumbItems`),
458
478
  name: "useBreadcrumbItems"
459
479
  });
460
480
  const polyfills = [
@@ -468,18 +488,18 @@ const module = defineNuxtModule({
468
488
  ];
469
489
  polyfills.forEach((name) => {
470
490
  addImports({
471
- from: resolve("./runtime/app/composables/polyfills"),
491
+ from: resolve(runtimeDir, "./app/composables/polyfills"),
472
492
  name
473
493
  });
474
494
  });
475
495
  nuxt.options.experimental.headNext = true;
476
496
  if (!nuxt.options.dev && config.redirectToCanonicalSiteUrl) {
477
497
  addServerHandler({
478
- handler: resolve("./runtime/server/middleware/redirectCanonical"),
498
+ handler: resolve(runtimeDir, "./server/middleware/redirectCanonical"),
479
499
  middleware: true
480
500
  });
481
501
  }
482
- nuxt.options.alias["#seo-utils"] = resolve("./runtime");
502
+ nuxt.options.alias["#seo-utils"] = runtimeDir;
483
503
  nuxt.options.runtimeConfig.public["seo-utils"] = defu(nuxt.options.runtimeConfig.public["seo-utils"] || {}, {
484
504
  canonicalQueryWhitelist: config.canonicalQueryWhitelist || [
485
505
  "page",
@@ -489,20 +509,22 @@ const module = defineNuxtModule({
489
509
  "q",
490
510
  "category",
491
511
  "tag"
492
- ]
512
+ ],
513
+ canonicalLowercase: config.canonicalLowercase
493
514
  });
494
515
  if (config.extendNuxtConfigAppHeadSeoMeta)
495
- extendNuxtConfigAppHeadSeoMeta();
516
+ extendNuxtConfigAppHeadSeoMeta(unpackMeta);
496
517
  if (config.setupNuxtConfigAppHeadWithMoreDefaults)
497
518
  setupNuxtConfigAppHeadWithMoreDefaults(nuxt);
498
519
  if (config.extendNuxtConfigAppHeadTypes)
499
520
  extendNuxtConfigAppHeadTypes();
500
521
  extendTypes("nuxt-seo-utils", async () => {
522
+ const pkg = isUnheadV2 ? "@unhead/vue/types" : "@unhead/schema";
501
523
  let unheadSchemaPath;
502
524
  try {
503
- unheadSchemaPath = relative(resolve(nuxt.options.rootDir, nuxt.options.buildDir, "module"), await resolvePath("@unhead/schema", { url: import.meta.url }));
525
+ unheadSchemaPath = relative(resolve(nuxt.options.rootDir, nuxt.options.buildDir, "module"), await resolvePath(pkg));
504
526
  } catch {
505
- unheadSchemaPath = "@unhead/schema";
527
+ unheadSchemaPath = pkg;
506
528
  }
507
529
  return `
508
530
  declare module 'nitropack' {
@@ -525,17 +547,17 @@ declare module 'nuxt/schema' {
525
547
  }
526
548
  `;
527
549
  });
528
- const runtimeDir = resolve("./runtime/app");
550
+ const appRuntimeDir = resolve(runtimeDir, "./app");
529
551
  if (config.treeShakeUseSeoMeta)
530
552
  addVitePlugin(UnheadVite());
531
553
  if (config.automaticOgAndTwitterTags)
532
- addPlugin({ src: resolve(runtimeDir, "plugins", "inferSeoMetaPlugin") });
554
+ addPlugin({ src: resolve(appRuntimeDir, "plugins", "inferSeoMetaPlugin") });
533
555
  if (config.mergeWithSiteConfig)
534
- addPlugin({ src: resolve(runtimeDir, "plugins", "siteConfig") });
556
+ addPlugin({ src: resolve(appRuntimeDir, "plugins", "siteConfig") });
535
557
  if (config.extendRouteRules)
536
- addPlugin({ src: resolve(runtimeDir, "plugins", "0.routeRules.server"), mode: "server" });
558
+ addPlugin({ src: resolve(appRuntimeDir, "plugins", "0.routeRules.server"), mode: "server" });
537
559
  if (config.fixRequiredAbsoluteMetaTagsLinks)
538
- addPlugin({ src: resolve(runtimeDir, "plugins", "1.absoluteImageUrls.server"), mode: "server" });
560
+ addPlugin({ src: resolve(appRuntimeDir, "plugins", "1.absoluteImageUrls.server"), mode: "server" });
539
561
  }
540
562
  });
541
563
 
@@ -61,6 +61,12 @@ export interface BreadcrumbProps {
61
61
  * Should the root breadcrumb be shown.
62
62
  */
63
63
  hideRoot?: MaybeRefOrGetter<boolean>;
64
+ /**
65
+ * The root segment of the breadcrumb list.
66
+ *
67
+ * By default, this will be `/`, unless you're using Nuxt I18n with a prefix strategy.
68
+ */
69
+ rootSegment?: string;
64
70
  }
65
71
  export interface BreadcrumbItemProps extends NuxtUIBreadcrumbItem {
66
72
  /** Whether the breadcrumb item represents the aria-current. */
@@ -91,5 +97,5 @@ export interface BreadcrumbItemProps extends NuxtUIBreadcrumbItem {
91
97
  * @see https://github.com/harlan-zw/nuxt-seo/blob/main/src/runtime/nuxt/composables/useBreadcrumbItems.ts
92
98
  * @docs https://nuxtseo.com/nuxt-seo/api/breadcrumbs
93
99
  */
94
- export declare function useBreadcrumbItems(options?: BreadcrumbProps): import("vue").ComputedRef<BreadcrumbItemProps[]>;
100
+ export declare function useBreadcrumbItems(_options?: BreadcrumbProps): import("vue").ComputedRef<BreadcrumbItemProps[]>;
95
101
  export {};
@@ -9,7 +9,7 @@ import { defu } from "defu";
9
9
  import { fixSlashes } from "nuxt-site-config/urls";
10
10
  import { useRouter } from "nuxt/app";
11
11
  import { withoutTrailingSlash } from "ufo";
12
- import { computed, toValue } from "vue";
12
+ import { computed, inject, onUnmounted, getCurrentInstance, provide, ref, toRaw, toValue } from "vue";
13
13
  import { pathBreadcrumbSegments } from "../../shared/breadcrumbs.js";
14
14
  function withoutQuery(path) {
15
15
  return path.split("?")[0];
@@ -17,7 +17,33 @@ function withoutQuery(path) {
17
17
  function titleCase(s) {
18
18
  return s.replaceAll("-", " ").replace(/\w\S*/g, (w) => w.charAt(0).toUpperCase() + w.substr(1).toLowerCase());
19
19
  }
20
- export function useBreadcrumbItems(options = {}) {
20
+ const BreadcrumbCtx = Symbol("BreadcrumbCtx");
21
+ export function useBreadcrumbItems(_options = {}) {
22
+ const vm = getCurrentInstance();
23
+ let isCreatingState = false;
24
+ if (vm) {
25
+ let stateRef2 = inject(BreadcrumbCtx, null);
26
+ if (!stateRef2) {
27
+ stateRef2 = ref({});
28
+ provide(BreadcrumbCtx, stateRef2);
29
+ isCreatingState = false;
30
+ }
31
+ const id2 = "breadcrumb";
32
+ const state = stateRef2.value;
33
+ if (!state[id2]) {
34
+ state[id2] = [];
35
+ }
36
+ const idx = state[id2].push(_options) - 1;
37
+ stateRef2.value = state;
38
+ onUnmounted(() => {
39
+ stateRef2.value = Object.fromEntries(Object.entries(stateRef2.value).map(([k, v]) => {
40
+ if (k === id2) {
41
+ return v.filter((_, i) => i !== idx);
42
+ }
43
+ return v;
44
+ }));
45
+ });
46
+ }
21
47
  const router = useRouter();
22
48
  const i18n = useI18n();
23
49
  const siteResolver = createSitePathResolver({
@@ -26,32 +52,50 @@ export function useBreadcrumbItems(options = {}) {
26
52
  });
27
53
  const siteConfig = useSiteConfig();
28
54
  const items = computed(() => {
29
- let rootNode = "/";
55
+ const optionStack = stateRef.value?.[id];
56
+ const flatOptions = toRaw([...optionStack]).reduce((acc, cur) => {
57
+ acc.rootSegment = acc.rootSegment || cur.rootSegment;
58
+ acc.path = acc.path || cur.path;
59
+ return acc;
60
+ }, {});
61
+ let rootNode = flatOptions.rootSegment || "/";
30
62
  if (i18n) {
31
63
  if (i18n.strategy === "prefix" || i18n.strategy !== "no_prefix" && toValue(i18n.defaultLocale) !== toValue(i18n.locale))
32
- rootNode = `/${toValue(i18n.locale)}`;
64
+ rootNode = `${rootNode}${toValue(i18n.locale)}`;
33
65
  }
34
- const current = withoutQuery(withoutTrailingSlash(toValue(options.path || router.currentRoute.value?.path) || rootNode));
35
- const overrides = toValue(options.overrides) || [];
66
+ const current = withoutQuery(withoutTrailingSlash(toValue(flatOptions.path || router.currentRoute.value?.path) || rootNode));
67
+ const allOverrides = toRaw([...optionStack])?.map((opts) => toValue(opts.overrides)).filter(Boolean);
68
+ const flatOverrides = allOverrides?.reduce((acc, i) => {
69
+ if (i) {
70
+ i.forEach((item, index) => {
71
+ if (item !== void 0) {
72
+ acc[index] = item;
73
+ }
74
+ });
75
+ }
76
+ return acc;
77
+ }, []) || {};
36
78
  const segments = pathBreadcrumbSegments(current, rootNode).map((path, index) => {
37
79
  let item = {
38
80
  to: path
39
81
  };
40
- if (typeof overrides[index] !== "undefined") {
41
- if (overrides[index] === false)
82
+ if (typeof flatOverrides[index] !== "undefined") {
83
+ if (flatOverrides[index] === false)
42
84
  return false;
43
- item = defu(overrides[index], item);
85
+ item = defu(flatOverrides[index], item);
44
86
  }
45
87
  return item;
46
88
  });
47
- if (options.prepend)
48
- segments.unshift(...toValue(options.prepend));
49
- if (options.append)
50
- segments.push(...toValue(options.append));
89
+ const allPrepends = toRaw([...optionStack]).flatMap((opts) => toValue(opts.prepend)).filter(Boolean);
90
+ const allAppends = toRaw([...optionStack]).flatMap((opts) => toValue(opts.append)).filter(Boolean);
91
+ if (allPrepends.length)
92
+ segments.unshift(...allPrepends);
93
+ if (allAppends.length)
94
+ segments.push(...allAppends);
51
95
  return segments.filter(Boolean).map((item) => {
52
96
  let fallbackLabel = titleCase(String((item.to || "").split("/").pop()));
53
97
  let fallbackAriaLabel = "";
54
- const route = router.resolve(item.to)?.matched?.[0];
98
+ const route = item.to ? router.resolve(item.to)?.matched?.[0] : null;
55
99
  if (route) {
56
100
  const routeMeta = route?.meta || {};
57
101
  if (routeMeta.breadcrumb) {
@@ -65,29 +109,29 @@ export function useBreadcrumbItems(options = {}) {
65
109
  fallbackLabel = "Home";
66
110
  }
67
111
  fallbackLabel = routeMeta.breadcrumbLabel || routeMeta.breadcrumbTitle || routeMeta.title || fallbackLabel;
68
- fallbackLabel = i18n.t(`breadcrumb.items.${routeName}.label`, fallbackLabel, { missingWarn: true });
112
+ fallbackLabel = i18n.t(`breadcrumb.items.${routeName}.label`, fallbackLabel, { missingWarn: false });
69
113
  fallbackAriaLabel = i18n.t(`breadcrumb.items.${routeName}.ariaLabel`, fallbackAriaLabel, { missingWarn: false });
70
114
  }
71
115
  item.label = item.label || fallbackLabel;
72
116
  item.ariaLabel = item.ariaLabel || fallbackAriaLabel || item.label;
73
117
  item.current = item.current || item.to === current;
74
- if (toValue(options.hideCurrent) && item.current)
118
+ if (toValue(flatOptions.hideCurrent) && item.current)
75
119
  return false;
76
120
  return item;
77
121
  }).map((m) => {
78
122
  if (m && m.to) {
79
123
  m.to = fixSlashes(siteConfig.trailingSlash, m.to);
80
- if (m.to === rootNode && toValue(options.hideRoot))
124
+ if (m.to === rootNode && toValue(flatOptions.hideRoot))
81
125
  return false;
82
126
  }
83
127
  return m;
84
128
  }).filter(Boolean);
85
129
  });
86
- const schemaOrgEnabled = typeof options.schemaOrg === "undefined" ? true : options.schemaOrg;
87
- if ((import.meta.dev || import.meta.server) && schemaOrgEnabled) {
130
+ const schemaOrgEnabled = typeof _options.schemaOrg === "undefined" ? true : _options.schemaOrg;
131
+ if (isCreatingState && (import.meta.dev || import.meta.server || import.meta.env?.NODE_ENV === "test") && schemaOrgEnabled) {
88
132
  useSchemaOrg([
89
133
  defineBreadcrumb({
90
- id: `#${options.id || "breadcrumb"}`,
134
+ id: `#${_options.id || "breadcrumb"}`,
91
135
  itemListElement: computed(() => items.value.map((item) => ({
92
136
  name: item.label || item.ariaLabel,
93
137
  item: item.to ? siteResolver(item.to) : void 0
@@ -1,11 +1,11 @@
1
+ import { useHead, useSeoMeta } from "#imports";
1
2
  import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
2
3
  import { createSitePathResolver } from "#site-config/app/composables/utils";
3
- import { useHead, useSeoMeta } from "@unhead/vue";
4
4
  import { useError, useRoute, useRuntimeConfig } from "nuxt/app";
5
5
  import { stringifyQuery } from "ufo";
6
6
  import { computed, ref, watch } from "vue";
7
7
  export function applyDefaults(i18n) {
8
- const { canonicalQueryWhitelist } = useRuntimeConfig().public["seo-utils"];
8
+ const { canonicalQueryWhitelist, canonicalLowercase } = useRuntimeConfig().public["seo-utils"];
9
9
  const siteConfig = useSiteConfig();
10
10
  const route = useRoute();
11
11
  const resolveUrl = createSitePathResolver({ withBase: true, absolute: true });
@@ -15,9 +15,13 @@ export function applyDefaults(i18n) {
15
15
  return null;
16
16
  }
17
17
  const { query } = route;
18
- const url = resolveUrl(route.path || "/").value || route.path;
18
+ let url = resolveUrl(route.path || "/").value || route.path;
19
+ if (canonicalLowercase) {
20
+ url = url.toLocaleLowerCase(siteConfig.currentLocale);
21
+ }
19
22
  const filteredQuery = Object.fromEntries(
20
- Object.entries(query).filter(([key]) => canonicalQueryWhitelist.includes(key))
23
+ Object.entries(query).filter(([key]) => canonicalQueryWhitelist.includes(key)).sort(([a], [b]) => a.localeCompare(b))
24
+ // Sort params
21
25
  );
22
26
  return Object.keys(filteredQuery).length ? `${url}?${stringifyQuery(filteredQuery)}` : url;
23
27
  });
@@ -29,6 +33,7 @@ export function applyDefaults(i18n) {
29
33
  deep: true
30
34
  });
31
35
  }
36
+ useHead({ templateParams });
32
37
  const minimalPriority = {
33
38
  // give nuxt.config values higher priority
34
39
  tagPriority: 101
@@ -43,10 +48,10 @@ export function applyDefaults(i18n) {
43
48
  ogType: "website",
44
49
  ogUrl: () => canonicalUrl.value,
45
50
  ogLocale: () => i18n.locale.value,
46
- ogSiteName: "%siteName",
47
- ogDescription: "%site.description"
51
+ ogSiteName: siteConfig.name
48
52
  };
49
- seoMeta.description = "%site.description";
53
+ if (siteConfig.description)
54
+ seoMeta.description = siteConfig.description;
50
55
  if (siteConfig.twitter) {
51
56
  const id = siteConfig.twitter.startsWith("@") ? siteConfig.twitter : `@${siteConfig.twitter}`;
52
57
  seoMeta.twitterCreator = id;
@@ -1,5 +1,5 @@
1
- import { unpackMeta } from "@unhead/shared";
2
- import { injectHead } from "@unhead/vue";
1
+ import { injectHead } from "#imports";
2
+ import { unpackMeta } from "@unhead/vue/utils";
3
3
  import { defineNuxtPlugin, useRequestEvent } from "nuxt/app";
4
4
  export default defineNuxtPlugin({
5
5
  enforce: "post",
@@ -1,5 +1,5 @@
1
+ import { injectHead } from "#imports";
1
2
  import { createSitePathResolver } from "#site-config/app/composables/utils";
2
- import { injectHead } from "@unhead/vue";
3
3
  import { defineNuxtPlugin } from "nuxt/app";
4
4
  import { unref } from "vue";
5
5
  export default defineNuxtPlugin({
@@ -14,6 +14,7 @@ export default defineNuxtPlugin({
14
14
  canonical: true
15
15
  });
16
16
  head.use({
17
+ key: "absoluteImageUrls",
17
18
  hooks: {
18
19
  "tags:resolve": async ({ tags }) => {
19
20
  for (const tag of tags) {
@@ -1,5 +1,5 @@
1
+ import { injectHead } from "#imports";
1
2
  import { InferSeoMetaPlugin } from "@unhead/addons";
2
- import { injectHead } from "@unhead/vue";
3
3
  import { defineNuxtPlugin } from "nuxt/app";
4
4
  export default defineNuxtPlugin(() => {
5
5
  const head = injectHead();
@@ -1,5 +1,5 @@
1
+ import { injectHead } from "#imports";
1
2
  import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
2
- import { injectHead } from "@unhead/vue";
3
3
  import { defineNuxtPlugin } from "nuxt/app";
4
4
  export default defineNuxtPlugin(() => {
5
5
  const head = injectHead();
@@ -24,9 +24,10 @@ export default defineNuxtPlugin(() => {
24
24
  input.meta.push(
25
25
  {
26
26
  name: "description",
27
- content: "%site.description"
27
+ content: "%site.description",
28
+ tagPriority: "low"
28
29
  }
29
30
  );
30
31
  }
31
- head.push(input, { tagPriority: 150 });
32
+ head.push(input);
32
33
  });
@@ -1,4 +1,4 @@
1
- import { useHead } from "@unhead/vue";
1
+ import { useHead } from "#imports";
2
2
  import { defineNuxtPlugin, useError, useRoute } from "nuxt/app";
3
3
  import { titleCase } from "scule";
4
4
  import { withoutTrailingSlash } from "ufo";
@@ -1,3 +1,3 @@
1
- {
2
- "extends": "../../../.nuxt/tsconfig.server.json"
3
- }
1
+ {
2
+ "extends": "../../../../.nuxt/tsconfig.server.json"
3
+ }
@@ -1 +1 @@
1
- export type { Head, UseSeoMetaInput } from '@unhead/schema';
1
+ export type { Head, UseSeoMetaInput } from '@unhead/vue';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-seo-utils",
3
3
  "type": "module",
4
- "version": "6.0.11",
4
+ "version": "7.0.0",
5
5
  "description": "SEO utilities to improve your Nuxt sites discoverability and shareability",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/harlan-zw",
@@ -29,44 +29,50 @@
29
29
  "externals": [
30
30
  "@unhead/shared",
31
31
  "@unhead/schema",
32
- "unhead"
32
+ "unhead",
33
+ "@unhead/vue/utils"
33
34
  ]
34
35
  },
36
+ "peerDependencies": {
37
+ "@unhead/vue": "^1 || ^2"
38
+ },
35
39
  "dependencies": {
36
- "@nuxt/kit": "^3.15.4",
37
- "@unhead/addons": "^1.11.19",
40
+ "@nuxt/kit": "^3.16.0",
41
+ "@unhead/addons": "2.0.0-rc.9",
38
42
  "defu": "^6.1.4",
39
43
  "escape-string-regexp": "^5.0.0",
40
44
  "fast-glob": "^3.3.3",
41
45
  "image-size": "^1.2.0",
42
- "mlly": "^1.7.4",
43
- "nuxt-site-config": "^3.1.0",
46
+ "nuxt-site-config": "^3.1.4",
44
47
  "pathe": "^2.0.3",
48
+ "pkg-types": "^2.1.0",
45
49
  "scule": "^1.3.0",
50
+ "semver": "^7.7.1",
46
51
  "ufo": "^1.5.4"
47
52
  },
48
53
  "devDependencies": {
49
- "@antfu/eslint-config": "4.3.0",
54
+ "@antfu/eslint-config": "4.8.1",
50
55
  "@nuxt/module-builder": "^0.8.4",
51
- "@nuxt/test-utils": "^3.15.4",
52
- "@nuxt/ui": "^2.21.0",
56
+ "@nuxt/test-utils": "^3.17.2",
57
+ "@nuxt/ui": "^2.21.1",
53
58
  "@nuxtjs/eslint-config-typescript": "^12.1.0",
54
- "@nuxtjs/i18n": "^9.2.0",
55
- "@unhead/schema": "^1.11.19",
56
- "@unhead/shared": "^1.11.19",
59
+ "@nuxtjs/i18n": "^9.3.0",
60
+ "@unhead/schema": "^1.11.20",
61
+ "@unhead/shared": "^1.11.20",
62
+ "@unhead/vue": "2.0.0-rc.9",
57
63
  "@vue/test-utils": "^2.4.6",
58
64
  "bumpp": "^10.0.3",
59
65
  "case-police": "^1.0.0",
60
66
  "cheerio": "1.0.0",
61
- "eslint": "^9.20.1",
62
- "happy-dom": "^17.1.1",
67
+ "eslint": "^9.22.0",
68
+ "happy-dom": "^17.4.2",
63
69
  "markdownlint-cli": "^0.44.0",
64
- "nuxt": "^3.15.4",
70
+ "nuxt": "^3.16.0",
65
71
  "nuxt-content-twoslash": "^0.1.2",
66
- "playwright-core": "^1.50.1",
67
- "sass": "^1.85.0",
68
- "typescript": "5.6.3",
69
- "vitest": "^3.0.6"
72
+ "playwright-core": "^1.51.0",
73
+ "sass": "^1.85.1",
74
+ "typescript": "5.8.2",
75
+ "vitest": "^3.0.8"
70
76
  },
71
77
  "resolutions": {
72
78
  "typescript": "5.6.3"