nuxt-link-checker 2.0.0-beta.5 → 2.0.0-beta.7
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 +17 -107
- package/dist/module.d.ts +72 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +367 -89
- package/dist/runtime/inspect.d.ts +18 -1
- package/dist/runtime/inspect.mjs +13 -18
- package/dist/runtime/inspections/no-baseless.mjs +1 -3
- package/dist/runtime/inspections/no-error-response-status.mjs +1 -1
- package/dist/runtime/inspections/redirects.mjs +2 -2
- package/dist/runtime/plugin/search.nitro.mjs +1 -1
- package/dist/runtime/plugin/view/client.mjs +28 -15
- package/dist/runtime/server/api/inspect.mjs +13 -13
- package/dist/runtime/server/api/links.mjs +1 -1
- package/dist/runtime/sharedUtils.d.ts +8 -0
- package/dist/runtime/sharedUtils.mjs +13 -0
- package/dist/runtime/types.d.ts +2 -3
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -34,127 +34,38 @@ Find and magically fix links that may be negatively effecting your Nuxt sites SE
|
|
|
34
34
|
|
|
35
35
|
## Features
|
|
36
36
|
|
|
37
|
-
- ✅
|
|
38
|
-
-
|
|
39
|
-
-
|
|
37
|
+
- ✅ 7 SEO focused link inspections (more coming soon)
|
|
38
|
+
- ✨ See live inspections right in your Nuxt App
|
|
39
|
+
- 🧙 Magically fix them in Nuxt Dev Tools
|
|
40
|
+
- 🚩 Generate reports on build (html, markdown)
|
|
40
41
|
|
|
41
|
-
## Install
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
npm install --save-dev nuxt-link-checker
|
|
45
|
-
|
|
46
|
-
# Using yarn
|
|
47
|
-
yarn add --dev nuxt-link-checker
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Setup
|
|
51
|
-
|
|
52
|
-
_nuxt.config.ts_
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
export default defineNuxtConfig({
|
|
56
|
-
modules: [
|
|
57
|
-
'nuxt-link-checker',
|
|
58
|
-
],
|
|
59
|
-
})
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
To have routes scanned for broken links automatically, they need to be pre-rendered by Nitro.
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
export default defineNuxtConfig({
|
|
66
|
-
nitro: {
|
|
67
|
-
prerender: {
|
|
68
|
-
crawlLinks: true,
|
|
69
|
-
routes: [
|
|
70
|
-
'/',
|
|
71
|
-
// any URLs that can't be discovered by crawler
|
|
72
|
-
'/my-hidden-url'
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Set host (optional)
|
|
80
|
-
|
|
81
|
-
You'll need to provide the host of your site so that the crawler can resolve absolute URLs that may be internal.
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
export default defineNuxtConfig({
|
|
85
|
-
// Recommended
|
|
86
|
-
runtimeConfig: {
|
|
87
|
-
public: {
|
|
88
|
-
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'https://example.com',
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
// OR
|
|
92
|
-
linkChecker: {
|
|
93
|
-
host: 'https://example.com',
|
|
94
|
-
},
|
|
95
|
-
})
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Exclude URLs from throwing errors
|
|
43
|
+
## Installation
|
|
99
44
|
|
|
100
|
-
|
|
45
|
+
1. Install `nuxt-link-checker` dependency to your project:
|
|
101
46
|
|
|
102
|
-
For example, if you have an `/admin` route that is a separate application, you can ignore all `/admin` links with:
|
|
103
47
|
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
})
|
|
48
|
+
```bash
|
|
49
|
+
#
|
|
50
|
+
yarn add -D nuxt-link-checker
|
|
51
|
+
#
|
|
52
|
+
npm install -D nuxt-link-checker
|
|
53
|
+
#
|
|
54
|
+
pnpm i -D nuxt-link-checker
|
|
112
55
|
```
|
|
113
56
|
|
|
114
|
-
### Disable errors on broken links
|
|
115
57
|
|
|
116
|
-
|
|
58
|
+
2. Add it to your `modules` section in your `nuxt.config`:
|
|
117
59
|
|
|
118
60
|
```ts
|
|
119
61
|
export default defineNuxtConfig({
|
|
120
|
-
|
|
121
|
-
failOn404: false,
|
|
122
|
-
},
|
|
62
|
+
modules: ['nuxt-link-checker']
|
|
123
63
|
})
|
|
124
64
|
```
|
|
125
65
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
### `failOn404`
|
|
129
|
-
|
|
130
|
-
- Type: `boolean`
|
|
131
|
-
- Default: `true`
|
|
132
|
-
|
|
133
|
-
If set to `true`, the build will fail if any broken links are found.
|
|
66
|
+
# Documentation
|
|
134
67
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
- Type: `string[]`
|
|
138
|
-
- Default: `[]`
|
|
139
|
-
|
|
140
|
-
An array of URLs to exclude from the check.
|
|
141
|
-
|
|
142
|
-
This can be useful if you have a route that is not pre-rendered, but you know it will be valid.
|
|
143
|
-
|
|
144
|
-
### `host`
|
|
145
|
-
|
|
146
|
-
- Type: `string`
|
|
147
|
-
- Default: `runtimeConfig.public.siteUrl || localhost`
|
|
148
|
-
- Required: `false`
|
|
149
|
-
|
|
150
|
-
The host of your site. This is required to validate absolute URLs which may be internal.
|
|
151
|
-
|
|
152
|
-
### `trailingSlash`
|
|
153
|
-
|
|
154
|
-
- Type: `boolean`
|
|
155
|
-
- Default: `false`
|
|
156
|
-
|
|
157
|
-
Whether internal links should have a trailing slash or not.
|
|
68
|
+
[📖 Read the full documentation](https://nuxtseo.com/nuxt-link-checker/getting-started/installation) for more information.
|
|
158
69
|
|
|
159
70
|
## Sponsors
|
|
160
71
|
|
|
@@ -164,7 +75,6 @@ Whether internal links should have a trailing slash or not.
|
|
|
164
75
|
</a>
|
|
165
76
|
</p>
|
|
166
77
|
|
|
167
|
-
|
|
168
78
|
## License
|
|
169
79
|
|
|
170
80
|
MIT License © 2023-PRESENT [Harlan Wilton](https://github.com/harlan-zw)
|
package/dist/module.d.ts
CHANGED
|
@@ -1,14 +1,83 @@
|
|
|
1
1
|
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
import { FetchResponse } from 'ofetch';
|
|
3
|
+
import { SiteConfig } from 'nuxt-site-config-kit';
|
|
4
|
+
import Fuse from 'fuse.js';
|
|
5
|
+
import { ParsedURL } from 'ufo';
|
|
6
|
+
|
|
7
|
+
interface Rule {
|
|
8
|
+
test(ctx: RuleTestContext): void;
|
|
9
|
+
}
|
|
10
|
+
interface RuleTestContext {
|
|
11
|
+
link: string;
|
|
12
|
+
url: ParsedURL;
|
|
13
|
+
ids: string[];
|
|
14
|
+
fromPath: string;
|
|
15
|
+
response: FetchResponse<any>;
|
|
16
|
+
siteConfig: SiteConfig;
|
|
17
|
+
pageSearch?: Fuse<string>;
|
|
18
|
+
report: (report: RuleReport) => void;
|
|
19
|
+
skipInspections?: string[];
|
|
20
|
+
}
|
|
21
|
+
interface RuleReport {
|
|
22
|
+
name: string;
|
|
23
|
+
scope: 'error' | 'warning';
|
|
24
|
+
message: string;
|
|
25
|
+
fix?: string;
|
|
26
|
+
fixDescription?: string;
|
|
27
|
+
tip?: string;
|
|
28
|
+
canRetry?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare const DefaultInspections: {
|
|
32
|
+
readonly 'missing-hash': Rule;
|
|
33
|
+
readonly 'no-error-response': Rule;
|
|
34
|
+
readonly 'no-baseless': Rule;
|
|
35
|
+
readonly 'no-javascript': Rule;
|
|
36
|
+
readonly 'trailing-slash': Rule;
|
|
37
|
+
readonly 'absolute-site-urls': Rule;
|
|
38
|
+
readonly redirects: Rule;
|
|
39
|
+
};
|
|
2
40
|
|
|
3
41
|
interface ModuleOptions {
|
|
4
42
|
/**
|
|
5
43
|
* Whether the build should fail when a 404 is encountered.
|
|
6
44
|
*/
|
|
7
|
-
|
|
45
|
+
failOnError: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Skip specific inspections from running.
|
|
48
|
+
*/
|
|
49
|
+
skipInspections: (Partial<keyof typeof DefaultInspections>)[];
|
|
50
|
+
/**
|
|
51
|
+
* The timeout for fetching a URL.
|
|
52
|
+
*
|
|
53
|
+
* @default 5000
|
|
54
|
+
*/
|
|
55
|
+
fetchTimeout: number;
|
|
56
|
+
/**
|
|
57
|
+
* Links to ignore when running inspections.
|
|
58
|
+
*/
|
|
59
|
+
excludeLinks: string[];
|
|
60
|
+
/**
|
|
61
|
+
* Generate a report when using nuxt build` or `nuxt generate`.
|
|
62
|
+
*/
|
|
63
|
+
report?: {
|
|
64
|
+
/**
|
|
65
|
+
* Whether to output a HTML report.
|
|
66
|
+
*/
|
|
67
|
+
html?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Whether to output a JSON report.
|
|
70
|
+
*/
|
|
71
|
+
markdown?: boolean;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Whether to show live inspections in your Nuxt app.
|
|
75
|
+
*/
|
|
76
|
+
showLiveInspections: boolean;
|
|
8
77
|
/**
|
|
9
|
-
*
|
|
78
|
+
* Whether to run the module on `nuxt build` or `nuxt generate`.
|
|
10
79
|
*/
|
|
11
|
-
|
|
80
|
+
runOnBuild: boolean;
|
|
12
81
|
/**
|
|
13
82
|
* Whether the module is enabled.
|
|
14
83
|
*
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,49 +1,18 @@
|
|
|
1
1
|
import { useNuxt, extendPages, defineNuxtModule, useLogger, createResolver, addPlugin, addServerHandler, addServerPlugin, hasNuxtModule } from '@nuxt/kit';
|
|
2
|
-
import { installNuxtSiteConfig, updateSiteConfig } from 'nuxt-site-config-kit';
|
|
2
|
+
import { useSiteConfig, installNuxtSiteConfig, updateSiteConfig } from 'nuxt-site-config-kit';
|
|
3
|
+
import fs, { readFile, writeFile } from 'node:fs/promises';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
|
-
import
|
|
5
|
+
import Fuse from 'fuse.js';
|
|
6
|
+
import { resolve } from 'pathe';
|
|
5
7
|
import { load } from 'cheerio';
|
|
6
8
|
import { toRouteMatcher, createRouter } from 'radix3';
|
|
9
|
+
import { parseURL, joinURL } from 'ufo';
|
|
10
|
+
import { fixSlashes } from 'site-config-stack';
|
|
7
11
|
import { existsSync } from 'node:fs';
|
|
8
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
9
12
|
import { onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit';
|
|
10
13
|
import { diffLines, diffArrays } from 'diff';
|
|
11
14
|
import MagicString from 'magic-string';
|
|
12
15
|
|
|
13
|
-
const linkMap = {};
|
|
14
|
-
const EXT_REGEX = /\.[\da-z]+$/;
|
|
15
|
-
const allowedExtensions = /* @__PURE__ */ new Set(["", ".json"]);
|
|
16
|
-
function getExtension(path) {
|
|
17
|
-
return (path.match(EXT_REGEX) || [])[0] || "";
|
|
18
|
-
}
|
|
19
|
-
function extractLinks(html, from, { host, trailingSlash }) {
|
|
20
|
-
const links = [];
|
|
21
|
-
const hostname = parseURL(host).host;
|
|
22
|
-
const $ = load(html);
|
|
23
|
-
$("body [href]").each((i, el) => {
|
|
24
|
-
const href = $(el).attr("href");
|
|
25
|
-
if (!href)
|
|
26
|
-
return;
|
|
27
|
-
const url = parseURL(href);
|
|
28
|
-
if (hasProtocol(href) && !href.startsWith("/") && url.host !== hostname)
|
|
29
|
-
return;
|
|
30
|
-
if (href.startsWith("#"))
|
|
31
|
-
return;
|
|
32
|
-
if (el.tagName === "link" && el.attribs.rel === "canonical")
|
|
33
|
-
return;
|
|
34
|
-
if (!allowedExtensions.has(getExtension(href)))
|
|
35
|
-
return;
|
|
36
|
-
links.push({
|
|
37
|
-
pathname: url.pathname || "/",
|
|
38
|
-
url,
|
|
39
|
-
badAbsolute: Boolean(hostname) && hostname === url.host,
|
|
40
|
-
badTrailingSlash: url.pathname !== "/" && !url.pathname.split("/").at(-1).includes(".") && (trailingSlash && !url.pathname.endsWith("/") || !trailingSlash && url.pathname.endsWith("/")),
|
|
41
|
-
element: $.html(el) || ""
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
return links;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
16
|
function createFilter(options = {}) {
|
|
48
17
|
const include = options.include || [];
|
|
49
18
|
const exclude = options.exclude || [];
|
|
@@ -71,65 +40,365 @@ function createFilter(options = {}) {
|
|
|
71
40
|
};
|
|
72
41
|
}
|
|
73
42
|
|
|
74
|
-
|
|
43
|
+
function defineRule(rule) {
|
|
44
|
+
return rule;
|
|
45
|
+
}
|
|
46
|
+
function isInvalidLinkProtocol(link) {
|
|
47
|
+
return link.startsWith("javascript:") || link.startsWith("blob:") || link.startsWith("data:");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function RuleTrailingSlash() {
|
|
51
|
+
return defineRule({
|
|
52
|
+
test({ report, link, siteConfig }) {
|
|
53
|
+
if (!link.startsWith("/") && !link.startsWith(siteConfig.url))
|
|
54
|
+
return;
|
|
55
|
+
const $url = parseURL(link);
|
|
56
|
+
if ($url.pathname === "/")
|
|
57
|
+
return;
|
|
58
|
+
const fix = fixSlashes(siteConfig.trailingSlash, link);
|
|
59
|
+
if (!$url.pathname.endsWith("/") && siteConfig.trailingSlash) {
|
|
60
|
+
report({
|
|
61
|
+
name: "trailing-slash",
|
|
62
|
+
scope: "warning",
|
|
63
|
+
message: "Should have a trailing slash.",
|
|
64
|
+
tip: "Incorrect trailing slashes can cause duplicate pages in search engines and waste crawl budget.",
|
|
65
|
+
fix,
|
|
66
|
+
fixDescription: "Add trailing slash."
|
|
67
|
+
});
|
|
68
|
+
} else if ($url.pathname.endsWith("/") && !siteConfig.trailingSlash) {
|
|
69
|
+
report({
|
|
70
|
+
name: "trailing-slash",
|
|
71
|
+
scope: "warning",
|
|
72
|
+
message: "Should not have a trailing slash.",
|
|
73
|
+
tip: "Incorrect trailing slashes can cause duplicate pages in search engines and waste crawl budget.",
|
|
74
|
+
fix,
|
|
75
|
+
fixDescription: "Removing trailing slash."
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function RuleMissingHash() {
|
|
83
|
+
return defineRule({
|
|
84
|
+
test({ link, report, ids, fromPath }) {
|
|
85
|
+
const [path, hash] = link.split("#");
|
|
86
|
+
if (!link.includes("#") || fixSlashes(false, path) !== fromPath)
|
|
87
|
+
return;
|
|
88
|
+
if (ids.includes(hash))
|
|
89
|
+
return;
|
|
90
|
+
const fuse = new Fuse(ids, {
|
|
91
|
+
threshold: 0.6
|
|
92
|
+
});
|
|
93
|
+
const fixedHash = fuse.search(hash.replace("#", ""))?.[0]?.item;
|
|
94
|
+
const payload = {
|
|
95
|
+
name: "missing-hash",
|
|
96
|
+
scope: "error",
|
|
97
|
+
message: `No element with id "${hash}" found.`
|
|
98
|
+
};
|
|
99
|
+
if (fixedHash) {
|
|
100
|
+
payload.fix = `${link.split("#")[0]}#${fixedHash}`;
|
|
101
|
+
payload.fixDescription = `Did you mean ${payload.fix}?`;
|
|
102
|
+
}
|
|
103
|
+
report(payload);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function RuleNoBaseLess() {
|
|
109
|
+
return defineRule({
|
|
110
|
+
test({ link, fromPath, report }) {
|
|
111
|
+
if (link.startsWith("/") || link.startsWith("http") || isInvalidLinkProtocol(link) || link.startsWith("#"))
|
|
112
|
+
return;
|
|
113
|
+
report({
|
|
114
|
+
name: "no-baseless",
|
|
115
|
+
scope: "warning",
|
|
116
|
+
message: "Should not have a base.",
|
|
117
|
+
fix: `${joinURL(fromPath, link)}`,
|
|
118
|
+
fixDescription: `Add base ${fromPath}.`
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function RuleNoJavascript() {
|
|
125
|
+
return defineRule({
|
|
126
|
+
test({ link, report }) {
|
|
127
|
+
if (link.startsWith("javascript:")) {
|
|
128
|
+
report({
|
|
129
|
+
name: "no-javascript",
|
|
130
|
+
scope: "error",
|
|
131
|
+
tip: 'Using a <button type="button"> instead as a better practice.',
|
|
132
|
+
message: "Should not use JavaScript"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function RuleAbsoluteSiteUrls() {
|
|
140
|
+
return defineRule({
|
|
141
|
+
test({ report, link, siteConfig }) {
|
|
142
|
+
if (!link.startsWith(siteConfig.url))
|
|
143
|
+
return;
|
|
144
|
+
const $url = parseURL(link);
|
|
145
|
+
report({
|
|
146
|
+
name: "absolute-site-urls",
|
|
147
|
+
scope: "warning",
|
|
148
|
+
message: "Internal links should be relative.",
|
|
149
|
+
tip: "Using internal links that start with / is recommended to avoid issues when deploying your site to different domain names",
|
|
150
|
+
fix: $url.pathname,
|
|
151
|
+
fixDescription: `Remove ${siteConfig.url}.`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function RuleRedirects() {
|
|
158
|
+
return defineRule({
|
|
159
|
+
test({ report, response }) {
|
|
160
|
+
if (response.status !== 301 && response.status !== 302)
|
|
161
|
+
return;
|
|
162
|
+
const payload = {
|
|
163
|
+
name: "redirects",
|
|
164
|
+
scope: "warning",
|
|
165
|
+
message: "Should not redirect.",
|
|
166
|
+
tip: "Redirects use up your crawl budget and increase loading times, it's recommended to avoid them when possible."
|
|
167
|
+
};
|
|
168
|
+
const fix = typeof response.headers?.get === "function" ? response.headers.get("location") : response.headers?.location || false;
|
|
169
|
+
if (fix) {
|
|
170
|
+
payload.fix = fix;
|
|
171
|
+
payload.fixDescription = `Set to redirect URL ${fix}.`;
|
|
172
|
+
}
|
|
173
|
+
report(payload);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function RuleNoErrorResponse() {
|
|
179
|
+
return defineRule({
|
|
180
|
+
test({ link, response, report, pageSearch }) {
|
|
181
|
+
if (response.status.toString().startsWith("2") || response.status.toString().startsWith("3") || isInvalidLinkProtocol(link) || link.startsWith("#"))
|
|
182
|
+
return;
|
|
183
|
+
const payload = {
|
|
184
|
+
name: "no-error-response",
|
|
185
|
+
scope: "error",
|
|
186
|
+
message: `Should not respond with ${response.status} ${response.statusText}.`
|
|
187
|
+
};
|
|
188
|
+
if (link.startsWith("/") && pageSearch) {
|
|
189
|
+
const fix = pageSearch.search(link)?.[0]?.item;
|
|
190
|
+
if (fix && fix !== link) {
|
|
191
|
+
payload.fix = fix;
|
|
192
|
+
payload.fixDescription = `Did you mean ${fix}?`;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
payload.canRetry = true;
|
|
196
|
+
}
|
|
197
|
+
report(payload);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const DefaultInspections = {
|
|
203
|
+
"missing-hash": RuleMissingHash(),
|
|
204
|
+
"no-error-response": RuleNoErrorResponse(),
|
|
205
|
+
"no-baseless": RuleNoBaseLess(),
|
|
206
|
+
"no-javascript": RuleNoJavascript(),
|
|
207
|
+
"trailing-slash": RuleTrailingSlash(),
|
|
208
|
+
"absolute-site-urls": RuleAbsoluteSiteUrls(),
|
|
209
|
+
"redirects": RuleRedirects()
|
|
210
|
+
};
|
|
211
|
+
function inspect(ctx, rules = DefaultInspections) {
|
|
212
|
+
const res = { error: [], warning: [], fix: ctx.link, link: ctx.link };
|
|
213
|
+
let link = ctx.link;
|
|
214
|
+
const url = parseURL(link);
|
|
215
|
+
if (!url.pathname && !url.protocol && !url.host && !link.startsWith("javascript:")) {
|
|
216
|
+
res.error.push({
|
|
217
|
+
name: "invalid-url",
|
|
218
|
+
scope: "error",
|
|
219
|
+
message: `Invalid URL: ${link}`
|
|
220
|
+
});
|
|
221
|
+
return res;
|
|
222
|
+
}
|
|
223
|
+
const validInspections = Object.entries(rules).filter(([name]) => !ctx.skipInspections || !ctx.skipInspections.includes(name)).map(([, rule]) => rule);
|
|
224
|
+
for (const rule of validInspections) {
|
|
225
|
+
rule.test({
|
|
226
|
+
...ctx,
|
|
227
|
+
link,
|
|
228
|
+
url,
|
|
229
|
+
report(obj) {
|
|
230
|
+
res[obj.scope].push(obj);
|
|
231
|
+
if (obj.fix)
|
|
232
|
+
link = obj.fix;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
res.passes = !res.error?.length && !res.warning?.length;
|
|
237
|
+
res.fix = link;
|
|
238
|
+
return res;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function crawlFetch(link, options = {}) {
|
|
242
|
+
const fetch = options.fetch || globalThis.fetch;
|
|
243
|
+
const timeout = options.timeout || 5e3;
|
|
244
|
+
const timeoutController = new AbortController();
|
|
245
|
+
const abortRequestTimeout = setTimeout(() => timeoutController.abort(), timeout);
|
|
246
|
+
return await fetch(link, {
|
|
247
|
+
method: "HEAD",
|
|
248
|
+
signal: timeoutController.signal,
|
|
249
|
+
headers: {
|
|
250
|
+
"user-agent": "Nuxt Link Checker"
|
|
251
|
+
}
|
|
252
|
+
}).catch(() => ({ status: 404, statusText: "Not Found", headers: {} })).finally(() => clearTimeout(abortRequestTimeout));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const responses = {};
|
|
256
|
+
const linkMap = {};
|
|
257
|
+
function extractPayload(html) {
|
|
258
|
+
const $ = load(html);
|
|
259
|
+
const ids = $("#__nuxt [id]").map((i, el) => $(el).attr("id")).get();
|
|
260
|
+
const links = $("#__nuxt a[href]").map((i, el) => $(el).attr("href")).get();
|
|
261
|
+
return { ids, links };
|
|
262
|
+
}
|
|
263
|
+
async function getLinkResponse(link, timeout) {
|
|
264
|
+
const response = responses[link];
|
|
265
|
+
if (!response) {
|
|
266
|
+
responses[link] = crawlFetch(link, { timeout });
|
|
267
|
+
}
|
|
268
|
+
return responses[link];
|
|
269
|
+
}
|
|
75
270
|
function prerender(config, nuxt = useNuxt()) {
|
|
76
271
|
const urlFilter = createFilter({
|
|
77
|
-
exclude: config.
|
|
272
|
+
exclude: config.excludeLinks
|
|
78
273
|
});
|
|
79
274
|
nuxt.hooks.hook("nitro:init", async (nitro) => {
|
|
80
|
-
const
|
|
275
|
+
const siteConfig = useSiteConfig();
|
|
81
276
|
nitro.hooks.hook("prerender:generate", async (ctx) => {
|
|
82
|
-
if (ctx.contents && ctx.fileName?.endsWith(".html"))
|
|
83
|
-
linkMap[ctx.route] =
|
|
84
|
-
|
|
85
|
-
|
|
277
|
+
if (ctx.contents && !ctx.error && ctx.fileName?.endsWith(".html") && !ctx.route.endsWith(".html") && urlFilter(ctx.route)) {
|
|
278
|
+
linkMap[ctx.route] = extractPayload(ctx.contents);
|
|
279
|
+
linkMap[ctx.route].links.forEach((link) => {
|
|
280
|
+
getLinkResponse(link, config.fetchTimeout);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
responses[ctx.route] = Promise.resolve({ status: Number(ctx.error?.statusCode) || 200, statusText: ctx.error?.statusMessage || "" });
|
|
86
284
|
});
|
|
87
285
|
nitro.hooks.hook("close", async () => {
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
286
|
+
const payloads = Object.entries(linkMap);
|
|
287
|
+
if (!payloads.length)
|
|
90
288
|
return;
|
|
91
|
-
|
|
289
|
+
const links = payloads.map(([, payloads2]) => payloads2.links).flat();
|
|
290
|
+
const pageSearcher = new Fuse(links, {
|
|
291
|
+
threshold: 0.5
|
|
292
|
+
});
|
|
293
|
+
nitro.logger.info("Running link inspections...");
|
|
92
294
|
let routeCount = 0;
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
brokenLinks.forEach((link) => {
|
|
106
|
-
badLinkCount++;
|
|
107
|
-
nitro.logger.log("");
|
|
108
|
-
if (link.statusCode !== 200) {
|
|
109
|
-
nitro.logger.log(chalk.red(
|
|
110
|
-
` ${link.statusCode} ${link.statusCode === 404 ? "Not Found" : "Redirect"}`
|
|
111
|
-
));
|
|
112
|
-
} else if (link.badAbsolute) {
|
|
113
|
-
nitro.logger.log(chalk.yellow(
|
|
114
|
-
" Absolute link, should be relative"
|
|
115
|
-
));
|
|
116
|
-
} else if (link.badTrailingSlash) {
|
|
117
|
-
nitro.logger.log(chalk.yellow(
|
|
118
|
-
" Incorrect trailing slash"
|
|
119
|
-
));
|
|
120
|
-
}
|
|
121
|
-
nitro.logger.log(` ${chalk.gray(link.element)}`);
|
|
295
|
+
let errorCount = 0;
|
|
296
|
+
await Promise.all(payloads.map(async ([route, payload]) => {
|
|
297
|
+
const reports = await Promise.all(payload.links.map(async (link) => {
|
|
298
|
+
const response = await getLinkResponse(link);
|
|
299
|
+
return inspect({
|
|
300
|
+
ids: linkMap[route].ids,
|
|
301
|
+
fromPath: route,
|
|
302
|
+
pageSearch: pageSearcher,
|
|
303
|
+
siteConfig,
|
|
304
|
+
link,
|
|
305
|
+
response,
|
|
306
|
+
skipInspections: config.skipInspections
|
|
122
307
|
});
|
|
308
|
+
}));
|
|
309
|
+
const valid = !reports.filter((r) => !r.passes).length;
|
|
310
|
+
if (valid)
|
|
311
|
+
return;
|
|
312
|
+
const errors = reports.filter((r) => r.error?.length).length;
|
|
313
|
+
errorCount += errors;
|
|
314
|
+
const warnings = reports.filter((r) => r.warning?.length).length;
|
|
315
|
+
const statusString = [
|
|
316
|
+
errors > 0 ? chalk.red(`${errors} error${errors > 1 ? "s" : ""}`) : false,
|
|
317
|
+
warnings > 0 ? chalk.yellow(`${warnings} warning${warnings > 1 ? "s" : ""}`) : false
|
|
318
|
+
].filter(Boolean).join(chalk.gray(", "));
|
|
319
|
+
nitro.logger.log(chalk.gray(
|
|
320
|
+
` ${Number(++routeCount) === payload.links.length - 1 ? "\u2514\u2500" : "\u251C\u2500"} ${chalk.white(route)} ${chalk.gray("[")}${statusString}${chalk.gray("]")}`
|
|
321
|
+
));
|
|
322
|
+
reports.forEach((report) => {
|
|
323
|
+
if (!report.passes) {
|
|
324
|
+
nitro.logger.log(chalk.gray(` ${report.link}`));
|
|
325
|
+
report.error?.forEach((error) => {
|
|
326
|
+
nitro.logger.log(chalk.red(` \u2716 ${error.message}`) + chalk.gray(` (${error.name})`));
|
|
327
|
+
});
|
|
328
|
+
report.warning?.forEach((warning) => {
|
|
329
|
+
nitro.logger.log(chalk.yellow(` \u26A0 ${warning.message}`) + chalk.gray(` (${warning.name})`));
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
if (config.report?.html) {
|
|
334
|
+
if (reports.length) {
|
|
335
|
+
const reportHtml = reports.map((r) => {
|
|
336
|
+
const errors2 = r.error?.map((error) => {
|
|
337
|
+
return `<li class="error">${error.message} (${error.name})</li>`;
|
|
338
|
+
}).join("");
|
|
339
|
+
const warnings2 = r.warning?.map((warning) => {
|
|
340
|
+
return `<li class="warning">${warning.message} (${warning.name})</li>`;
|
|
341
|
+
}).join("");
|
|
342
|
+
return `<li class="link"><a href="${r.link}">${r.link}</a><ul>${errors2}${warnings2}</ul></li>`;
|
|
343
|
+
}).join("");
|
|
344
|
+
const html = `
|
|
345
|
+
<html>
|
|
346
|
+
<head>
|
|
347
|
+
<title>Link Checker Report</title>
|
|
348
|
+
<style>
|
|
349
|
+
body {
|
|
350
|
+
font-family: sans-serif;
|
|
351
|
+
}
|
|
352
|
+
.link {
|
|
353
|
+
margin-bottom: 1rem;
|
|
354
|
+
}
|
|
355
|
+
.error {
|
|
356
|
+
color: red;
|
|
357
|
+
}
|
|
358
|
+
.warning {
|
|
359
|
+
color: yellow;
|
|
360
|
+
}
|
|
361
|
+
</style>
|
|
362
|
+
</head>
|
|
363
|
+
<body>
|
|
364
|
+
<h1>Link Checker Report</h1>
|
|
365
|
+
<ul>
|
|
366
|
+
${reportHtml}
|
|
367
|
+
</ul>
|
|
368
|
+
</body>
|
|
369
|
+
</html>
|
|
370
|
+
`;
|
|
371
|
+
await fs.writeFile(resolve(nitro.options.output.dir, "link-checker-report.html"), html);
|
|
372
|
+
nitro.logger.info(`Nuxt Link Checker Report written to ${resolve(nitro.options.output.dir, "link-checker-report.html")}`);
|
|
373
|
+
}
|
|
123
374
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
375
|
+
if (config.report?.markdown) {
|
|
376
|
+
if (reports.length) {
|
|
377
|
+
const reportMarkdown = reports.map((r) => {
|
|
378
|
+
const errors2 = r.error?.map((error) => {
|
|
379
|
+
return `| ${r.link} | ${error.message} (${error.name}) |`;
|
|
380
|
+
}).join("");
|
|
381
|
+
const warnings2 = r.warning?.map((warning) => {
|
|
382
|
+
return `| ${r.link} | ${warning.message} (${warning.name}) |`;
|
|
383
|
+
}).join("");
|
|
384
|
+
return `${errors2}${warnings2}`;
|
|
385
|
+
}).join("");
|
|
386
|
+
const markdown = [
|
|
387
|
+
"# Link Checker Report",
|
|
388
|
+
"",
|
|
389
|
+
"| Link | Message |",
|
|
390
|
+
"| --- | --- |",
|
|
391
|
+
reportMarkdown
|
|
392
|
+
].join("\n");
|
|
393
|
+
await fs.writeFile(resolve(nitro.options.output.dir, "link-checker-report.md"), markdown);
|
|
394
|
+
nitro.logger.info(`Nuxt Link Checker Report written to ${resolve(nitro.options.output.dir, "link-checker-report.md")}`);
|
|
395
|
+
}
|
|
130
396
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
397
|
+
}));
|
|
398
|
+
if (errorCount > 0 && config.failOnError) {
|
|
399
|
+
nitro.logger.error(`Nuxt Link Checker found ${errorCount} errors, failing build.`);
|
|
400
|
+
nitro.logger.log(chalk.gray('You can disable this by setting "linkChecker: { failOn404: false }" in your nuxt.config.ts.'));
|
|
401
|
+
process.exit(1);
|
|
133
402
|
}
|
|
134
403
|
});
|
|
135
404
|
});
|
|
@@ -298,10 +567,14 @@ const module = defineNuxtModule({
|
|
|
298
567
|
configKey: "linkChecker"
|
|
299
568
|
},
|
|
300
569
|
defaults: {
|
|
570
|
+
runOnBuild: true,
|
|
301
571
|
debug: false,
|
|
572
|
+
showLiveInspections: true,
|
|
302
573
|
enabled: true,
|
|
303
|
-
|
|
304
|
-
|
|
574
|
+
fetchTimeout: 5e3,
|
|
575
|
+
failOnError: false,
|
|
576
|
+
excludeLinks: [],
|
|
577
|
+
skipInspections: []
|
|
305
578
|
},
|
|
306
579
|
async setup(config, nuxt) {
|
|
307
580
|
const logger = useLogger("nuxt-link-checker");
|
|
@@ -337,13 +610,18 @@ const module = defineNuxtModule({
|
|
|
337
610
|
handler: resolve("./runtime/server/api/links")
|
|
338
611
|
});
|
|
339
612
|
}
|
|
340
|
-
nuxt.options.runtimeConfig["nuxt-link-checker"] = {
|
|
613
|
+
nuxt.options.runtimeConfig.public["nuxt-link-checker"] = {
|
|
341
614
|
hasSitemapModule: hasNuxtModule("nuxt-simple-sitemap"),
|
|
342
|
-
hasLinksEndpoint
|
|
615
|
+
hasLinksEndpoint,
|
|
616
|
+
excludeLinks: config.excludeLinks,
|
|
617
|
+
skipInspections: config.skipInspections,
|
|
618
|
+
fetchTimeout: config.fetchTimeout,
|
|
619
|
+
showLiveInspections: config.showLiveInspections
|
|
343
620
|
};
|
|
344
621
|
setupDevToolsUI(config, resolve);
|
|
345
622
|
}
|
|
346
|
-
|
|
623
|
+
if (config.runOnBuild)
|
|
624
|
+
prerender(config);
|
|
347
625
|
}
|
|
348
626
|
});
|
|
349
627
|
|
|
@@ -1,2 +1,19 @@
|
|
|
1
1
|
import type { LinkInspectionResult, Rule, RuleTestContext } from './types';
|
|
2
|
-
export declare
|
|
2
|
+
export declare const DefaultInspections: {
|
|
3
|
+
readonly 'missing-hash': Rule;
|
|
4
|
+
readonly 'no-error-response': Rule;
|
|
5
|
+
readonly 'no-baseless': Rule;
|
|
6
|
+
readonly 'no-javascript': Rule;
|
|
7
|
+
readonly 'trailing-slash': Rule;
|
|
8
|
+
readonly 'absolute-site-urls': Rule;
|
|
9
|
+
readonly redirects: Rule;
|
|
10
|
+
};
|
|
11
|
+
export declare function inspect(ctx: RuleTestContext, rules?: {
|
|
12
|
+
readonly 'missing-hash': Rule;
|
|
13
|
+
readonly 'no-error-response': Rule;
|
|
14
|
+
readonly 'no-baseless': Rule;
|
|
15
|
+
readonly 'no-javascript': Rule;
|
|
16
|
+
readonly 'trailing-slash': Rule;
|
|
17
|
+
readonly 'absolute-site-urls': Rule;
|
|
18
|
+
readonly redirects: Rule;
|
|
19
|
+
}): Partial<LinkInspectionResult>;
|
package/dist/runtime/inspect.mjs
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { getHeader } from "h3";
|
|
2
1
|
import { parseURL } from "ufo";
|
|
3
|
-
import { fixSlashes } from "site-config-stack";
|
|
4
2
|
import RuleTrailingSlash from "./inspections/trailing-slash.mjs";
|
|
5
3
|
import RuleMissingHash from "./inspections/missing-hash.mjs";
|
|
6
4
|
import RuleNoBaseLess from "./inspections/no-baseless.mjs";
|
|
@@ -8,22 +6,20 @@ import RuleNoJavascript from "./inspections/no-javascript.mjs";
|
|
|
8
6
|
import RuleAbsoluteSiteUrls from "./inspections/absolute-site-urls.mjs";
|
|
9
7
|
import RuleRedirects from "./inspections/redirects.mjs";
|
|
10
8
|
import RuleNoErrorResponse from "./inspections/no-error-response-status.mjs";
|
|
11
|
-
const
|
|
12
|
-
RuleMissingHash(),
|
|
13
|
-
RuleNoErrorResponse(),
|
|
14
|
-
RuleNoBaseLess(),
|
|
15
|
-
RuleNoJavascript(),
|
|
16
|
-
RuleTrailingSlash(),
|
|
17
|
-
RuleAbsoluteSiteUrls(),
|
|
18
|
-
RuleRedirects()
|
|
19
|
-
|
|
20
|
-
export function inspect(ctx, rules) {
|
|
21
|
-
if (!rules)
|
|
22
|
-
rules = inspection;
|
|
9
|
+
export const DefaultInspections = {
|
|
10
|
+
"missing-hash": RuleMissingHash(),
|
|
11
|
+
"no-error-response": RuleNoErrorResponse(),
|
|
12
|
+
"no-baseless": RuleNoBaseLess(),
|
|
13
|
+
"no-javascript": RuleNoJavascript(),
|
|
14
|
+
"trailing-slash": RuleTrailingSlash(),
|
|
15
|
+
"absolute-site-urls": RuleAbsoluteSiteUrls(),
|
|
16
|
+
"redirects": RuleRedirects()
|
|
17
|
+
};
|
|
18
|
+
export function inspect(ctx, rules = DefaultInspections) {
|
|
23
19
|
const res = { error: [], warning: [], fix: ctx.link, link: ctx.link };
|
|
24
20
|
let link = ctx.link;
|
|
25
21
|
const url = parseURL(link);
|
|
26
|
-
if (!url.pathname && !url.protocol && !url.host) {
|
|
22
|
+
if (!url.pathname && !url.protocol && !url.host && !link.startsWith("javascript:")) {
|
|
27
23
|
res.error.push({
|
|
28
24
|
name: "invalid-url",
|
|
29
25
|
scope: "error",
|
|
@@ -31,13 +27,12 @@ export function inspect(ctx, rules) {
|
|
|
31
27
|
});
|
|
32
28
|
return res;
|
|
33
29
|
}
|
|
34
|
-
const
|
|
35
|
-
for (const rule of
|
|
30
|
+
const validInspections = Object.entries(rules).filter(([name]) => !ctx.skipInspections || !ctx.skipInspections.includes(name)).map(([, rule]) => rule);
|
|
31
|
+
for (const rule of validInspections) {
|
|
36
32
|
rule.test({
|
|
37
33
|
...ctx,
|
|
38
34
|
link,
|
|
39
35
|
url,
|
|
40
|
-
fromPath,
|
|
41
36
|
report(obj) {
|
|
42
37
|
res[obj.scope].push(obj);
|
|
43
38
|
if (obj.fix)
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { joinURL } from "ufo";
|
|
2
|
-
import { getHeader } from "h3";
|
|
3
2
|
import { defineRule, isInvalidLinkProtocol } from "./util.mjs";
|
|
4
3
|
export default function RuleNoBaseLess() {
|
|
5
4
|
return defineRule({
|
|
6
|
-
test({ link,
|
|
5
|
+
test({ link, fromPath, report }) {
|
|
7
6
|
if (link.startsWith("/") || link.startsWith("http") || isInvalidLinkProtocol(link) || link.startsWith("#"))
|
|
8
7
|
return;
|
|
9
|
-
const fromPath = getHeader(e, "referer") || "";
|
|
10
8
|
report({
|
|
11
9
|
name: "no-baseless",
|
|
12
10
|
scope: "warning",
|
|
@@ -9,7 +9,7 @@ export default function RuleNoErrorResponse() {
|
|
|
9
9
|
scope: "error",
|
|
10
10
|
message: `Should not respond with ${response.status} ${response.statusText}.`
|
|
11
11
|
};
|
|
12
|
-
if (link.startsWith("/")) {
|
|
12
|
+
if (link.startsWith("/") && pageSearch) {
|
|
13
13
|
const fix = pageSearch.search(link)?.[0]?.item;
|
|
14
14
|
if (fix && fix !== link) {
|
|
15
15
|
payload.fix = fix;
|
|
@@ -10,10 +10,10 @@ export default function RuleRedirects() {
|
|
|
10
10
|
message: "Should not redirect.",
|
|
11
11
|
tip: "Redirects use up your crawl budget and increase loading times, it's recommended to avoid them when possible."
|
|
12
12
|
};
|
|
13
|
-
const fix = response.headers.get("location");
|
|
13
|
+
const fix = typeof response.headers?.get === "function" ? response.headers.get("location") : response.headers?.location || false;
|
|
14
14
|
if (fix) {
|
|
15
15
|
payload.fix = fix;
|
|
16
|
-
payload.fixDescription = `Set to
|
|
16
|
+
payload.fixDescription = `Set to redirect URL ${fix}.`;
|
|
17
17
|
}
|
|
18
18
|
report(payload);
|
|
19
19
|
}
|
|
@@ -2,7 +2,7 @@ import Fuse from "fuse.js";
|
|
|
2
2
|
import { defineNitroPlugin } from "nitropack/dist/runtime/plugin";
|
|
3
3
|
import { useRuntimeConfig } from "#imports";
|
|
4
4
|
export default defineNitroPlugin(async (nitro) => {
|
|
5
|
-
const runtimeConfig = useRuntimeConfig()["nuxt-link-checker"];
|
|
5
|
+
const runtimeConfig = useRuntimeConfig().public["nuxt-link-checker"];
|
|
6
6
|
const pages = runtimeConfig.hasLinksEndpoint ? await $fetch("/api/__link_checker__/links") : [];
|
|
7
7
|
nitro._linkCheckerPageSearch = new Fuse(pages, {
|
|
8
8
|
threshold: 0.5
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { computed, createApp, h, ref, shallowReactive, unref } from "vue";
|
|
2
|
+
import { createFilter } from "../../../urlFilter";
|
|
2
3
|
import Main from "./Main.vue";
|
|
3
4
|
import { linkDb } from "./state.mjs";
|
|
4
|
-
import { useRoute } from "#imports";
|
|
5
|
+
import { useRoute, useRuntimeConfig } from "#imports";
|
|
5
6
|
function resolveDevtoolsIframe() {
|
|
6
7
|
return document.querySelector("#nuxt-devtools-iframe")?.contentWindow?.__NUXT_DEVTOOLS__;
|
|
7
8
|
}
|
|
@@ -25,17 +26,23 @@ export async function setupLinkCheckerClient({ nuxt }) {
|
|
|
25
26
|
let devtoolsClient;
|
|
26
27
|
let isOpeningDevtools = false;
|
|
27
28
|
const route = useRoute();
|
|
28
|
-
let
|
|
29
|
-
let
|
|
29
|
+
let startQueueIdleId;
|
|
30
|
+
let startQueueTimeoutId;
|
|
31
|
+
const runtimeConfig = useRuntimeConfig().public["nuxt-link-checker"];
|
|
32
|
+
const filter = createFilter({
|
|
33
|
+
exclude: runtimeConfig.excludeLinks
|
|
34
|
+
});
|
|
30
35
|
const client = shallowReactive({
|
|
31
36
|
isWorkingQueue: false,
|
|
32
37
|
scanLinks() {
|
|
33
38
|
elMap = {};
|
|
34
39
|
visibleLinks.clear();
|
|
35
40
|
const ids = [...new Set([...document.querySelectorAll("#__nuxt [id]")].map((el) => el.id))];
|
|
36
|
-
[...document.querySelectorAll("#__nuxt a")].map((el) => ({ el, link: el.getAttribute("href") })).forEach(({ el, link }) => {
|
|
41
|
+
[...document.querySelectorAll("#__nuxt a[href]")].map((el) => ({ el, link: el.getAttribute("href") })).forEach(({ el, link }) => {
|
|
37
42
|
if (!link)
|
|
38
43
|
return;
|
|
44
|
+
if (!filter(link))
|
|
45
|
+
return;
|
|
39
46
|
visibleLinks.add(link);
|
|
40
47
|
elMap[link] = elMap[link] || [];
|
|
41
48
|
if (elMap[link].includes(el))
|
|
@@ -88,7 +95,7 @@ export async function setupLinkCheckerClient({ nuxt }) {
|
|
|
88
95
|
workQueue();
|
|
89
96
|
},
|
|
90
97
|
maybeAttachEls(payload) {
|
|
91
|
-
if (!payload || payload.passes)
|
|
98
|
+
if (!payload || payload.passes || !runtimeConfig.showLiveInspections)
|
|
92
99
|
return;
|
|
93
100
|
const els = elMap?.[payload.link] || [];
|
|
94
101
|
for (const el of els)
|
|
@@ -144,11 +151,16 @@ export async function setupLinkCheckerClient({ nuxt }) {
|
|
|
144
151
|
client.restart();
|
|
145
152
|
},
|
|
146
153
|
restart() {
|
|
147
|
-
|
|
148
|
-
|
|
154
|
+
startQueueIdleId && cancelIdleCallback(startQueueIdleId);
|
|
155
|
+
startQueueIdleId = requestIdleCallback(() => {
|
|
149
156
|
client.stopQueueWorker();
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
if (!startQueueTimeoutId) {
|
|
158
|
+
startQueueTimeoutId = setTimeout(() => {
|
|
159
|
+
client.scanLinks();
|
|
160
|
+
client.startQueueWorker();
|
|
161
|
+
startQueueTimeoutId = false;
|
|
162
|
+
}, 250);
|
|
163
|
+
}
|
|
152
164
|
});
|
|
153
165
|
},
|
|
154
166
|
start() {
|
|
@@ -157,18 +169,19 @@ export async function setupLinkCheckerClient({ nuxt }) {
|
|
|
157
169
|
client.reset(true);
|
|
158
170
|
});
|
|
159
171
|
import.meta.hot.on("vite:afterUpdate", (ctx) => {
|
|
160
|
-
console.log("vite after update", ctx.type, ctx.updates);
|
|
161
172
|
if (ctx.updates.some((c) => c.type === "js-update"))
|
|
162
173
|
client.reset(true);
|
|
163
174
|
});
|
|
164
175
|
}
|
|
165
176
|
const observer = new MutationObserver(() => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
177
|
+
client.reset(false);
|
|
178
|
+
});
|
|
179
|
+
observer.observe(document.querySelector("#__nuxt"), {
|
|
180
|
+
childList: true,
|
|
181
|
+
subtree: true,
|
|
182
|
+
// we only care if links are added, removed or updated
|
|
183
|
+
attributeFilter: ["href"]
|
|
170
184
|
});
|
|
171
|
-
observer.observe(document.querySelector("#__nuxt"), { childList: true, subtree: true });
|
|
172
185
|
if (nuxt.vueApp._instance)
|
|
173
186
|
nuxt.vueApp._instance.appContext.provides.linkChecker = client;
|
|
174
187
|
const holder = document.createElement("div");
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { defineEventHandler, getQuery, readBody } from "h3";
|
|
1
|
+
import { defineEventHandler, getHeader, getQuery, readBody } from "h3";
|
|
2
|
+
import { fixSlashes } from "site-config-stack";
|
|
3
|
+
import { parseURL } from "ufo";
|
|
2
4
|
import { inspect } from "../../inspect.mjs";
|
|
3
5
|
import { generateFileLinkDiff, generateFileLinkPreviews } from "../util.mjs";
|
|
4
6
|
import { isInvalidLinkProtocol } from "../../inspections/util.mjs";
|
|
5
|
-
import {
|
|
7
|
+
import { crawlFetch } from "../../sharedUtils.mjs";
|
|
8
|
+
import { useNitroApp, useRuntimeConfig, useSiteConfig } from "#imports";
|
|
6
9
|
export default defineEventHandler(async (e) => {
|
|
7
10
|
const link = decodeURIComponent(getQuery(e).link);
|
|
8
11
|
const body = await readBody(e);
|
|
9
12
|
const { ids, paths } = body;
|
|
10
13
|
const partialCtx = {
|
|
11
14
|
ids,
|
|
12
|
-
e,
|
|
15
|
+
fromPath: fixSlashes(false, parseURL(getHeader(e, "referer") || "/").pathname),
|
|
13
16
|
siteConfig: useSiteConfig(e)
|
|
14
17
|
};
|
|
18
|
+
const runtimeConfig = useRuntimeConfig().public["nuxt-link-checker"];
|
|
15
19
|
let response;
|
|
16
20
|
if (isInvalidLinkProtocol(link) || link.startsWith("#")) {
|
|
17
21
|
response = { status: 200, statusText: "OK", headers: {} };
|
|
18
22
|
} else {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
signal: timeoutController.signal,
|
|
24
|
-
headers: {
|
|
25
|
-
"user-agent": "Nuxt Link Checker"
|
|
26
|
-
}
|
|
27
|
-
}).catch(() => ({ status: 404, statusText: "Not Found", headers: {} })).finally(() => clearTimeout(abortRequestTimeout));
|
|
23
|
+
response = await crawlFetch(link, {
|
|
24
|
+
fetch: $fetch.raw,
|
|
25
|
+
timeout: runtimeConfig.timeout
|
|
26
|
+
});
|
|
28
27
|
}
|
|
29
28
|
const result = inspect({
|
|
30
29
|
...partialCtx,
|
|
31
30
|
link,
|
|
32
31
|
pageSearch: useNitroApp()._linkCheckerPageSearch,
|
|
33
|
-
response
|
|
32
|
+
response,
|
|
33
|
+
skipInspections: runtimeConfig.skipInspections
|
|
34
34
|
});
|
|
35
35
|
const filePaths = paths.map((p) => {
|
|
36
36
|
const [filepath] = p.split(":");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineEventHandler } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
3
|
export default defineEventHandler(async () => {
|
|
4
|
-
const runtimeConfig = useRuntimeConfig()["nuxt-link-checker"];
|
|
4
|
+
const runtimeConfig = useRuntimeConfig().public["nuxt-link-checker"];
|
|
5
5
|
const linkDb = [];
|
|
6
6
|
if (runtimeConfig.hasSitemapModule) {
|
|
7
7
|
const sitemapDebug = await $fetch("/api/__sitemap__/debug");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function crawlFetch(link, options = {}) {
|
|
2
|
+
const fetch = options.fetch || globalThis.fetch;
|
|
3
|
+
const timeout = options.timeout || 5e3;
|
|
4
|
+
const timeoutController = new AbortController();
|
|
5
|
+
const abortRequestTimeout = setTimeout(() => timeoutController.abort(), timeout);
|
|
6
|
+
return await fetch(link, {
|
|
7
|
+
method: "HEAD",
|
|
8
|
+
signal: timeoutController.signal,
|
|
9
|
+
headers: {
|
|
10
|
+
"user-agent": "Nuxt Link Checker"
|
|
11
|
+
}
|
|
12
|
+
}).catch(() => ({ status: 404, statusText: "Not Found", headers: {} })).finally(() => clearTimeout(abortRequestTimeout));
|
|
13
|
+
}
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { FetchResponse } from 'ofetch';
|
|
2
|
-
import type { H3Event } from 'h3';
|
|
3
2
|
import type { SiteConfig } from 'nuxt-site-config-kit';
|
|
4
3
|
import type Fuse from 'fuse.js';
|
|
5
4
|
import type { ComputedRef, Ref } from 'vue';
|
|
@@ -13,10 +12,10 @@ export interface RuleTestContext {
|
|
|
13
12
|
ids: string[];
|
|
14
13
|
fromPath: string;
|
|
15
14
|
response: FetchResponse<any>;
|
|
16
|
-
e: H3Event;
|
|
17
15
|
siteConfig: SiteConfig;
|
|
18
|
-
pageSearch
|
|
16
|
+
pageSearch?: Fuse<string>;
|
|
19
17
|
report: (report: RuleReport) => void;
|
|
18
|
+
skipInspections?: string[];
|
|
20
19
|
}
|
|
21
20
|
export interface RuleReport {
|
|
22
21
|
name: string;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-link-checker",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
5
|
-
"packageManager": "pnpm@8.6.
|
|
4
|
+
"version": "2.0.0-beta.7",
|
|
5
|
+
"packageManager": "pnpm@8.6.11",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"funding": "https://github.com/sponsors/harlan-zw",
|
|
8
8
|
"homepage": "https://github.com/harlan-zw/nuxt-link-checker#readme",
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"import": "./dist/module.mjs"
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
|
+
"build": {
|
|
24
|
+
"externals": [
|
|
25
|
+
"ofetch"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
23
28
|
"main": "./dist/module.cjs",
|
|
24
29
|
"types": "./dist/types.d.ts",
|
|
25
30
|
"files": [
|
|
@@ -41,6 +46,7 @@
|
|
|
41
46
|
"radix3": "^1.0.1",
|
|
42
47
|
"shiki-es": "^0.14.0",
|
|
43
48
|
"sirv": "^2.0.3",
|
|
49
|
+
"site-config-stack": "^1.0.9",
|
|
44
50
|
"ufo": "^1.2.0"
|
|
45
51
|
},
|
|
46
52
|
"devDependencies": {
|