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 CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
 
12
12
  <p align="center">
13
- Improve your sites SEO by identifying and fixing link issues in your Nuxt v3 app.
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> Early Access</b> <br>
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
- sitemap: {
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 © 2022-PRESENT [Harlan Wilton](https://github.com/harlan-zw)
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?: string;
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
@@ -5,5 +5,5 @@
5
5
  "bridge": false
6
6
  },
7
7
  "configKey": "linkChecker",
8
- "version": "0.1.6"
8
+ "version": "0.3.1"
9
9
  }
package/dist/module.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { defineNuxtModule } from '@nuxt/kit';
2
2
  import chalk from 'chalk';
3
- import { isRelative, parseURL } from 'ufo';
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 _links = [];
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
- if (host && !isRelative(href) && href.startsWith(host))
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
- _links.push({
25
- href,
26
- badTrailingSlash: href !== "/" && (trailingSlash && !href.endsWith("/") || !trailingSlash && href.endsWith("/")),
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("Scanning routes for broken links...");
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.href] || 200
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
- ` ${config.trailingSlash ? "Missing" : "Has added"} trailing slash`
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.6",
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.0",
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.30.0",
42
+ "eslint": "8.32.0",
42
43
  "execa": "^6.1.0",
43
44
  "nuxt": "^3.0.0",
44
- "pathe": "^1.0.0",
45
- "vitest": "^0.25.8"
45
+ "pathe": "^1.1.0",
46
+ "vitest": "^0.28.1"
46
47
  },
47
48
  "scripts": {
48
49
  "lint": "eslint \"**/*.{ts,vue,json,yml}\" --fix",