@nuxtjs/seo 2.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +46 -0
- package/dist/module.d.ts +46 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +102 -0
- package/dist/runtime/nitro/middleware/redirect.d.ts +2 -0
- package/dist/runtime/nitro/middleware/redirect.mjs +14 -0
- package/dist/runtime/nitro/tsconfig.json +3 -0
- package/dist/runtime/nuxt/composables/polyfills.d.ts +10 -0
- package/dist/runtime/nuxt/composables/polyfills.mjs +18 -0
- package/dist/runtime/nuxt/composables/useBreadcrumbItems.d.ts +48 -0
- package/dist/runtime/nuxt/composables/useBreadcrumbItems.mjs +78 -0
- package/dist/runtime/nuxt/plugin/defaults.d.ts +2 -0
- package/dist/runtime/nuxt/plugin/defaults.mjs +49 -0
- package/dist/runtime/nuxt/plugin/titles.d.ts +2 -0
- package/dist/runtime/nuxt/plugin/titles.mjs +28 -0
- package/dist/runtime/pure/breadcrumbs.d.ts +1 -0
- package/dist/runtime/pure/breadcrumbs.mjs +18 -0
- package/dist/types.d.mts +16 -0
- package/dist/types.d.ts +16 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<h1 align='center'>Nuxt SEO</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href='https://github.com/harlan-zw/nuxt-seo/actions/workflows/test.yml'>
|
|
5
|
+
</a>
|
|
6
|
+
<a href="https://www.npmjs.com/package/@nuxtjs/seo" target="__blank"><img src="https://img.shields.io/npm/v/@nuxtjs/seo?style=flat&colorA=002438&colorB=28CF8D" alt="NPM version"></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@nuxtjs/seo" target="__blank"><img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@nuxtjs/seo?flat&colorA=002438&colorB=28CF8D"></a>
|
|
8
|
+
<a href="https://github.com/harlan-zw/nuxt-seo" target="__blank"><img alt="GitHub stars" src="https://img.shields.io/github/stars/harlan-zw/nuxt-seo?flat&colorA=002438&colorB=28CF8D"></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
The complete SEO solution for Nuxt.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<table>
|
|
17
|
+
<tbody>
|
|
18
|
+
<td align="center">
|
|
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 Released</a></b> <br>
|
|
21
|
+
<sup> Please report any issues 🐛</sup><br>
|
|
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
|
+
<img width="800" height="0" />
|
|
24
|
+
</td>
|
|
25
|
+
</tbody>
|
|
26
|
+
</table>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
## Background
|
|
30
|
+
|
|
31
|
+
Technical SEO is hard. It requires many moving parts that need to work well together. Configuring all of these parts
|
|
32
|
+
correctly is a challenge.
|
|
33
|
+
|
|
34
|
+
Nuxt SEO is the total SEO solution for Nuxt. It combines 7 SEO modules and best practices into one module that requires
|
|
35
|
+
minimal effort to configure.
|
|
36
|
+
|
|
37
|
+
With powerful APIs built for fully dynamic sites and zero-config defaults for static sites.
|
|
38
|
+
|
|
39
|
+
## Modules
|
|
40
|
+
|
|
41
|
+
- 📖 [nuxt-simple-sitemap](https://github.com/nuxt-modules/sitemap) - Sitemap.xml Support
|
|
42
|
+
- 🤖 [nuxt-simple-robots](https://github.com/harlan-zw/nuxt-simple-robots) - Manage site crawling
|
|
43
|
+
- 🔎 [nuxt-schema-org](https://unhead-schema-org.harlanzw.com/) - Generate Schema.org JSON-LD for SEO
|
|
44
|
+
- △ [nuxt-seo-experiments](https://github.com/harlan-zw/nuxt-seo-experiments) - Experimental SEO meta features
|
|
45
|
+
- 🖼️ [nuxt-og-image](https://github.com/nuxt-modules/og-image) - Generate dynamic social share images
|
|
46
|
+
- ✅ [nuxt-link-checker](https://github.com/harlan-zw/nuxt-link-checker) - Check for broken links
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
1. Install `@nuxtjs/seo` dependency to your project:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
pnpm i -D @nuxtjs/seo
|
|
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
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
That's it!
|
|
67
|
+
|
|
68
|
+
All features are enabled by default. Learn more by exploring the [documentation](https://nuxtseo.com)
|
|
69
|
+
|
|
70
|
+
## Sponsors
|
|
71
|
+
|
|
72
|
+
<p align="center">
|
|
73
|
+
<a href="https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg">
|
|
74
|
+
<img src='https://raw.githubusercontent.com/harlan-zw/static/main/sponsors.svg'/>
|
|
75
|
+
</a>
|
|
76
|
+
</p>
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT License © 2022-PRESENT [Harlan Wilton](https://github.com/harlan-zw)
|
package/dist/module.cjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Will ensure a title is always set by providing a fallback title based on the casing the last slug segment.
|
|
6
|
+
*
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
fallbackTitle?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Will set up a number of defaults for meta tags and Schema.org, if the modules and config are available.
|
|
12
|
+
*
|
|
13
|
+
* @default true
|
|
14
|
+
*/
|
|
15
|
+
automaticDefaults?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* When enabled, it will redirect any request to the canonical domain (site url) using a 301 redirect on non-dev environments.
|
|
18
|
+
*
|
|
19
|
+
* E.g if the site url is 'www.example.com' and the user visits 'example.com',
|
|
20
|
+
* they will be redirected to 'www.example.com'.
|
|
21
|
+
*
|
|
22
|
+
* This is useful for SEO as it prevents duplicate content and consolidates page rank.
|
|
23
|
+
*
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
redirectToCanonicalSiteUrl?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the module should be loaded.
|
|
29
|
+
*/
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the debugging mode should be enabled.
|
|
33
|
+
*
|
|
34
|
+
* @default `nuxt.options.debug`
|
|
35
|
+
*/
|
|
36
|
+
debug: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the Nuxt SEO splash should be shown when Nuxt is started.
|
|
39
|
+
*
|
|
40
|
+
* @default `nuxt.options.dev`
|
|
41
|
+
*/
|
|
42
|
+
splash: boolean;
|
|
43
|
+
}
|
|
44
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
|
|
45
|
+
|
|
46
|
+
export { type ModuleOptions, _default as default };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Will ensure a title is always set by providing a fallback title based on the casing the last slug segment.
|
|
6
|
+
*
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
fallbackTitle?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Will set up a number of defaults for meta tags and Schema.org, if the modules and config are available.
|
|
12
|
+
*
|
|
13
|
+
* @default true
|
|
14
|
+
*/
|
|
15
|
+
automaticDefaults?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* When enabled, it will redirect any request to the canonical domain (site url) using a 301 redirect on non-dev environments.
|
|
18
|
+
*
|
|
19
|
+
* E.g if the site url is 'www.example.com' and the user visits 'example.com',
|
|
20
|
+
* they will be redirected to 'www.example.com'.
|
|
21
|
+
*
|
|
22
|
+
* This is useful for SEO as it prevents duplicate content and consolidates page rank.
|
|
23
|
+
*
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
redirectToCanonicalSiteUrl?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the module should be loaded.
|
|
29
|
+
*/
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the debugging mode should be enabled.
|
|
33
|
+
*
|
|
34
|
+
* @default `nuxt.options.debug`
|
|
35
|
+
*/
|
|
36
|
+
debug: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the Nuxt SEO splash should be shown when Nuxt is started.
|
|
39
|
+
*
|
|
40
|
+
* @default `nuxt.options.dev`
|
|
41
|
+
*/
|
|
42
|
+
splash: boolean;
|
|
43
|
+
}
|
|
44
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions>;
|
|
45
|
+
|
|
46
|
+
export { type ModuleOptions, _default as default };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { defineNuxtModule, useLogger, createResolver, installModule, addPlugin, hasNuxtModule, addImports, addServerHandler } from '@nuxt/kit';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { installNuxtSiteConfig } from 'nuxt-site-config-kit';
|
|
4
|
+
|
|
5
|
+
const version = "2.0.0-rc.1";
|
|
6
|
+
|
|
7
|
+
const Modules = [
|
|
8
|
+
"nuxt-simple-robots",
|
|
9
|
+
"nuxt-simple-sitemap",
|
|
10
|
+
"nuxt-og-image",
|
|
11
|
+
"nuxt-schema-org",
|
|
12
|
+
"nuxt-seo-experiments",
|
|
13
|
+
"nuxt-link-checker"
|
|
14
|
+
];
|
|
15
|
+
const module = defineNuxtModule({
|
|
16
|
+
meta: {
|
|
17
|
+
name: "nuxtseo",
|
|
18
|
+
compatibility: {
|
|
19
|
+
nuxt: "^3.7.0",
|
|
20
|
+
bridge: false
|
|
21
|
+
},
|
|
22
|
+
configKey: "seo"
|
|
23
|
+
},
|
|
24
|
+
defaults(nuxt) {
|
|
25
|
+
return {
|
|
26
|
+
enabled: true,
|
|
27
|
+
debug: nuxt.options.debug,
|
|
28
|
+
redirectToCanonicalSiteUrl: false,
|
|
29
|
+
splash: nuxt.options.dev,
|
|
30
|
+
automaticDefaults: true,
|
|
31
|
+
fallbackTitle: true
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async setup(config, nuxt) {
|
|
35
|
+
const logger = useLogger("@nuxtjs/seo");
|
|
36
|
+
logger.level = config.debug ? 4 : 3;
|
|
37
|
+
if (config.enabled === false) {
|
|
38
|
+
logger.debug("The module is disabled, skipping setup.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const { resolve, resolvePath } = createResolver(import.meta.url);
|
|
42
|
+
await installNuxtSiteConfig();
|
|
43
|
+
for (const module of Modules)
|
|
44
|
+
await installModule(await resolvePath(module));
|
|
45
|
+
if (config.automaticDefaults) {
|
|
46
|
+
addPlugin({
|
|
47
|
+
src: resolve("./runtime/nuxt/plugin/defaults")
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (config.fallbackTitle) {
|
|
51
|
+
addPlugin({
|
|
52
|
+
src: resolve("./runtime/nuxt/plugin/titles")
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!hasNuxtModule("@nuxtjs/i18n")) {
|
|
56
|
+
addImports({
|
|
57
|
+
from: resolve(`./runtime/nuxt/composables/polyfills`),
|
|
58
|
+
name: "useI18n"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
addImports({
|
|
62
|
+
from: resolve(`./runtime/nuxt/composables/useBreadcrumbItems`),
|
|
63
|
+
name: "useBreadcrumbItems"
|
|
64
|
+
});
|
|
65
|
+
const polyfills = {
|
|
66
|
+
schemaOrg: ["useSchemaOrg", "defineWebSite", "defineWebPage"]
|
|
67
|
+
};
|
|
68
|
+
for (const [module, composables] of Object.entries(polyfills)) {
|
|
69
|
+
if (nuxt.options[module]?.enable === false) {
|
|
70
|
+
composables.forEach((name) => {
|
|
71
|
+
addImports({
|
|
72
|
+
from: resolve("./runtime/nuxt/composables/polyfills"),
|
|
73
|
+
name
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
nuxt.options.experimental.headNext = true;
|
|
79
|
+
if (config.redirectToCanonicalSiteUrl) {
|
|
80
|
+
addServerHandler({
|
|
81
|
+
handler: resolve("./runtime/nitro/middleware/redirect"),
|
|
82
|
+
middleware: true
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (config.splash) {
|
|
86
|
+
logger.log("");
|
|
87
|
+
let latestTag = `v${version}`;
|
|
88
|
+
try {
|
|
89
|
+
latestTag = (await $fetch("https://ungh.unjs.io/repos/harlan-zw/nuxt-seo/releases/latest")).release.tag;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
}
|
|
92
|
+
const upToDate = latestTag === `v${version}`;
|
|
93
|
+
logger.log(`${chalk.green("Nuxt SEO")} ${chalk.yellow(`v${version}`)} ${chalk.gray(`by ${chalk.underline("@harlan_zw")}`)}`);
|
|
94
|
+
if (!upToDate)
|
|
95
|
+
logger.log(`${chalk.gray(" \u251C\u2500 ")}\u{1F389} New version available!${chalk.gray(` Run ${chalk.underline(`npm i @nuxtjs/seo@${latestTag}`)} to update.`)}`);
|
|
96
|
+
logger.log(chalk.dim(" \u2514\u2500 \u{1F9EA} Help get Nuxt SEO stable by providing feedback https://github.com/harlan-zw/nuxt-seo/discussions/108"));
|
|
97
|
+
logger.log("");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export { module as default };
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function useSchemaOrg(): void;
|
|
2
|
+
export declare function defineWebSite(): void;
|
|
3
|
+
export declare function defineWebPage(): void;
|
|
4
|
+
export declare function useI18n(): {
|
|
5
|
+
t: (_: string, fallback: string) => string;
|
|
6
|
+
te: (_: string) => boolean;
|
|
7
|
+
strategy: string;
|
|
8
|
+
defaultLocale: import("vue").Ref<any>;
|
|
9
|
+
locale: import("vue").Ref<any>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import { useSiteConfig } from "#imports";
|
|
3
|
+
export function useSchemaOrg() {
|
|
4
|
+
}
|
|
5
|
+
export function defineWebSite() {
|
|
6
|
+
}
|
|
7
|
+
export function defineWebPage() {
|
|
8
|
+
}
|
|
9
|
+
export function useI18n() {
|
|
10
|
+
const siteConfig = useSiteConfig();
|
|
11
|
+
return {
|
|
12
|
+
t: (_, fallback) => fallback,
|
|
13
|
+
te: (_) => false,
|
|
14
|
+
strategy: "no_prefix",
|
|
15
|
+
defaultLocale: ref(siteConfig.defaultLocale || "en"),
|
|
16
|
+
locale: ref(siteConfig.currentLocale || siteConfig.defaultLocale || "en")
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
2
|
+
import type { BreadcrumbLink } from '@nuxt/ui/dist/runtime/types';
|
|
3
|
+
export interface BreadcrumbProps {
|
|
4
|
+
path?: MaybeRefOrGetter<string>;
|
|
5
|
+
id?: string;
|
|
6
|
+
schemaOrg?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The Aria Label for the breadcrumbs.
|
|
9
|
+
* You shouldn't need to change this.
|
|
10
|
+
*
|
|
11
|
+
* @default 'Breadcrumbs'
|
|
12
|
+
*/
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Should the current breadcrumb item be shown.
|
|
16
|
+
*
|
|
17
|
+
* @default false
|
|
18
|
+
*/
|
|
19
|
+
hideCurrent?: MaybeRefOrGetter<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Should the root breadcrumb be shown.
|
|
22
|
+
*/
|
|
23
|
+
hideRoot?: MaybeRefOrGetter<boolean>;
|
|
24
|
+
}
|
|
25
|
+
export interface BreadcrumbItemProps extends BreadcrumbLink {
|
|
26
|
+
/** Whether the breadcrumb item represents the aria-current. */
|
|
27
|
+
current?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The type of current location the breadcrumb item represents, if `isCurrent` is true.
|
|
30
|
+
* @default 'page'
|
|
31
|
+
*/
|
|
32
|
+
ariaCurrent?: 'page' | 'step' | 'location' | 'date' | 'time' | boolean | 'true' | 'false';
|
|
33
|
+
/** Whether the breadcrumb item is disabled. */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
to: string;
|
|
36
|
+
ariaLabel?: string;
|
|
37
|
+
separator?: boolean | string;
|
|
38
|
+
icon?: string;
|
|
39
|
+
class?: (string | string[] | undefined)[] | string;
|
|
40
|
+
/**
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
_props?: {
|
|
44
|
+
first: boolean;
|
|
45
|
+
last: boolean;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export declare function useBreadcrumbItems(options?: BreadcrumbProps): any;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { withoutTrailingSlash } from "ufo";
|
|
2
|
+
import { pathBreadcrumbSegments } from "../../pure/breadcrumbs.mjs";
|
|
3
|
+
import {
|
|
4
|
+
computed,
|
|
5
|
+
defineBreadcrumb,
|
|
6
|
+
toValue,
|
|
7
|
+
useI18n,
|
|
8
|
+
useRoute,
|
|
9
|
+
useRouter,
|
|
10
|
+
useSchemaOrg,
|
|
11
|
+
withSiteTrailingSlash
|
|
12
|
+
} from "#imports";
|
|
13
|
+
function withoutQuery(path) {
|
|
14
|
+
return path.split("?")[0];
|
|
15
|
+
}
|
|
16
|
+
function titleCase(s) {
|
|
17
|
+
return s.replaceAll("-", " ").replace(/\w\S*/g, (w) => w.charAt(0).toUpperCase() + w.substr(1).toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
export function useBreadcrumbItems(options = {}) {
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const routes = router.getRoutes();
|
|
22
|
+
const i18n = useI18n();
|
|
23
|
+
const items = computed(() => {
|
|
24
|
+
let rootNode = "/";
|
|
25
|
+
if (i18n) {
|
|
26
|
+
if (i18n.strategy === "prefix" || i18n.strategy !== "no_prefix" && i18n.defaultLocale.value !== i18n.locale.value)
|
|
27
|
+
rootNode = `/${i18n.defaultLocale.value}`;
|
|
28
|
+
}
|
|
29
|
+
const current = withoutQuery(withoutTrailingSlash(toValue(options.path || useRoute().path) || rootNode));
|
|
30
|
+
return pathBreadcrumbSegments(current, rootNode).map((path) => ({
|
|
31
|
+
to: path
|
|
32
|
+
})).map((item) => {
|
|
33
|
+
const route = routes.find((r) => withoutTrailingSlash(r.path) === withoutTrailingSlash(item.to));
|
|
34
|
+
const routeMeta = route?.meta || {};
|
|
35
|
+
const routeName = route ? String(route.name || route.path) : item.to === "/" ? "index" : "unknown";
|
|
36
|
+
let [name] = routeName.split("___");
|
|
37
|
+
if (name === "unknown")
|
|
38
|
+
name = item.to.split("/").pop() || "";
|
|
39
|
+
if (routeMeta.breadcrumb) {
|
|
40
|
+
item = {
|
|
41
|
+
...item,
|
|
42
|
+
...routeMeta.breadcrumb
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
item.label = item.label || routeMeta.breadcrumbTitle || routeMeta.title;
|
|
46
|
+
if (typeof item.label === "undefined") {
|
|
47
|
+
item.label = item.label || i18n.t(`breadcrumb.items.${name}.label`, name === "index" ? "Home" : titleCase(name), { missingWarn: false });
|
|
48
|
+
item.ariaLabel = item.ariaLabel || i18n.t(`breadcrumb.items.${name}.ariaLabel`, item.label, { missingWarn: false });
|
|
49
|
+
}
|
|
50
|
+
item.ariaLabel = item.ariaLabel || item.label;
|
|
51
|
+
item.current = item.current || item.to === current;
|
|
52
|
+
if (toValue(options.hideCurrent) && item.current)
|
|
53
|
+
return false;
|
|
54
|
+
return item;
|
|
55
|
+
}).map((m) => {
|
|
56
|
+
if (m && m.to) {
|
|
57
|
+
m.to = withSiteTrailingSlash(m.to).value;
|
|
58
|
+
if (m.to === rootNode && toValue(options.hideRoot))
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return m;
|
|
62
|
+
}).filter(Boolean);
|
|
63
|
+
});
|
|
64
|
+
if (process.server && options.schemaOrg) {
|
|
65
|
+
useSchemaOrg([
|
|
66
|
+
defineBreadcrumb(computed(() => {
|
|
67
|
+
return {
|
|
68
|
+
id: `#${options.id || "breadcrumb"}`,
|
|
69
|
+
itemListElement: items.value.map((item) => ({
|
|
70
|
+
name: item.label || item.ariaLabel,
|
|
71
|
+
item: item.to
|
|
72
|
+
}))
|
|
73
|
+
};
|
|
74
|
+
}))
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
return items;
|
|
78
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { withoutTrailingSlash } from "ufo";
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
defineNuxtPlugin,
|
|
5
|
+
useHead,
|
|
6
|
+
useRoute
|
|
7
|
+
} from "#imports";
|
|
8
|
+
function titleCase(s) {
|
|
9
|
+
return s.replaceAll("-", " ").replace(/\w\S*/g, (w) => w.charAt(0).toUpperCase() + w.substr(1).toLowerCase());
|
|
10
|
+
}
|
|
11
|
+
export default defineNuxtPlugin({
|
|
12
|
+
name: "nuxt-seo:fallback-titles",
|
|
13
|
+
setup() {
|
|
14
|
+
const route = useRoute();
|
|
15
|
+
const title = computed(() => {
|
|
16
|
+
if (typeof route.meta?.title === "string")
|
|
17
|
+
return route.meta?.title;
|
|
18
|
+
const path = withoutTrailingSlash(route.path || "/");
|
|
19
|
+
const lastSegment = path.split("/").pop();
|
|
20
|
+
return lastSegment ? titleCase(lastSegment) : null;
|
|
21
|
+
});
|
|
22
|
+
const minimalPriority = {
|
|
23
|
+
// give nuxt.config values higher priority
|
|
24
|
+
tagPriority: 101
|
|
25
|
+
};
|
|
26
|
+
useHead({ title: () => title.value }, minimalPriority);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function pathBreadcrumbSegments(path: string, rootNode?: string): string[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { hasTrailingSlash, parseURL, stringifyParsedURL, withTrailingSlash } from "ufo";
|
|
2
|
+
export function pathBreadcrumbSegments(path, rootNode = "/") {
|
|
3
|
+
const startNode = parseURL(path);
|
|
4
|
+
const appendsTrailingSlash = hasTrailingSlash(startNode.pathname);
|
|
5
|
+
const stepNode = (node, nodes = []) => {
|
|
6
|
+
const fullPath = stringifyParsedURL(node);
|
|
7
|
+
const currentPathName = node.pathname || "/";
|
|
8
|
+
nodes.push(fullPath || "/");
|
|
9
|
+
if (currentPathName !== rootNode && currentPathName !== "/") {
|
|
10
|
+
node.pathname = currentPathName.substring(0, currentPathName.lastIndexOf("/"));
|
|
11
|
+
if (appendsTrailingSlash)
|
|
12
|
+
node.pathname = withTrailingSlash(node.pathname.substring(0, node.pathname.lastIndexOf("/")));
|
|
13
|
+
stepNode(node, nodes);
|
|
14
|
+
}
|
|
15
|
+
return nodes;
|
|
16
|
+
};
|
|
17
|
+
return stepNode(startNode).reverse();
|
|
18
|
+
}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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'
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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'
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nuxtjs/seo",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "2.0.0-rc.1",
|
|
5
|
+
"packageManager": "pnpm@8.14.0",
|
|
6
|
+
"description": "The all-in-one SEO layer for Nuxt 3.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Harlan Wilton",
|
|
9
|
+
"email": "harlan@harlanzw.com",
|
|
10
|
+
"url": "https://harlanzw.com/"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"funding": "https://github.com/sponsors/harlan-zw",
|
|
14
|
+
"homepage": "https://nuxtseo.com/",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/harlan-zw/nuxt-seo.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/harlan-zw/nuxt-seo/issues"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@nuxt/kit": "^3.9.0",
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"defu": "^6.1.3",
|
|
29
|
+
"nuxt-link-checker": "^3.0.0-rc.3",
|
|
30
|
+
"nuxt-og-image": "^3.0.0-rc.21",
|
|
31
|
+
"nuxt-schema-org": "^3.3.0",
|
|
32
|
+
"nuxt-seo-experiments": "^4.0.0-rc.0",
|
|
33
|
+
"nuxt-simple-robots": "^4.0.0-rc.9",
|
|
34
|
+
"nuxt-simple-sitemap": "^4.4.1",
|
|
35
|
+
"nuxt-site-config": "^2.2.0",
|
|
36
|
+
"nuxt-site-config-kit": "^2.2.0",
|
|
37
|
+
"ufo": "^1.3.2"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@antfu/eslint-config": "^2.6.1",
|
|
41
|
+
"@nuxt/module-builder": "^0.5.5",
|
|
42
|
+
"@nuxt/schema": "^3.9.0",
|
|
43
|
+
"@nuxt/test-utils": "3.9.0",
|
|
44
|
+
"@nuxt/ui": "^2.11.1",
|
|
45
|
+
"@nuxtjs/i18n": "8.0.0",
|
|
46
|
+
"bumpp": "^9.2.1",
|
|
47
|
+
"eslint": "^8.56.0",
|
|
48
|
+
"execa": "^8.0.1",
|
|
49
|
+
"nitropack": "^2.8.1",
|
|
50
|
+
"nuxt": "^3.9.0",
|
|
51
|
+
"typescript": "^5.3.3",
|
|
52
|
+
"vitest": "^1.1.1"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt-module-build build",
|
|
56
|
+
"dev": "nuxi dev .playground",
|
|
57
|
+
"lint": "eslint . --fix",
|
|
58
|
+
"release": "bumpp && pnpm -r publish --access public",
|
|
59
|
+
"test": "nuxi prepare .playground && true",
|
|
60
|
+
"typecheck": "tsc --noEmit --strict"
|
|
61
|
+
}
|
|
62
|
+
}
|