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 +9 -9
- package/README.md +74 -74
- package/dist/module.d.mts +2 -2
- package/dist/module.d.ts +2 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +14 -20
- package/dist/runtime/app/composables/useBreadcrumbItems.d.ts +1 -1
- package/dist/runtime/app/composables/useBreadcrumbItems.js +46 -17
- package/dist/runtime/app/logic/applyDefaults.js +32 -19
- package/dist/runtime/app/plugins/0.routeRules.d.ts +2 -0
- package/dist/runtime/app/plugins/0.routeRules.js +24 -0
- package/dist/runtime/app/plugins/1.absoluteImageUrls.server.d.ts +1 -1
- package/dist/runtime/app/plugins/defaults.d.ts +1 -1
- package/dist/runtime/app/plugins/defaultsWaitI18n.d.ts +1 -1
- package/dist/runtime/app/plugins/defaultsWaitI18n.js +0 -1
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.d.ts +1 -1
- package/dist/runtime/app/plugins/siteConfig.d.ts +1 -1
- package/dist/runtime/app/plugins/titles.d.ts +1 -1
- package/dist/runtime/app/plugins/titles.js +1 -1
- package/dist/runtime/server/tsconfig.json +3 -3
- package/dist/runtime/types.d.ts +6 -1
- package/package.json +16 -15
- package/dist/runtime/app/plugins/0.routeRules.server.d.ts +0 -2
- package/dist/runtime/app/plugins/0.routeRules.server.js +0 -20
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 {
|
|
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:
|
|
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 {
|
|
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:
|
|
104
|
+
meta: MetaFlatSerializable;
|
|
105
105
|
/**
|
|
106
106
|
* Enables debug logs and a debug endpoint.
|
|
107
107
|
*
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { useNuxt, createResolver, addTemplate, defineNuxtModule, useLogger, addServerHandler, hasNuxtModule, addPlugin, addImports, addVitePlugin
|
|
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 ||
|
|
16
|
-
const configSeoMeta = nuxt.options.seo?.meta || nuxt.options.seo?.seoMeta ||
|
|
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
|
-
|
|
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('${
|
|
514
|
-
head?: import('${
|
|
507
|
+
seoMeta?: import('${ctx.typesPath}').MetaFlatSerializable
|
|
508
|
+
head?: import('${ctx.typesPath}').Head
|
|
515
509
|
}
|
|
516
510
|
interface NitroRouteConfig {
|
|
517
|
-
seoMeta?: import('${
|
|
518
|
-
head?: import('${
|
|
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('${
|
|
517
|
+
interface AppHeadMetaObject { seoMeta?: import('${ctx.typesPath}').MetaFlatSerializable }
|
|
524
518
|
}
|
|
525
519
|
|
|
526
520
|
declare module 'nuxt/schema' {
|
|
527
|
-
interface AppHeadMetaObject { seoMeta?: import('${
|
|
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
|
|
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
|
}
|
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 ||
|
|
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
|
-
|
|
121
|
+
lastBreadcrumbs.value = segments.filter(Boolean).map((item) => {
|
|
95
122
|
let fallbackLabel = titleCase(String((item.to || "").split("/").pop()));
|
|
96
123
|
let fallbackAriaLabel = "";
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
const routeMeta =
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: [
|
|
47
|
+
link: [() => canonicalUrl.value]
|
|
46
48
|
}, minimalPriority);
|
|
47
49
|
const seoMeta = {
|
|
48
50
|
ogType: "website",
|
|
49
|
-
ogUrl: () =>
|
|
50
|
-
|
|
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,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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
+
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "^
|
|
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.
|
|
58
|
+
"@unhead/vue": "2.0.0-rc.12",
|
|
61
59
|
"@vue/test-utils": "^2.4.6",
|
|
62
|
-
"bumpp": "^10.0
|
|
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.
|
|
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
|
-
"
|
|
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,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
|
-
});
|