nuxt-seo-utils 7.0.2 → 7.0.4

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
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { UseSeoMetaInput } from '@unhead/vue/types';
2
+ import { MetaFlatSerializable } from '../dist/runtime/types.js';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
@@ -101,7 +101,7 @@ interface ModuleOptions {
101
101
  /**
102
102
  * The SEO meta object to be used as defaults.
103
103
  */
104
- meta: UseSeoMetaInput;
104
+ meta: MetaFlatSerializable;
105
105
  /**
106
106
  * Enables debug logs and a debug endpoint.
107
107
  *
package/dist/module.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- import { UseSeoMetaInput } from '@unhead/vue/types';
2
+ import { MetaFlatSerializable } from '../dist/runtime/types.js';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
@@ -101,7 +101,7 @@ interface ModuleOptions {
101
101
  /**
102
102
  * The SEO meta object to be used as defaults.
103
103
  */
104
- meta: UseSeoMetaInput;
104
+ meta: MetaFlatSerializable;
105
105
  /**
106
106
  * Enables debug logs and a debug endpoint.
107
107
  *
package/dist/module.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "nuxt": ">=3.16.0",
6
6
  "bridge": false
7
7
  },
8
- "version": "7.0.1",
8
+ "version": "7.0.3",
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, resolvePath } from '@nuxt/kit';
1
+ import { useNuxt, createResolver, addTemplate, defineNuxtModule, useLogger, addServerHandler, hasNuxtModule, addPlugin, addImports, addVitePlugin } from '@nuxt/kit';
2
2
  import UnheadVite from '@unhead/addons/vite';
3
3
  import { defu } from 'defu';
4
4
  import { installNuxtSiteConfig } from 'nuxt-site-config/kit';
5
- import { relative, resolve, extname, dirname, basename } from 'pathe';
6
5
  import { unpackMeta } from '@unhead/vue/utils';
7
6
  import fg from 'fast-glob';
7
+ import { relative, resolve, extname, dirname, basename } from 'pathe';
8
8
  import fs from 'node:fs';
9
9
  import { encodePath, joinURL } from 'ufo';
10
10
  import escapeRE from 'escape-string-regexp';
@@ -12,8 +12,8 @@ import { readFile } from 'node:fs/promises';
12
12
  import imageSize from 'image-size';
13
13
 
14
14
  function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
15
- const seoMeta = nuxt.options.app?.seoMeta || nuxt.options.app?.head?.seoMeta || false;
16
- const configSeoMeta = nuxt.options.seo?.meta || nuxt.options.seo?.seoMeta || false;
15
+ const seoMeta = nuxt.options.app?.seoMeta || nuxt.options.app?.head?.seoMeta || {};
16
+ const configSeoMeta = nuxt.options.seo?.meta || nuxt.options.seo?.seoMeta || {};
17
17
  if (!seoMeta && !configSeoMeta)
18
18
  return;
19
19
  nuxt.options.app.head = nuxt.options.app.head || {};
@@ -21,7 +21,8 @@ function extendNuxtConfigAppHeadSeoMeta(nuxt = useNuxt()) {
21
21
  ...nuxt.options.app.head,
22
22
  meta: [
23
23
  ...nuxt.options.app.head.meta || [],
24
- ...unpackMeta(seoMeta || {}),
24
+ // @ts-expect-error untyped
25
+ ...unpackMeta(seoMeta),
25
26
  ...unpackMeta(configSeoMeta || {})
26
27
  ]
27
28
  };
@@ -499,32 +500,25 @@ const module = defineNuxtModule({
499
500
  setupNuxtConfigAppHeadWithMoreDefaults(nuxt);
500
501
  if (config.extendNuxtConfigAppHeadTypes)
501
502
  extendNuxtConfigAppHeadTypes();
502
- extendTypes("nuxt-seo-utils", async () => {
503
- const pkg = "@unhead/vue/types";
504
- let unheadSchemaPath;
505
- try {
506
- unheadSchemaPath = relative(resolve(nuxt.options.rootDir, nuxt.options.buildDir, "module"), await resolvePath(pkg));
507
- } catch {
508
- unheadSchemaPath = pkg;
509
- }
503
+ extendTypes("nuxt-seo-utils", async (ctx) => {
510
504
  return `
511
505
  declare module 'nitropack' {
512
506
  interface NitroRouteRules {
513
- seoMeta?: import('${unheadSchemaPath}').UseSeoMetaInput
514
- head?: import('${unheadSchemaPath}').Head
507
+ seoMeta?: import('${ctx.typesPath}').MetaFlatSerializable
508
+ head?: import('${ctx.typesPath}').Head
515
509
  }
516
510
  interface NitroRouteConfig {
517
- seoMeta?: import('${unheadSchemaPath}').UseSeoMetaInput
518
- head?: import('${unheadSchemaPath}').Head
511
+ seoMeta?: import('${ctx.typesPath}').MetaFlatSerializable
512
+ head?: import('${ctx.typesPath}').Head
519
513
  }
520
514
  }
521
515
 
522
516
  declare module '@nuxt/schema' {
523
- interface AppHeadMetaObject { seoMeta?: import('${unheadSchemaPath}').UseSeoMetaInput }
517
+ interface AppHeadMetaObject { seoMeta?: import('${ctx.typesPath}').MetaFlatSerializable }
524
518
  }
525
519
 
526
520
  declare module 'nuxt/schema' {
527
- interface AppHeadMetaObject { seoMeta?: import('${unheadSchemaPath}').UseSeoMetaInput }
521
+ interface AppHeadMetaObject { seoMeta?: import('${ctx.typesPath}').MetaFlatSerializable }
528
522
  }
529
523
  `;
530
524
  });
@@ -536,7 +530,7 @@ declare module 'nuxt/schema' {
536
530
  if (config.mergeWithSiteConfig)
537
531
  addPlugin({ src: resolve(appRuntimeDir, "plugins", "siteConfig") });
538
532
  if (config.extendRouteRules)
539
- addPlugin({ src: resolve(appRuntimeDir, "plugins", "0.routeRules.server"), mode: "server" });
533
+ addPlugin({ src: resolve(appRuntimeDir, "plugins", "0.routeRules") });
540
534
  if (config.fixRequiredAbsoluteMetaTagsLinks)
541
535
  addPlugin({ src: resolve(appRuntimeDir, "plugins", "1.absoluteImageUrls.server"), mode: "server" });
542
536
  }
@@ -6,7 +6,7 @@ interface NuxtUIBreadcrumbItem extends NuxtLinkProps {
6
6
  icon?: string;
7
7
  iconClass?: string;
8
8
  as?: string;
9
- type?: string;
9
+ type?: 'button' | 'submit' | 'reset';
10
10
  disabled?: boolean;
11
11
  active?: boolean;
12
12
  exact?: boolean;
@@ -7,9 +7,9 @@ import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
7
7
  import { createSitePathResolver } from "#site-config/app/composables/utils";
8
8
  import { defu } from "defu";
9
9
  import { fixSlashes } from "nuxt-site-config/urls";
10
- import { useRouter } from "nuxt/app";
10
+ import { useNuxtApp, useRoute, useRouter, useState } from "nuxt/app";
11
11
  import { withoutTrailingSlash } from "ufo";
12
- import { computed, getCurrentInstance, inject, onUnmounted, provide, ref, toRaw, toValue } from "vue";
12
+ import { computed, getCurrentInstance, inject, onScopeDispose, onUnmounted, 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];
@@ -21,6 +21,7 @@ const BreadcrumbCtx = Symbol("BreadcrumbCtx");
21
21
  export function useBreadcrumbItems(_options = {}) {
22
22
  const vm = getCurrentInstance();
23
23
  const id = _options.id || "breadcrumb";
24
+ const pauseUpdates = ref(import.meta.client);
24
25
  let stateRef = null;
25
26
  if (vm) {
26
27
  stateRef = inject(BreadcrumbCtx, null);
@@ -35,24 +36,50 @@ export function useBreadcrumbItems(_options = {}) {
35
36
  const idx = state[id].push(_options) - 1;
36
37
  stateRef.value = state;
37
38
  onUnmounted(() => {
38
- stateRef.value = Object.fromEntries(Object.entries(stateRef.value).map(([k, v]) => {
39
- if (k === id) {
40
- return v.filter((_, i) => i !== idx);
39
+ const state2 = toRaw(stateRef.value);
40
+ if (state2[id]) {
41
+ state2[id] = state2[id].map((_, i) => i === idx ? false : _);
42
+ stateRef.value = state2;
43
+ }
44
+ });
45
+ if (import.meta.client) {
46
+ const nuxtApp = useNuxtApp();
47
+ const _ = [];
48
+ _.push(nuxtApp.hooks.hook("page:start", () => {
49
+ pauseUpdates.value = true;
50
+ }));
51
+ _.push(nuxtApp.hooks.hook("page:finish", () => {
52
+ if (!nuxtApp.isHydrating) {
53
+ pauseUpdates.value = false;
41
54
  }
42
- return v;
43
55
  }));
44
- });
56
+ _.push(nuxtApp.hooks.hook("app:error", () => {
57
+ pauseUpdates.value = false;
58
+ }));
59
+ _.push(nuxtApp.hooks.hook("app:suspense:resolve", () => {
60
+ pauseUpdates.value = false;
61
+ }));
62
+ onScopeDispose(() => {
63
+ _.forEach((hook) => hook?.());
64
+ _.length = 0;
65
+ });
66
+ }
45
67
  }
68
+ const route = useRoute();
46
69
  const router = useRouter();
47
70
  const i18n = useI18n();
48
71
  const siteResolver = createSitePathResolver({
49
72
  canonical: true,
50
73
  absolute: true
51
74
  });
75
+ const lastBreadcrumbs = useState(`nuxt-seo:breadcrumb:${id}`, () => []);
52
76
  const siteConfig = useSiteConfig();
53
77
  const items = computed(() => {
78
+ if (import.meta.client && pauseUpdates.value) {
79
+ return lastBreadcrumbs.value;
80
+ }
54
81
  const optionStack = stateRef?.value?.[id] || [_options];
55
- const flatOptions = toRaw([...optionStack]).reduce((acc, cur) => {
82
+ const flatOptions = toRaw([...optionStack]).filter(Boolean).reduce((acc, cur) => {
56
83
  acc.rootSegment = acc.rootSegment || cur.rootSegment;
57
84
  acc.path = acc.path || cur.path;
58
85
  return acc;
@@ -62,8 +89,8 @@ export function useBreadcrumbItems(_options = {}) {
62
89
  if (i18n.strategy === "prefix" || i18n.strategy !== "no_prefix" && toValue(i18n.defaultLocale) !== toValue(i18n.locale))
63
90
  rootNode = `${rootNode}${toValue(i18n.locale)}`;
64
91
  }
65
- const current = withoutQuery(withoutTrailingSlash(toValue(flatOptions.path || router.currentRoute.value?.path) || rootNode));
66
- const allOverrides = toRaw([...optionStack])?.map((opts) => toValue(opts.overrides)).filter(Boolean);
92
+ const current = withoutQuery(withoutTrailingSlash(toValue(flatOptions.path) || toRaw(route).path || rootNode));
93
+ const allOverrides = toRaw([...optionStack]).filter(Boolean)?.map((opts) => toValue(opts.overrides)).filter(Boolean);
67
94
  const flatOverrides = allOverrides?.reduce((acc, i) => {
68
95
  if (i) {
69
96
  i.forEach((item, index) => {
@@ -85,25 +112,25 @@ export function useBreadcrumbItems(_options = {}) {
85
112
  }
86
113
  return item;
87
114
  });
88
- const allPrepends = toRaw([...optionStack]).flatMap((opts) => toValue(opts.prepend)).filter(Boolean);
89
- const allAppends = toRaw([...optionStack]).flatMap((opts) => toValue(opts.append)).filter(Boolean);
115
+ const allPrepends = toRaw([...optionStack]).filter(Boolean).flatMap((opts) => toValue(opts.prepend)).filter(Boolean);
116
+ const allAppends = toRaw([...optionStack]).filter(Boolean).flatMap((opts) => toValue(opts.append)).filter(Boolean);
90
117
  if (allPrepends.length)
91
118
  segments.unshift(...allPrepends);
92
119
  if (allAppends.length)
93
120
  segments.push(...allAppends);
94
- return segments.filter(Boolean).map((item) => {
121
+ lastBreadcrumbs.value = segments.filter(Boolean).map((item) => {
95
122
  let fallbackLabel = titleCase(String((item.to || "").split("/").pop()));
96
123
  let fallbackAriaLabel = "";
97
- const route = item.to ? router.resolve(item.to)?.matched?.[0] : null;
98
- if (route) {
99
- const routeMeta = route?.meta || {};
124
+ const route2 = item.to ? router.resolve(item.to)?.matched?.[0] : null;
125
+ if (route2) {
126
+ const routeMeta = route2?.meta || {};
100
127
  if (routeMeta.breadcrumb) {
101
128
  item = {
102
129
  ...item,
103
130
  ...routeMeta.breadcrumb
104
131
  };
105
132
  }
106
- const routeName = String(route.name).split("___")[0];
133
+ const routeName = String(route2.name).split("___")[0];
107
134
  if (routeName === "index") {
108
135
  fallbackLabel = "Home";
109
136
  }
@@ -125,10 +152,12 @@ export function useBreadcrumbItems(_options = {}) {
125
152
  }
126
153
  return m;
127
154
  }).filter(Boolean);
155
+ return lastBreadcrumbs.value;
128
156
  });
129
157
  const schemaOrgEnabled = typeof _options.schemaOrg === "undefined" ? true : _options.schemaOrg;
130
158
  if ((import.meta.dev || import.meta.server || import.meta.env?.NODE_ENV === "test") && schemaOrgEnabled) {
131
159
  useSchemaOrg([
160
+ // @ts-expect-error untyped
132
161
  defineBreadcrumb({
133
162
  id: `#${id}`,
134
163
  itemListElement: computed(() => items.value.map((item) => ({
@@ -1,10 +1,13 @@
1
- import { useHead, useSeoMeta } from "#imports";
1
+ import { injectHead, useHead, useSeoMeta } from "#imports";
2
2
  import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
3
3
  import { createSitePathResolver } from "#site-config/app/composables/utils";
4
+ import { TemplateParamsPlugin } from "@unhead/vue/plugins";
4
5
  import { useError, useRoute, useRuntimeConfig } from "nuxt/app";
5
6
  import { stringifyQuery } from "ufo";
6
- import { computed, ref, watch } from "vue";
7
+ import { computed } from "vue";
7
8
  export function applyDefaults(i18n) {
9
+ const head = injectHead();
10
+ head.use(TemplateParamsPlugin);
8
11
  const { canonicalQueryWhitelist, canonicalLowercase } = useRuntimeConfig().public["seo-utils"];
9
12
  const siteConfig = useSiteConfig();
10
13
  const route = useRoute();
@@ -12,42 +15,52 @@ export function applyDefaults(i18n) {
12
15
  const err = useError();
13
16
  const canonicalUrl = computed(() => {
14
17
  if (err.value) {
15
- return null;
18
+ return false;
16
19
  }
17
20
  const { query } = route;
18
21
  let url = resolveUrl(route.path || "/").value || route.path;
19
22
  if (canonicalLowercase) {
20
- url = url.toLocaleLowerCase(siteConfig.currentLocale);
23
+ try {
24
+ url = url.toLocaleLowerCase(siteConfig.currentLocale || "en");
25
+ } catch {
26
+ url = url.toLowerCase();
27
+ }
21
28
  }
22
29
  const filteredQuery = Object.fromEntries(
23
30
  Object.entries(query).filter(([key]) => canonicalQueryWhitelist.includes(key)).sort(([a], [b]) => a.localeCompare(b))
24
31
  // Sort params
25
32
  );
26
- return Object.keys(filteredQuery).length ? `${url}?${stringifyQuery(filteredQuery)}` : url;
33
+ const href = Object.keys(filteredQuery).length ? `${url}?${stringifyQuery(filteredQuery)}` : url;
34
+ return { rel: "canonical", href };
27
35
  });
28
- const templateParams = ref({ site: siteConfig, siteName: siteConfig.name });
29
- if (import.meta.client) {
30
- watch(siteConfig, (v) => {
31
- templateParams.value = { site: v, siteName: v.name || "" };
32
- }, {
33
- deep: true
34
- });
35
- }
36
- useHead({ templateParams });
37
36
  const minimalPriority = {
38
37
  // give nuxt.config values higher priority
39
- tagPriority: 101
38
+ tagPriority: "low"
40
39
  };
41
40
  useHead({
42
41
  htmlAttrs: { lang: i18n.locale },
43
- templateParams,
42
+ templateParams: {
43
+ site: () => siteConfig,
44
+ siteName: () => siteConfig.name
45
+ },
44
46
  titleTemplate: "%s %separator %siteName",
45
- link: [{ rel: "canonical", href: () => canonicalUrl.value }]
47
+ link: [() => canonicalUrl.value]
46
48
  }, minimalPriority);
47
49
  const seoMeta = {
48
50
  ogType: "website",
49
- ogUrl: () => canonicalUrl.value,
50
- ogLocale: () => i18n.locale.value,
51
+ ogUrl: () => {
52
+ const url = canonicalUrl.value;
53
+ return url ? url.href : false;
54
+ },
55
+ ogLocale: () => {
56
+ if (i18n.locale.value) {
57
+ const locale = i18n.locale.value.replace("-", "_");
58
+ if (locale.includes("_")) {
59
+ return locale;
60
+ }
61
+ }
62
+ return false;
63
+ },
51
64
  ogSiteName: siteConfig.name
52
65
  };
53
66
  if (siteConfig.description)
@@ -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,24 @@
1
+ import { injectHead, useSeoMeta } from "#imports";
2
+ import { defineNuxtPlugin, getRouteRules, useRequestEvent, useState } from "nuxt/app";
3
+ export default defineNuxtPlugin({
4
+ enforce: "post",
5
+ async setup() {
6
+ const head = injectHead();
7
+ const routeRuleState = useState("nuxt-seo-utils:routeRules", () => null);
8
+ if (import.meta.server) {
9
+ const event = useRequestEvent();
10
+ const routeRules = await getRouteRules(event);
11
+ routeRuleState.value = {
12
+ head: routeRules.head,
13
+ seoMeta: routeRules.seoMeta
14
+ };
15
+ }
16
+ if (routeRuleState.value) {
17
+ const { head: headInput, seoMeta } = routeRuleState.value;
18
+ if (headInput)
19
+ head.push(headInput);
20
+ if (seoMeta)
21
+ useSeoMeta(seoMeta);
22
+ }
23
+ }
24
+ });
@@ -1,2 +1,2 @@
1
- declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
2
  export default _default;
@@ -8,7 +8,6 @@ export default defineNuxtPlugin({
8
8
  islands: false
9
9
  },
10
10
  // we need to wait for the i18n plugin to run first
11
- // @ts-expect-error dynamic
12
11
  dependsOn: ["nuxt-site-config:i18n"],
13
12
  setup(nuxtApp) {
14
13
  const siteConfig = useSiteConfig();
@@ -1,2 +1,2 @@
1
- declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
1
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
2
2
  export default _default;
@@ -12,7 +12,7 @@ export default defineNuxtPlugin({
12
12
  const route = useRoute();
13
13
  const err = useError();
14
14
  const title = computed(() => {
15
- if ([404, 500].includes(err.value?.statusCode)) {
15
+ if (err.value && [404, 500].includes(err.value?.statusCode)) {
16
16
  return `${err.value.statusCode} - ${err.value.message}`;
17
17
  }
18
18
  if (typeof route.meta?.title === "string")
@@ -1,3 +1,3 @@
1
- {
2
- "extends": "../../../../.nuxt/tsconfig.server.json"
3
- }
1
+ {
2
+ "extends": "../../../../.nuxt/tsconfig.server.json"
3
+ }
@@ -1 +1,6 @@
1
- export type { Head, UseSeoMetaInput } from '@unhead/vue';
1
+ import type { Head, MetaFlat, RawInput } from '@unhead/vue/types';
2
+ export type MetaFlatSerializable = MetaFlat & {
3
+ title?: RawInput<'title'>;
4
+ titleTemplate?: RawInput<'titleTemplate'>;
5
+ };
6
+ export type { Head };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-seo-utils",
3
3
  "type": "module",
4
- "version": "7.0.2",
4
+ "version": "7.0.4",
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",
@@ -27,23 +27,20 @@
27
27
  ],
28
28
  "build": {
29
29
  "externals": [
30
- "@unhead/shared",
31
- "@unhead/schema",
32
30
  "unhead",
31
+ "@unhead/vue",
32
+ "@unhead/vue/plugins",
33
33
  "@unhead/vue/utils"
34
34
  ]
35
35
  },
36
- "peerDependencies": {
37
- "@unhead/vue": "^2.0.0-rc.9"
38
- },
39
36
  "dependencies": {
40
37
  "@nuxt/kit": "^3.16.0",
41
- "@unhead/addons": "2.0.0-rc.9",
38
+ "@unhead/addons": "^2.0.0-rc.12",
42
39
  "defu": "^6.1.4",
43
40
  "escape-string-regexp": "^5.0.0",
44
41
  "fast-glob": "^3.3.3",
45
42
  "image-size": "^2.0.0",
46
- "nuxt-site-config": "^3.1.4",
43
+ "nuxt-site-config": "^3.1.5",
47
44
  "pathe": "^2.0.3",
48
45
  "pkg-types": "^2.1.0",
49
46
  "scule": "^1.3.0",
@@ -51,28 +48,31 @@
51
48
  "ufo": "^1.5.4"
52
49
  },
53
50
  "devDependencies": {
54
- "@antfu/eslint-config": "4.8.1",
51
+ "@antfu/eslint-config": "4.10.1",
55
52
  "@nuxt/module-builder": "^0.8.4",
53
+ "@nuxt/schema": "3.16.0",
56
54
  "@nuxt/test-utils": "^3.17.2",
57
- "@nuxt/ui": "^2.21.1",
55
+ "@nuxt/ui": "^3.0.0",
58
56
  "@nuxtjs/eslint-config-typescript": "^12.1.0",
59
57
  "@nuxtjs/i18n": "^9.3.1",
60
- "@unhead/vue": "2.0.0-rc.9",
58
+ "@unhead/vue": "2.0.0-rc.12",
61
59
  "@vue/test-utils": "^2.4.6",
62
- "bumpp": "^10.0.3",
60
+ "bumpp": "^10.1.0",
63
61
  "case-police": "^1.0.0",
64
62
  "cheerio": "1.0.0",
65
63
  "eslint": "^9.22.0",
66
- "happy-dom": "^17.4.3",
64
+ "happy-dom": "^17.4.4",
67
65
  "markdownlint-cli": "^0.44.0",
68
66
  "nuxt": "^3.16.0",
69
67
  "nuxt-content-twoslash": "^0.1.2",
70
68
  "playwright-core": "^1.51.0",
71
69
  "sass": "^1.85.1",
72
70
  "typescript": "5.8.2",
73
- "vitest": "^3.0.8"
71
+ "vitest": "^3.0.8",
72
+ "vue-tsc": "^2.2.8"
74
73
  },
75
74
  "resolutions": {
75
+ "nuxt": "3.16.0",
76
76
  "typescript": "5.6.3"
77
77
  },
78
78
  "scripts": {
@@ -81,7 +81,8 @@
81
81
  "build": "pnpm stub && nuxt-module-build build",
82
82
  "dev": "nuxi dev playground",
83
83
  "dev:build": "nuxi build playground",
84
- "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
84
+ "typecheck": "vue-tsc --noEmit",
85
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare test/fixtures/basic && nuxi prepare test/fixtures/i18n",
85
86
  "release": "pnpm build && bumpp -x \"npx changelogen --output=CHANGELOG.log\" && pnpm publish",
86
87
  "lint:docs": "markdownlint ./docs/content && case-police 'docs/content/**/*.md' *.md",
87
88
  "lint:docs:fix": "markdownlint ./docs/content --fix && case-police 'docs/content/**/*.md' *.md --fix",
@@ -1,2 +0,0 @@
1
- declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
2
- export default _default;
@@ -1,20 +0,0 @@
1
- import { injectHead } from "#imports";
2
- import { unpackMeta } from "@unhead/vue/utils";
3
- import { defineNuxtPlugin, useRequestEvent } from "nuxt/app";
4
- export default defineNuxtPlugin({
5
- enforce: "post",
6
- async setup() {
7
- const head = injectHead();
8
- if (!head)
9
- return;
10
- const event = useRequestEvent();
11
- if (event.context._nitro.routeRules.head)
12
- head.push(event.context._nitro.routeRules.head, { mode: "server", tagPriority: -9 });
13
- if (event.context._nitro.routeRules.seoMeta) {
14
- const meta = unpackMeta({ ...event.context._nitro.routeRules.seoMeta });
15
- head.push({
16
- meta
17
- }, { mode: "server", tagPriority: -9 });
18
- }
19
- }
20
- });