@nuxtjs/seo 2.0.0-rc.2 → 2.0.0-rc.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -15
- package/dist/module.d.mts +5 -1
- package/dist/module.d.ts +5 -1
- package/dist/module.json +6 -2
- package/dist/module.mjs +44 -24
- package/dist/runtime/nitro/middleware/redirect.js +18 -0
- package/dist/runtime/nuxt/composables/useBreadcrumbItems.d.ts +45 -7
- package/dist/runtime/nuxt/composables/{useBreadcrumbItems.mjs → useBreadcrumbItems.js} +40 -19
- package/dist/runtime/nuxt/logic/applyDefaults.d.ts +4 -0
- package/dist/runtime/nuxt/logic/applyDefaults.js +48 -0
- package/dist/runtime/nuxt/plugin/defaults.d.ts +1 -1
- package/dist/runtime/nuxt/plugin/defaults.js +20 -0
- package/dist/runtime/nuxt/plugin/defaultsWaitI18n.d.ts +2 -0
- package/dist/runtime/nuxt/plugin/defaultsWaitI18n.js +23 -0
- package/dist/runtime/nuxt/plugin/titles.d.ts +1 -1
- package/dist/runtime/nuxt/plugin/{titles.mjs → titles.js} +3 -0
- package/dist/types.d.mts +1 -16
- package/dist/types.d.ts +1 -16
- package/package.json +43 -30
- package/dist/runtime/nitro/middleware/redirect.mjs +0 -14
- package/dist/runtime/nuxt/plugin/defaults.mjs +0 -49
- /package/dist/runtime/nuxt/composables/{polyfills.mjs → polyfills.js} +0 -0
- /package/dist/runtime/pure/{breadcrumbs.mjs → breadcrumbs.js} +0 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The complete SEO solution for Nuxt.
|
|
|
17
17
|
<tbody>
|
|
18
18
|
<td align="center">
|
|
19
19
|
<img width="800" height="0" /><br>
|
|
20
|
-
<i>Status:</i> <a href="https://github.com/harlan-zw/nuxt-seo/releases/tag/v2.0.0">v2
|
|
20
|
+
<i>Status:</i> <a href="https://github.com/harlan-zw/nuxt-seo/releases/tag/v2.0.0">v2 RC is available</a></b> <br>
|
|
21
21
|
<sup> Please report any issues 🐛</sup><br>
|
|
22
22
|
<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>
|
|
23
23
|
<img width="800" height="0" />
|
|
@@ -38,9 +38,9 @@ With powerful APIs built for fully dynamic sites and zero-config defaults for st
|
|
|
38
38
|
|
|
39
39
|
## Modules
|
|
40
40
|
|
|
41
|
-
- 📖 [
|
|
42
|
-
- 🤖 [
|
|
43
|
-
- 🔎 [nuxt-schema-org](https://
|
|
41
|
+
- 📖 [@nuxtjs/sitemap](https://github.com/nuxt-modules/sitemap) - Sitemap.xml Support
|
|
42
|
+
- 🤖 [@nuxtjs/robots](https://github.com/nuxt-modules/robots) - Manage site crawling
|
|
43
|
+
- 🔎 [nuxt-schema-org](https://github.com/harlan-zw/nuxt-schema-org) - Generate Schema.org JSON-LD for SEO
|
|
44
44
|
- △ [nuxt-seo-experiments](https://github.com/harlan-zw/nuxt-seo-experiments) - Experimental SEO meta features
|
|
45
45
|
- 🖼️ [nuxt-og-image](https://github.com/nuxt-modules/og-image) - Generate dynamic social share images
|
|
46
46
|
- ✅ [nuxt-link-checker](https://github.com/harlan-zw/nuxt-link-checker) - Check for broken links
|
|
@@ -50,23 +50,18 @@ With powerful APIs built for fully dynamic sites and zero-config defaults for st
|
|
|
50
50
|
1. Install `@nuxtjs/seo` dependency to your project:
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
|
-
|
|
54
|
-
yarn add -D @nuxtjs/seo
|
|
55
|
-
npm install -D @nuxtjs/seo
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
2. Add it to your `modules` section in your `nuxt.config`:
|
|
59
|
-
|
|
60
|
-
```ts [nuxt.config]
|
|
61
|
-
export default defineNuxtConfig({
|
|
62
|
-
modules: ['@nuxtjs/seo']
|
|
63
|
-
})
|
|
53
|
+
npx nuxi@latest module add seo
|
|
64
54
|
```
|
|
65
55
|
|
|
66
56
|
That's it!
|
|
67
57
|
|
|
68
58
|
All features are enabled by default. Learn more by exploring the [documentation](https://nuxtseo.com)
|
|
69
59
|
|
|
60
|
+
## Stackblitz
|
|
61
|
+
|
|
62
|
+
For reproductions and demo environments, you can use the [Nuxt SEO Basic Reproduction](https://stackblitz.com/edit/nuxt-starter-gfrej6?file=nuxt.config.ts)
|
|
63
|
+
template.
|
|
64
|
+
|
|
70
65
|
## Sponsors
|
|
71
66
|
|
|
72
67
|
<p align="center">
|
package/dist/module.d.mts
CHANGED
|
@@ -13,6 +13,10 @@ interface ModuleOptions {
|
|
|
13
13
|
* @default true
|
|
14
14
|
*/
|
|
15
15
|
automaticDefaults?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* When enabled, it will whitelist the query parameters that are allowed in the canonical URL.
|
|
18
|
+
*/
|
|
19
|
+
canonicalQueryWhitelist?: string[];
|
|
16
20
|
/**
|
|
17
21
|
* When enabled, it will redirect any request to the canonical domain (site url) using a 301 redirect on non-dev environments.
|
|
18
22
|
*
|
|
@@ -41,6 +45,6 @@ interface ModuleOptions {
|
|
|
41
45
|
*/
|
|
42
46
|
splash: boolean;
|
|
43
47
|
}
|
|
44
|
-
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
|
|
48
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
45
49
|
|
|
46
50
|
export { type ModuleOptions, _default as default };
|
package/dist/module.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ interface ModuleOptions {
|
|
|
13
13
|
* @default true
|
|
14
14
|
*/
|
|
15
15
|
automaticDefaults?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* When enabled, it will whitelist the query parameters that are allowed in the canonical URL.
|
|
18
|
+
*/
|
|
19
|
+
canonicalQueryWhitelist?: string[];
|
|
16
20
|
/**
|
|
17
21
|
* When enabled, it will redirect any request to the canonical domain (site url) using a 301 redirect on non-dev environments.
|
|
18
22
|
*
|
|
@@ -41,6 +45,6 @@ interface ModuleOptions {
|
|
|
41
45
|
*/
|
|
42
46
|
splash: boolean;
|
|
43
47
|
}
|
|
44
|
-
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
|
|
48
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
45
49
|
|
|
46
50
|
export { type ModuleOptions, _default as default };
|
package/dist/module.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxtseo",
|
|
3
3
|
"compatibility": {
|
|
4
|
-
"nuxt": "
|
|
4
|
+
"nuxt": ">=3.7.0",
|
|
5
5
|
"bridge": false
|
|
6
6
|
},
|
|
7
7
|
"configKey": "seo",
|
|
8
|
-
"version": "2.0.0-rc.
|
|
8
|
+
"version": "2.0.0-rc.19",
|
|
9
|
+
"builder": {
|
|
10
|
+
"@nuxt/module-builder": "0.8.3",
|
|
11
|
+
"unbuild": "2.0.0"
|
|
12
|
+
}
|
|
9
13
|
}
|
package/dist/module.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { defineNuxtModule,
|
|
2
|
-
import
|
|
1
|
+
import { defineNuxtModule, createResolver, useLogger, installModule, hasNuxtModule, addPlugin, addImports, addServerHandler } from '@nuxt/kit';
|
|
2
|
+
import { colors } from 'consola/utils';
|
|
3
3
|
import { installNuxtSiteConfig } from 'nuxt-site-config-kit';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { readPackageJSON } from 'pkg-types';
|
|
5
|
+
import { $fetch } from 'ofetch';
|
|
6
6
|
|
|
7
7
|
const Modules = [
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"@nuxtjs/robots",
|
|
9
|
+
"@nuxtjs/sitemap",
|
|
10
10
|
"nuxt-og-image",
|
|
11
11
|
"nuxt-schema-org",
|
|
12
12
|
"nuxt-seo-experiments",
|
|
@@ -16,7 +16,7 @@ const module = defineNuxtModule({
|
|
|
16
16
|
meta: {
|
|
17
17
|
name: "nuxtseo",
|
|
18
18
|
compatibility: {
|
|
19
|
-
nuxt: "
|
|
19
|
+
nuxt: ">=3.7.0",
|
|
20
20
|
bridge: false
|
|
21
21
|
},
|
|
22
22
|
configKey: "seo"
|
|
@@ -26,26 +26,45 @@ const module = defineNuxtModule({
|
|
|
26
26
|
enabled: true,
|
|
27
27
|
debug: nuxt.options.debug,
|
|
28
28
|
redirectToCanonicalSiteUrl: false,
|
|
29
|
-
splash:
|
|
29
|
+
splash: false,
|
|
30
|
+
// nuxt.options.dev, - figure out a solution for this in the future
|
|
30
31
|
automaticDefaults: true,
|
|
31
32
|
fallbackTitle: true
|
|
32
33
|
};
|
|
33
34
|
},
|
|
34
35
|
async setup(config, nuxt) {
|
|
35
|
-
const
|
|
36
|
+
const { resolve, resolvePath } = createResolver(import.meta.url);
|
|
37
|
+
const { name, version } = await readPackageJSON(resolve("../package.json"));
|
|
38
|
+
const logger = useLogger(name);
|
|
36
39
|
logger.level = config.debug ? 4 : 3;
|
|
37
40
|
if (config.enabled === false) {
|
|
38
41
|
logger.debug("The module is disabled, skipping setup.");
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
|
-
const { resolve, resolvePath } = createResolver(import.meta.url);
|
|
42
44
|
await installNuxtSiteConfig();
|
|
43
45
|
for (const module of Modules)
|
|
44
46
|
await installModule(await resolvePath(module));
|
|
47
|
+
nuxt.options.runtimeConfig.public["nuxt-seo"] = {
|
|
48
|
+
canonicalQueryWhitelist: config.canonicalQueryWhitelist || [
|
|
49
|
+
"page",
|
|
50
|
+
"sort",
|
|
51
|
+
"filter",
|
|
52
|
+
"search",
|
|
53
|
+
"q",
|
|
54
|
+
"category",
|
|
55
|
+
"tag"
|
|
56
|
+
]
|
|
57
|
+
};
|
|
45
58
|
if (config.automaticDefaults) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
if (hasNuxtModule("@nuxtjs/i18n")) {
|
|
60
|
+
addPlugin({
|
|
61
|
+
src: resolve(`./runtime/nuxt/plugin/defaultsWaitI18n`)
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
addPlugin({
|
|
65
|
+
src: resolve(`./runtime/nuxt/plugin/defaults`)
|
|
66
|
+
});
|
|
67
|
+
}
|
|
49
68
|
}
|
|
50
69
|
if (config.fallbackTitle) {
|
|
51
70
|
addPlugin({
|
|
@@ -67,33 +86,34 @@ const module = defineNuxtModule({
|
|
|
67
86
|
};
|
|
68
87
|
for (const [module, composables] of Object.entries(polyfills)) {
|
|
69
88
|
if (nuxt.options[module]?.enable === false) {
|
|
70
|
-
composables.forEach((
|
|
89
|
+
composables.forEach((name2) => {
|
|
71
90
|
addImports({
|
|
72
91
|
from: resolve("./runtime/nuxt/composables/polyfills"),
|
|
73
|
-
name
|
|
92
|
+
name: name2
|
|
74
93
|
});
|
|
75
94
|
});
|
|
76
95
|
}
|
|
77
96
|
}
|
|
78
97
|
nuxt.options.experimental.headNext = true;
|
|
79
|
-
if (config.redirectToCanonicalSiteUrl) {
|
|
98
|
+
if (!nuxt.options.dev && config.redirectToCanonicalSiteUrl) {
|
|
80
99
|
addServerHandler({
|
|
81
100
|
handler: resolve("./runtime/nitro/middleware/redirect"),
|
|
82
101
|
middleware: true
|
|
83
102
|
});
|
|
84
103
|
}
|
|
85
|
-
if (config.splash) {
|
|
104
|
+
if (config.splash && !version.includes("rc") && nuxt.options.dev) {
|
|
86
105
|
logger.log("");
|
|
87
106
|
let latestTag = `v${version}`;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
107
|
+
latestTag = (await $fetch("https://ungh.unjs.io/repos/harlan-zw/nuxt-seo/releases/latest", {
|
|
108
|
+
timeout: 2e3
|
|
109
|
+
}).catch(() => {
|
|
110
|
+
return { release: { tag: `v${version}` } };
|
|
111
|
+
})).release.tag;
|
|
92
112
|
const upToDate = latestTag === `v${version}`;
|
|
93
|
-
logger.log(`${
|
|
113
|
+
logger.log(`${colors.green("Nuxt SEO")} ${colors.yellow(`v${version}`)} ${colors.gray(`by ${colors.underline("@harlan_zw")}`)}`);
|
|
94
114
|
if (!upToDate)
|
|
95
|
-
logger.log(`${
|
|
96
|
-
logger.log(
|
|
115
|
+
logger.log(`${colors.gray(" \u251C\u2500 ")}\u{1F389} New version available!${colors.gray(` Run ${colors.underline(`npm i @nuxtjs/seo@${latestTag}`)} to update.`)}`);
|
|
116
|
+
logger.log(colors.dim(" \u2514\u2500 \u{1F9EA} Help get Nuxt SEO stable by providing feedback https://github.com/harlan-zw/nuxt-seo/discussions/108"));
|
|
97
117
|
logger.log("");
|
|
98
118
|
}
|
|
99
119
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineEventHandler, sendRedirect, setHeader } from "h3";
|
|
2
|
+
import { joinURL } from "ufo";
|
|
3
|
+
import { useNitroOrigin, useSiteConfig } from "#imports";
|
|
4
|
+
export default defineEventHandler((e) => {
|
|
5
|
+
const siteConfig = useSiteConfig(e);
|
|
6
|
+
if (siteConfig.url) {
|
|
7
|
+
const siteConfigHostName = new URL(e.path, siteConfig.url).hostname;
|
|
8
|
+
const origin = useNitroOrigin(e);
|
|
9
|
+
const originHostname = new URL(e.path, origin).hostname;
|
|
10
|
+
if (e.headers.get("x-nuxt-seo-redirected")) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (originHostname !== siteConfigHostName && originHostname !== new URL(e.path, origin).hostname) {
|
|
14
|
+
setHeader(e, "x-nuxt-seo-redirected", "true");
|
|
15
|
+
return sendRedirect(e, joinURL(siteConfig.url, e.path), 301);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
import type { MaybeRefOrGetter } from 'vue';
|
|
2
|
-
import type {
|
|
2
|
+
import type { NuxtLinkProps } from 'nuxt/app';
|
|
3
|
+
interface NuxtUIBreadcrumbItem extends NuxtLinkProps {
|
|
4
|
+
label: string;
|
|
5
|
+
labelClass?: string;
|
|
6
|
+
icon?: string;
|
|
7
|
+
iconClass?: string;
|
|
8
|
+
as?: string;
|
|
9
|
+
type?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
active?: boolean;
|
|
12
|
+
exact?: boolean;
|
|
13
|
+
exactQuery?: boolean;
|
|
14
|
+
exactMatch?: boolean;
|
|
15
|
+
inactiveClass?: string;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
3
18
|
export interface BreadcrumbProps {
|
|
19
|
+
/**
|
|
20
|
+
* Generate the breadcrumbs based on a different path than the current route.
|
|
21
|
+
*/
|
|
4
22
|
path?: MaybeRefOrGetter<string>;
|
|
23
|
+
/**
|
|
24
|
+
* The id of the breadcrumb list. It's recommended to provide a unique
|
|
25
|
+
* id when adding multiple breadcrumb lists to the same page.
|
|
26
|
+
*/
|
|
5
27
|
id?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Append additional breadcrumb items to the end of the list. This is applied
|
|
30
|
+
* after the `overrides` option.
|
|
31
|
+
*/
|
|
32
|
+
append?: BreadcrumbItemProps[];
|
|
33
|
+
/**
|
|
34
|
+
* Prepend additional breadcrumb items to the start of the list. This is applied
|
|
35
|
+
* after the `overrides` option.
|
|
36
|
+
*/
|
|
37
|
+
prepend?: BreadcrumbItemProps[];
|
|
38
|
+
/**
|
|
39
|
+
* Override any of the breadcrumb items based on the index.
|
|
40
|
+
*/
|
|
41
|
+
overrides?: (BreadcrumbItemProps | false | undefined)[];
|
|
42
|
+
/**
|
|
43
|
+
* Should the schema.org breadcrumb be generated.
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
6
46
|
schemaOrg?: boolean;
|
|
7
47
|
/**
|
|
8
48
|
* The Aria Label for the breadcrumbs.
|
|
@@ -22,7 +62,7 @@ export interface BreadcrumbProps {
|
|
|
22
62
|
*/
|
|
23
63
|
hideRoot?: MaybeRefOrGetter<boolean>;
|
|
24
64
|
}
|
|
25
|
-
export interface BreadcrumbItemProps extends
|
|
65
|
+
export interface BreadcrumbItemProps extends NuxtUIBreadcrumbItem {
|
|
26
66
|
/** Whether the breadcrumb item represents the aria-current. */
|
|
27
67
|
current?: boolean;
|
|
28
68
|
/**
|
|
@@ -30,12 +70,9 @@ export interface BreadcrumbItemProps extends BreadcrumbLink {
|
|
|
30
70
|
* @default 'page'
|
|
31
71
|
*/
|
|
32
72
|
ariaCurrent?: 'page' | 'step' | 'location' | 'date' | 'time' | boolean | 'true' | 'false';
|
|
33
|
-
|
|
34
|
-
disabled?: boolean;
|
|
35
|
-
to: string;
|
|
73
|
+
to?: string;
|
|
36
74
|
ariaLabel?: string;
|
|
37
75
|
separator?: boolean | string;
|
|
38
|
-
icon?: string;
|
|
39
76
|
class?: (string | string[] | undefined)[] | string;
|
|
40
77
|
/**
|
|
41
78
|
* @internal
|
|
@@ -45,4 +82,5 @@ export interface BreadcrumbItemProps extends BreadcrumbLink {
|
|
|
45
82
|
last: boolean;
|
|
46
83
|
};
|
|
47
84
|
}
|
|
48
|
-
export declare function useBreadcrumbItems(options?: BreadcrumbProps):
|
|
85
|
+
export declare function useBreadcrumbItems(options?: BreadcrumbProps): import("vue").ComputedRef<BreadcrumbItemProps[]>;
|
|
86
|
+
export {};
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { withoutTrailingSlash } from "ufo";
|
|
2
|
-
import {
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
import { fixSlashes } from "site-config-stack/urls";
|
|
4
|
+
import { pathBreadcrumbSegments } from "../../pure/breadcrumbs.js";
|
|
3
5
|
import {
|
|
4
6
|
computed,
|
|
7
|
+
createSitePathResolver,
|
|
5
8
|
defineBreadcrumb,
|
|
6
9
|
toValue,
|
|
7
10
|
useI18n,
|
|
8
11
|
useRoute,
|
|
9
12
|
useRouter,
|
|
10
13
|
useSchemaOrg,
|
|
11
|
-
|
|
14
|
+
useSiteConfig
|
|
12
15
|
} from "#imports";
|
|
13
16
|
function withoutQuery(path) {
|
|
14
17
|
return path.split("?")[0];
|
|
@@ -20,22 +23,41 @@ export function useBreadcrumbItems(options = {}) {
|
|
|
20
23
|
const router = useRouter();
|
|
21
24
|
const routes = router.getRoutes();
|
|
22
25
|
const i18n = useI18n();
|
|
26
|
+
const siteResolver = createSitePathResolver({
|
|
27
|
+
canonical: true,
|
|
28
|
+
absolute: true
|
|
29
|
+
});
|
|
30
|
+
const siteConfig = useSiteConfig();
|
|
23
31
|
const items = computed(() => {
|
|
24
32
|
let rootNode = "/";
|
|
25
33
|
if (i18n) {
|
|
26
|
-
if (i18n.strategy === "prefix" || i18n.strategy !== "no_prefix" && i18n.defaultLocale
|
|
27
|
-
rootNode = `/${i18n.
|
|
34
|
+
if (i18n.strategy === "prefix" || i18n.strategy !== "no_prefix" && toValue(i18n.defaultLocale) !== toValue(i18n.locale))
|
|
35
|
+
rootNode = `/${toValue(i18n.locale)}`;
|
|
28
36
|
}
|
|
29
37
|
const current = withoutQuery(withoutTrailingSlash(toValue(options.path || useRoute().path) || rootNode));
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const overrides = options.overrides || [];
|
|
39
|
+
const segments = pathBreadcrumbSegments(current, rootNode).map((path, index) => {
|
|
40
|
+
let item = {
|
|
41
|
+
to: path
|
|
42
|
+
};
|
|
43
|
+
if (typeof overrides[index] !== "undefined") {
|
|
44
|
+
if (overrides[index] === false)
|
|
45
|
+
return false;
|
|
46
|
+
item = defu(overrides[index], item);
|
|
47
|
+
}
|
|
48
|
+
return item;
|
|
49
|
+
});
|
|
50
|
+
if (options.prepend)
|
|
51
|
+
segments.unshift(...options.prepend);
|
|
52
|
+
if (options.append)
|
|
53
|
+
segments.push(...options.append);
|
|
54
|
+
return segments.filter(Boolean).map((item) => {
|
|
33
55
|
const route = routes.find((r) => withoutTrailingSlash(r.path) === withoutTrailingSlash(item.to));
|
|
34
56
|
const routeMeta = route?.meta || {};
|
|
35
57
|
const routeName = route ? String(route.name || route.path) : item.to === "/" ? "index" : "unknown";
|
|
36
58
|
let [name] = routeName.split("___");
|
|
37
59
|
if (name === "unknown")
|
|
38
|
-
name = item.to.split("/").pop() || "";
|
|
60
|
+
name = (item.to || "").split("/").pop() || "";
|
|
39
61
|
if (routeMeta.breadcrumb) {
|
|
40
62
|
item = {
|
|
41
63
|
...item,
|
|
@@ -54,24 +76,23 @@ export function useBreadcrumbItems(options = {}) {
|
|
|
54
76
|
return item;
|
|
55
77
|
}).map((m) => {
|
|
56
78
|
if (m && m.to) {
|
|
57
|
-
m.to =
|
|
79
|
+
m.to = fixSlashes(siteConfig.trailingSlash, m.to);
|
|
58
80
|
if (m.to === rootNode && toValue(options.hideRoot))
|
|
59
81
|
return false;
|
|
60
82
|
}
|
|
61
83
|
return m;
|
|
62
84
|
}).filter(Boolean);
|
|
63
85
|
});
|
|
64
|
-
|
|
86
|
+
const schemaOrgEnabled = typeof options.schemaOrg === "undefined" ? true : options.schemaOrg;
|
|
87
|
+
if ((import.meta.dev || import.meta.server) && schemaOrgEnabled) {
|
|
65
88
|
useSchemaOrg([
|
|
66
|
-
defineBreadcrumb(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
74
|
-
}))
|
|
89
|
+
defineBreadcrumb({
|
|
90
|
+
id: `#${options.id || "breadcrumb"}`,
|
|
91
|
+
itemListElement: computed(() => items.value.map((item) => ({
|
|
92
|
+
name: item.label || item.ariaLabel,
|
|
93
|
+
item: item.to ? siteResolver(item.to) : void 0
|
|
94
|
+
})))
|
|
95
|
+
})
|
|
75
96
|
]);
|
|
76
97
|
}
|
|
77
98
|
return items;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { stringifyQuery } from "ufo";
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
createSitePathResolver,
|
|
5
|
+
useHead,
|
|
6
|
+
useRoute,
|
|
7
|
+
useRuntimeConfig,
|
|
8
|
+
useSeoMeta,
|
|
9
|
+
useSiteConfig
|
|
10
|
+
} from "#imports";
|
|
11
|
+
export function applyDefaults(i18n) {
|
|
12
|
+
const { canonicalQueryWhitelist } = useRuntimeConfig().public["nuxt-seo"];
|
|
13
|
+
const siteConfig = useSiteConfig();
|
|
14
|
+
const route = useRoute();
|
|
15
|
+
const resolveUrl = createSitePathResolver({ withBase: true, absolute: true });
|
|
16
|
+
const canonicalUrl = computed(() => {
|
|
17
|
+
const { query } = route;
|
|
18
|
+
const url = resolveUrl(route.path || "/").value || route.path;
|
|
19
|
+
const filteredQuery = Object.fromEntries(
|
|
20
|
+
Object.entries(query).filter(([key]) => canonicalQueryWhitelist.includes(key))
|
|
21
|
+
);
|
|
22
|
+
return Object.keys(filteredQuery).length ? `${url}?${stringifyQuery(filteredQuery)}` : url;
|
|
23
|
+
});
|
|
24
|
+
const minimalPriority = {
|
|
25
|
+
// give nuxt.config values higher priority
|
|
26
|
+
tagPriority: 101
|
|
27
|
+
};
|
|
28
|
+
useHead({
|
|
29
|
+
htmlAttrs: { lang: i18n.locale },
|
|
30
|
+
templateParams: { site: siteConfig, siteName: siteConfig.name || "" },
|
|
31
|
+
titleTemplate: "%s %separator %siteName",
|
|
32
|
+
link: [{ rel: "canonical", href: () => canonicalUrl.value }]
|
|
33
|
+
}, minimalPriority);
|
|
34
|
+
const seoMeta = {
|
|
35
|
+
ogType: "website",
|
|
36
|
+
ogUrl: () => canonicalUrl.value,
|
|
37
|
+
ogLocale: () => i18n.locale.value,
|
|
38
|
+
ogSiteName: siteConfig.name
|
|
39
|
+
};
|
|
40
|
+
if (siteConfig.description)
|
|
41
|
+
seoMeta.description = siteConfig.description;
|
|
42
|
+
if (siteConfig.twitter) {
|
|
43
|
+
const id = siteConfig.twitter.startsWith("@") ? siteConfig.twitter : `@${siteConfig.twitter}`;
|
|
44
|
+
seoMeta.twitterCreator = id;
|
|
45
|
+
seoMeta.twitterSite = id;
|
|
46
|
+
}
|
|
47
|
+
useSeoMeta(seoMeta, minimalPriority);
|
|
48
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { applyDefaults } from "../logic/applyDefaults.js";
|
|
2
|
+
import {
|
|
3
|
+
defineNuxtPlugin,
|
|
4
|
+
ref,
|
|
5
|
+
useSiteConfig
|
|
6
|
+
} from "#imports";
|
|
7
|
+
export default defineNuxtPlugin({
|
|
8
|
+
name: "nuxt-seo:defaults",
|
|
9
|
+
order: 999,
|
|
10
|
+
env: {
|
|
11
|
+
islands: false
|
|
12
|
+
},
|
|
13
|
+
setup() {
|
|
14
|
+
const siteConfig = useSiteConfig();
|
|
15
|
+
const locale = ref(siteConfig.currentLocale || siteConfig.defaultLocale);
|
|
16
|
+
applyDefaults({
|
|
17
|
+
locale
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { applyDefaults } from "../logic/applyDefaults.js";
|
|
2
|
+
import { defineNuxtPlugin, ref, useSiteConfig } from "#imports";
|
|
3
|
+
export default defineNuxtPlugin({
|
|
4
|
+
name: "nuxt-seo:defaults",
|
|
5
|
+
env: {
|
|
6
|
+
islands: false
|
|
7
|
+
},
|
|
8
|
+
// we need to wait for the i18n plugin to run first
|
|
9
|
+
// @ts-expect-error dynamic
|
|
10
|
+
dependsOn: import.meta.server ? [
|
|
11
|
+
"nuxt-site-config:i18n"
|
|
12
|
+
] : [
|
|
13
|
+
"i18n:plugin"
|
|
14
|
+
],
|
|
15
|
+
setup(nuxtApp) {
|
|
16
|
+
const siteConfig = useSiteConfig();
|
|
17
|
+
const locale = ref(nuxtApp.$i18n?.locale?.value || siteConfig.currentLocale || siteConfig.defaultLocale);
|
|
18
|
+
nuxtApp.hook("i18n:localeSwitched", ({ newLocale }) => {
|
|
19
|
+
locale.value = newLocale;
|
|
20
|
+
});
|
|
21
|
+
applyDefaults({ locale });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
|
|
2
2
|
export default _default;
|
package/dist/types.d.mts
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { ModuleOptions } from './module.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
declare module '@nuxt/schema' {
|
|
6
|
-
interface NuxtConfig { ['seo']?: Partial<ModuleOptions> }
|
|
7
|
-
interface NuxtOptions { ['seo']?: ModuleOptions }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
declare module 'nuxt/schema' {
|
|
11
|
-
interface NuxtConfig { ['seo']?: Partial<ModuleOptions> }
|
|
12
|
-
interface NuxtOptions { ['seo']?: ModuleOptions }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export type { ModuleOptions, default } from './module.js'
|
|
1
|
+
export { type ModuleOptions, default } from './module.js'
|
package/dist/types.d.ts
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { ModuleOptions } from './module'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
declare module '@nuxt/schema' {
|
|
6
|
-
interface NuxtConfig { ['seo']?: Partial<ModuleOptions> }
|
|
7
|
-
interface NuxtOptions { ['seo']?: ModuleOptions }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
declare module 'nuxt/schema' {
|
|
11
|
-
interface NuxtConfig { ['seo']?: Partial<ModuleOptions> }
|
|
12
|
-
interface NuxtOptions { ['seo']?: ModuleOptions }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export type { ModuleOptions, default } from './module'
|
|
1
|
+
export { type ModuleOptions, default } from './module'
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtjs/seo",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.0-rc.
|
|
5
|
-
"packageManager": "pnpm@8.14.0",
|
|
4
|
+
"version": "2.0.0-rc.20",
|
|
6
5
|
"description": "The all-in-one SEO layer for Nuxt 3.",
|
|
7
6
|
"author": {
|
|
8
7
|
"name": "Harlan Wilton",
|
|
@@ -32,40 +31,54 @@
|
|
|
32
31
|
"dist"
|
|
33
32
|
],
|
|
34
33
|
"dependencies": {
|
|
35
|
-
"@nuxt/kit": "^3.
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"nuxt-
|
|
40
|
-
"nuxt-
|
|
41
|
-
"nuxt-
|
|
42
|
-
"nuxt-
|
|
43
|
-
"nuxt-
|
|
44
|
-
"nuxt-site-config": "^2.2.
|
|
45
|
-
"
|
|
46
|
-
"ufo": "^1.
|
|
34
|
+
"@nuxt/kit": "^3.13.0",
|
|
35
|
+
"@nuxtjs/robots": "^4.1.6",
|
|
36
|
+
"@nuxtjs/sitemap": "6.0.0",
|
|
37
|
+
"defu": "^6.1.4",
|
|
38
|
+
"nuxt-link-checker": "^3.1.1",
|
|
39
|
+
"nuxt-og-image": "3.0.0-rc.66",
|
|
40
|
+
"nuxt-schema-org": "^3.3.9",
|
|
41
|
+
"nuxt-seo-experiments": "^4.0.1",
|
|
42
|
+
"nuxt-site-config": "^2.2.16",
|
|
43
|
+
"nuxt-site-config-kit": "^2.2.16",
|
|
44
|
+
"pkg-types": "^1.2.0",
|
|
45
|
+
"ufo": "^1.5.4"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"@antfu/eslint-config": "^
|
|
50
|
-
"@nuxt/module-builder": "^0.
|
|
51
|
-
"@nuxt/schema": "
|
|
52
|
-
"@nuxt/test-utils": "3.
|
|
53
|
-
"@nuxt/ui": "^2.
|
|
54
|
-
"@nuxtjs/i18n": "8.
|
|
55
|
-
"bumpp": "^9.2
|
|
56
|
-
"eslint": "^
|
|
57
|
-
"execa": "^
|
|
58
|
-
"nitropack": "^2.
|
|
59
|
-
"nuxt": "
|
|
60
|
-
"typescript": "
|
|
61
|
-
"vitest": "^
|
|
48
|
+
"@antfu/eslint-config": "^3.0.0",
|
|
49
|
+
"@nuxt/module-builder": "^0.8.3",
|
|
50
|
+
"@nuxt/schema": "3.13.0",
|
|
51
|
+
"@nuxt/test-utils": "^3.14.1",
|
|
52
|
+
"@nuxt/ui": "^2.18.4",
|
|
53
|
+
"@nuxtjs/i18n": "^8.5.1",
|
|
54
|
+
"bumpp": "^9.5.2",
|
|
55
|
+
"eslint": "^9.9.1",
|
|
56
|
+
"execa": "^9.3.1",
|
|
57
|
+
"nitropack": "^2.9.7",
|
|
58
|
+
"nuxt": "3.13.0",
|
|
59
|
+
"typescript": "5.5.4",
|
|
60
|
+
"vitest": "^2.0.5"
|
|
61
|
+
},
|
|
62
|
+
"build": {
|
|
63
|
+
"externals": [
|
|
64
|
+
"ofetch",
|
|
65
|
+
"consola/utils"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"resolutions": {
|
|
69
|
+
"@nuxt/schema": "3.13.0",
|
|
70
|
+
"nuxt": "3.13.0",
|
|
71
|
+
"shiki": "1.10.1",
|
|
72
|
+
"typescript": "5.4.5"
|
|
62
73
|
},
|
|
63
74
|
"scripts": {
|
|
64
75
|
"build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build",
|
|
65
76
|
"dev": "nuxi dev .playground",
|
|
77
|
+
"dev:docs": "nuxi dev docs",
|
|
78
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare .playground",
|
|
66
79
|
"lint": "eslint . --fix",
|
|
67
|
-
"release": "bumpp && pnpm -r publish --access public",
|
|
68
|
-
"test": "nuxi prepare .playground &&
|
|
69
|
-
"typecheck": "tsc --noEmit --strict"
|
|
80
|
+
"release": "pnpm build && bumpp && pnpm -r publish --access public",
|
|
81
|
+
"test": "nuxi prepare .playground && vitest",
|
|
82
|
+
"typecheck": "npx vue-tsc --noEmit --strict"
|
|
70
83
|
}
|
|
71
84
|
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { defineEventHandler, sendRedirect } from "h3";
|
|
2
|
-
import { joinURL } from "ufo";
|
|
3
|
-
import { useNitroOrigin, useSiteConfig } from "#imports";
|
|
4
|
-
export default defineEventHandler((e) => {
|
|
5
|
-
const siteConfig = useSiteConfig(e);
|
|
6
|
-
if (siteConfig.site) {
|
|
7
|
-
const origin = useNitroOrigin(e);
|
|
8
|
-
if (!siteConfig.site.startsWith(origin)) {
|
|
9
|
-
const url = new URL(e.path, origin);
|
|
10
|
-
url.hostname = siteConfig.site;
|
|
11
|
-
return sendRedirect(e, joinURL(siteConfig.site, url.pathname), 301);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
computed,
|
|
3
|
-
createSitePathResolver,
|
|
4
|
-
defineNuxtPlugin,
|
|
5
|
-
useHead,
|
|
6
|
-
useRoute,
|
|
7
|
-
useSeoMeta,
|
|
8
|
-
useServerHead,
|
|
9
|
-
useSiteConfig
|
|
10
|
-
} from "#imports";
|
|
11
|
-
export default defineNuxtPlugin({
|
|
12
|
-
name: "nuxt-seo:defaults",
|
|
13
|
-
setup() {
|
|
14
|
-
const siteConfig = useSiteConfig() || {};
|
|
15
|
-
const route = useRoute();
|
|
16
|
-
const resolveUrl = createSitePathResolver({ withBase: true, absolute: true });
|
|
17
|
-
const canonicalUrl = computed(() => resolveUrl(route.path || "/").value || route.path);
|
|
18
|
-
const minimalPriority = {
|
|
19
|
-
// give nuxt.config values higher priority
|
|
20
|
-
tagPriority: 101
|
|
21
|
-
};
|
|
22
|
-
useHead({
|
|
23
|
-
link: [{ rel: "canonical", href: () => canonicalUrl.value }]
|
|
24
|
-
});
|
|
25
|
-
const locale = siteConfig.currentLocale || siteConfig.defaultLocale;
|
|
26
|
-
if (locale) {
|
|
27
|
-
useServerHead({
|
|
28
|
-
htmlAttrs: { lang: locale }
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
useHead({
|
|
32
|
-
templateParams: { site: siteConfig, siteName: siteConfig.name || "" },
|
|
33
|
-
titleTemplate: "%s %separator %siteName"
|
|
34
|
-
}, minimalPriority);
|
|
35
|
-
const seoMeta = {
|
|
36
|
-
ogUrl: () => canonicalUrl.value,
|
|
37
|
-
ogLocale: locale,
|
|
38
|
-
ogSiteName: siteConfig.name
|
|
39
|
-
};
|
|
40
|
-
if (siteConfig.description)
|
|
41
|
-
seoMeta.description = siteConfig.description;
|
|
42
|
-
if (siteConfig.twitter) {
|
|
43
|
-
const id = siteConfig.twitter.startsWith("@") ? siteConfig.twitter : `@${siteConfig.twitter}`;
|
|
44
|
-
seoMeta.twitterCreator = id;
|
|
45
|
-
seoMeta.twitterSite = id;
|
|
46
|
-
}
|
|
47
|
-
useSeoMeta(seoMeta, minimalPriority);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
File without changes
|
|
File without changes
|