nuxt-link-checker 0.1.6 → 0.3.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 +45 -8
- package/dist/module.d.ts +5 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +51 -26
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
|
|
13
|
+
Identify and fix link issues for prerendered Nuxt 3 apps.
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
@@ -18,7 +18,7 @@ Improve your sites SEO by identifying and fixing link issues in your Nuxt v3 app
|
|
|
18
18
|
<tbody>
|
|
19
19
|
<td align="center">
|
|
20
20
|
<img width="800" height="0" /><br>
|
|
21
|
-
<i>Status:</i>
|
|
21
|
+
<i>Status:</i> Stable</b> <br>
|
|
22
22
|
<sup> Please report any issues 🐛</sup><br>
|
|
23
23
|
<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>
|
|
24
24
|
<img width="800" height="0" />
|
|
@@ -27,15 +27,16 @@ Improve your sites SEO by identifying and fixing link issues in your Nuxt v3 app
|
|
|
27
27
|
</table>
|
|
28
28
|
</p>
|
|
29
29
|
|
|
30
|
+
ℹ️ Looking for a complete SEO solution? Check out [Nuxt SEO Kit](https://github.com/harlan-zw/nuxt-seo-kit).
|
|
31
|
+
|
|
30
32
|
## Features
|
|
31
33
|
|
|
32
34
|
- ✅ Discover broken links - 404s and internal redirects
|
|
35
|
+
- 🚩 Warnings for bad practice links - absolute instead of relative and wrong trailing slash
|
|
33
36
|
- 🕵️ Fail on build if broken links are found (optional)
|
|
34
37
|
|
|
35
38
|
## Install
|
|
36
39
|
|
|
37
|
-
⚠️ The module is in early access and only works when pre-rendering.
|
|
38
|
-
|
|
39
40
|
```bash
|
|
40
41
|
npm install --save-dev nuxt-link-checker
|
|
41
42
|
|
|
@@ -72,7 +73,6 @@ export default defineNuxtConfig({
|
|
|
72
73
|
})
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
|
|
76
76
|
### Set host (optional)
|
|
77
77
|
|
|
78
78
|
You'll need to provide the host of your site so that the crawler can resolve absolute URLs that may be internal.
|
|
@@ -84,12 +84,40 @@ export default defineNuxtConfig({
|
|
|
84
84
|
siteUrl: 'https://example.com',
|
|
85
85
|
},
|
|
86
86
|
// OR
|
|
87
|
-
|
|
87
|
+
linkChecker: {
|
|
88
88
|
host: 'https://example.com',
|
|
89
89
|
},
|
|
90
90
|
})
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Exclude URLs from throwing errors
|
|
94
|
+
|
|
95
|
+
You can exclude URLs from throwing errors by adding them to the `exclude` array.
|
|
96
|
+
|
|
97
|
+
For example, if you have an `/admin` route that is a separate application, you can ignore all `/admin` links with:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
export default defineNuxtConfig({
|
|
101
|
+
linkChecker: {
|
|
102
|
+
exclude: [
|
|
103
|
+
'/admin/**'
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Disable errors on broken links
|
|
110
|
+
|
|
111
|
+
You can disable errors on broken links by setting `failOn404` to `false`.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
export default defineNuxtConfig({
|
|
115
|
+
linkChecker: {
|
|
116
|
+
failOn404: false,
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
93
121
|
## Module Config
|
|
94
122
|
|
|
95
123
|
### `failOn404`
|
|
@@ -99,10 +127,19 @@ export default defineNuxtConfig({
|
|
|
99
127
|
|
|
100
128
|
If set to `true`, the build will fail if any broken links are found.
|
|
101
129
|
|
|
130
|
+
### `exclude`
|
|
131
|
+
|
|
132
|
+
- Type: `string[]`
|
|
133
|
+
- Default: `[]`
|
|
134
|
+
|
|
135
|
+
An array of URLs to exclude from the check.
|
|
136
|
+
|
|
137
|
+
This can be useful if you have a route that is not pre-rendered, but you know it will be valid.
|
|
138
|
+
|
|
102
139
|
### `host`
|
|
103
140
|
|
|
104
141
|
- Type: `string`
|
|
105
|
-
- Default: `runtimeConfig.public.siteUrl`
|
|
142
|
+
- Default: `runtimeConfig.public.siteUrl || localhost`
|
|
106
143
|
- Required: `false`
|
|
107
144
|
|
|
108
145
|
The host of your site. This is required to validate absolute URLs which may be internal.
|
|
@@ -125,4 +162,4 @@ Whether internal links should have a trailing slash or not.
|
|
|
125
162
|
|
|
126
163
|
## License
|
|
127
164
|
|
|
128
|
-
MIT License ©
|
|
165
|
+
MIT License © 2023-PRESENT [Harlan Wilton](https://github.com/harlan-zw)
|
package/dist/module.d.ts
CHANGED
|
@@ -10,11 +10,15 @@ interface ModuleOptions {
|
|
|
10
10
|
/**
|
|
11
11
|
* Your site hostname. Used to determine if absolute links are internal.
|
|
12
12
|
*/
|
|
13
|
-
host
|
|
13
|
+
host: string;
|
|
14
14
|
/**
|
|
15
15
|
* Whether the build should fail when a 404 is encountered.
|
|
16
16
|
*/
|
|
17
17
|
failOn404: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Paths to ignore when checking links.
|
|
20
|
+
*/
|
|
21
|
+
exclude: string[];
|
|
18
22
|
}
|
|
19
23
|
interface ModuleHooks {
|
|
20
24
|
}
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { defineNuxtModule } from '@nuxt/kit';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
3
|
+
import { parseURL, hasProtocol } from 'ufo';
|
|
4
4
|
import { load } from 'cheerio';
|
|
5
|
+
import { toRouteMatcher, createRouter } from 'radix3';
|
|
5
6
|
|
|
6
7
|
const linkMap = {};
|
|
7
8
|
const EXT_REGEX = /\.[\da-z]+$/;
|
|
@@ -11,39 +12,55 @@ function getExtension(path) {
|
|
|
11
12
|
}
|
|
12
13
|
function extractLinks(html, from, { host, trailingSlash }) {
|
|
13
14
|
const links = [];
|
|
14
|
-
const
|
|
15
|
+
const hostname = parseURL(host).host;
|
|
15
16
|
const $ = load(html);
|
|
16
17
|
$("[href]").each((i, el) => {
|
|
17
18
|
const href = $(el).attr("href");
|
|
18
19
|
if (!href)
|
|
19
20
|
return;
|
|
20
|
-
|
|
21
|
+
const url = parseURL(href);
|
|
22
|
+
if (hasProtocol(href) && !href.startsWith("/") && url.host !== hostname)
|
|
21
23
|
return;
|
|
22
24
|
if (!allowedExtensions.has(getExtension(href)))
|
|
23
25
|
return;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
links.push({
|
|
27
|
+
pathname: url.pathname || "/",
|
|
28
|
+
url,
|
|
29
|
+
badAbsolute: Boolean(hostname) && hostname === url.host,
|
|
30
|
+
badTrailingSlash: url.pathname !== "/" && (trailingSlash && !url.pathname.endsWith("/") || !trailingSlash && url.pathname.endsWith("/")),
|
|
27
31
|
element: $.html(el) || ""
|
|
28
32
|
});
|
|
29
33
|
});
|
|
30
|
-
for (const link of _links.filter(Boolean)) {
|
|
31
|
-
const parsed = parseURL(link.href);
|
|
32
|
-
if (parsed.protocol)
|
|
33
|
-
continue;
|
|
34
|
-
let { pathname } = parsed;
|
|
35
|
-
if (!pathname.startsWith("/")) {
|
|
36
|
-
const fromURL = new URL(from, "http://localhost");
|
|
37
|
-
pathname = new URL(pathname, fromURL).pathname;
|
|
38
|
-
}
|
|
39
|
-
links.push({
|
|
40
|
-
...link,
|
|
41
|
-
href: pathname
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
34
|
return links;
|
|
45
35
|
}
|
|
46
36
|
|
|
37
|
+
function createFilter(options = {}) {
|
|
38
|
+
const include = options.include || [];
|
|
39
|
+
const exclude = options.exclude || [];
|
|
40
|
+
if (include.length === 0 && exclude.length === 0)
|
|
41
|
+
return () => true;
|
|
42
|
+
return function(path) {
|
|
43
|
+
for (const v of [{ rules: exclude, result: false }, { rules: include, result: true }]) {
|
|
44
|
+
const regexRules = v.rules.filter((r) => r instanceof RegExp);
|
|
45
|
+
if (regexRules.some((r) => r.test(path)))
|
|
46
|
+
return v.result;
|
|
47
|
+
const stringRules = v.rules.filter((r) => typeof r === "string");
|
|
48
|
+
if (stringRules.length > 0) {
|
|
49
|
+
const routes = {};
|
|
50
|
+
for (const r of stringRules) {
|
|
51
|
+
if (r === path)
|
|
52
|
+
return v.result;
|
|
53
|
+
routes[r] = true;
|
|
54
|
+
}
|
|
55
|
+
const routeRulesMatcher = toRouteMatcher(createRouter({ routes, ...options }));
|
|
56
|
+
if (routeRulesMatcher.matchAll(path).length > 0)
|
|
57
|
+
return Boolean(v.result);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return include.length === 0;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
const invalidStatusCodes = [404, 302, 301, 307, 303];
|
|
48
65
|
const module = defineNuxtModule({
|
|
49
66
|
meta: {
|
|
@@ -56,14 +73,18 @@ const module = defineNuxtModule({
|
|
|
56
73
|
},
|
|
57
74
|
defaults(nuxt) {
|
|
58
75
|
return {
|
|
59
|
-
host: nuxt.options.runtimeConfig.public?.siteUrl,
|
|
76
|
+
host: nuxt.options.runtimeConfig.public?.siteUrl || "localhost",
|
|
60
77
|
trailingSlash: nuxt.options.runtimeConfig.public?.trailingSlash || false,
|
|
61
|
-
failOn404: true
|
|
78
|
+
failOn404: true,
|
|
79
|
+
exclude: []
|
|
62
80
|
};
|
|
63
81
|
},
|
|
64
82
|
setup(config, nuxt) {
|
|
65
83
|
if (nuxt.options.dev)
|
|
66
84
|
return;
|
|
85
|
+
const urlFilter = createFilter({
|
|
86
|
+
exclude: config.exclude
|
|
87
|
+
});
|
|
67
88
|
nuxt.hooks.hook("nitro:init", async (nitro) => {
|
|
68
89
|
const invalidRoutes = {};
|
|
69
90
|
nitro.hooks.hook("prerender:generate", async (ctx) => {
|
|
@@ -76,16 +97,16 @@ const module = defineNuxtModule({
|
|
|
76
97
|
const links = Object.entries(linkMap);
|
|
77
98
|
if (!links.length)
|
|
78
99
|
return;
|
|
79
|
-
nitro.logger.info(
|
|
100
|
+
nitro.logger.info(`Scanning routes for broken links... ${chalk.gray(`trailingSlashes: ${config.trailingSlash ? "`true`" : "`false`"}`)}`);
|
|
80
101
|
let routeCount = 0;
|
|
81
102
|
let badLinkCount = 0;
|
|
82
103
|
links.forEach(([route, routes]) => {
|
|
83
104
|
const brokenLinks = routes.map((r) => {
|
|
84
105
|
return {
|
|
85
106
|
...r,
|
|
86
|
-
statusCode: invalidRoutes[r.
|
|
107
|
+
statusCode: invalidRoutes[r.pathname] || 200
|
|
87
108
|
};
|
|
88
|
-
}).filter((r) => r.statusCode !== 200 || r.badTrailingSlash);
|
|
109
|
+
}).filter((r) => r.statusCode !== 200 || r.badTrailingSlash || r.badAbsolute).filter((r) => urlFilter(r.pathname));
|
|
89
110
|
if (brokenLinks.length) {
|
|
90
111
|
nitro.logger.log(chalk.gray(
|
|
91
112
|
` ${Number(++routeCount) === links.length - 1 ? "\u2514\u2500" : "\u251C\u2500"} ${chalk.white(route)}`
|
|
@@ -97,9 +118,13 @@ const module = defineNuxtModule({
|
|
|
97
118
|
nitro.logger.log(chalk.red(
|
|
98
119
|
` ${link.statusCode} ${link.statusCode === 404 ? "Not Found" : "Redirect"}`
|
|
99
120
|
));
|
|
121
|
+
} else if (link.badAbsolute) {
|
|
122
|
+
nitro.logger.log(chalk.yellow(
|
|
123
|
+
" Absolute link, should be relative"
|
|
124
|
+
));
|
|
100
125
|
} else if (link.badTrailingSlash) {
|
|
101
126
|
nitro.logger.log(chalk.yellow(
|
|
102
|
-
|
|
127
|
+
" Incorrect trailing slash"
|
|
103
128
|
));
|
|
104
129
|
}
|
|
105
130
|
nitro.logger.log(` ${chalk.gray(link.element)}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-link-checker",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"packageManager": "pnpm@7.18.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": "https://github.com/sponsors/harlan-zw",
|
|
@@ -29,20 +29,21 @@
|
|
|
29
29
|
"@nuxt/kit": "3.0.0",
|
|
30
30
|
"chalk": "^5.2.0",
|
|
31
31
|
"cheerio": "1.0.0-rc.12",
|
|
32
|
+
"radix3": "^1.0.0",
|
|
32
33
|
"ufo": "^1.0.1"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"@antfu/eslint-config": "^0.34.
|
|
36
|
+
"@antfu/eslint-config": "^0.34.1",
|
|
36
37
|
"@nuxt/kit": "3.0.0",
|
|
37
38
|
"@nuxt/module-builder": "^0.2.1",
|
|
38
39
|
"@nuxt/test-utils": "3.0.0",
|
|
39
40
|
"@nuxtjs/eslint-config-typescript": "^12.0.0",
|
|
40
41
|
"bumpp": "^8.2.1",
|
|
41
|
-
"eslint": "8.
|
|
42
|
+
"eslint": "8.32.0",
|
|
42
43
|
"execa": "^6.1.0",
|
|
43
44
|
"nuxt": "^3.0.0",
|
|
44
|
-
"pathe": "^1.
|
|
45
|
-
"vitest": "^0.
|
|
45
|
+
"pathe": "^1.1.0",
|
|
46
|
+
"vitest": "^0.28.1"
|
|
46
47
|
},
|
|
47
48
|
"scripts": {
|
|
48
49
|
"lint": "eslint \"**/*.{ts,vue,json,yml}\" --fix",
|