nuxt-link-checker 4.2.0 → 4.3.0
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/dist/client/200.html +2 -2
- package/dist/client/404.html +2 -2
- package/dist/client/_nuxt/builds/latest.json +1 -1
- package/dist/client/_nuxt/builds/meta/958252ce-e0d2-4b11-8149-9f7e96bca1a3.json +1 -0
- package/dist/client/index.html +2 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +218 -45
- package/dist/runtime/shared/crawl.d.ts +10 -6
- package/dist/runtime/shared/crawl.js +37 -10
- package/dist/runtime/shared/inspections/no-error-response.js +1 -1
- package/dist/runtime/shared/inspections/util.js +2 -1
- package/dist/runtime/shared/redirects.js +1 -1
- package/package.json +4 -4
- package/dist/client/_nuxt/builds/meta/cdbc357b-6180-4103-97aa-19c53e0b9c3c.json +0 -1
package/dist/client/200.html
CHANGED
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
<link rel="prefetch" as="script" crossorigin href="/__nuxt-link-checker/_nuxt/BFWaZzNn.js">
|
|
13
13
|
<link rel="prefetch" as="style" crossorigin href="/__nuxt-link-checker/_nuxt/error-500.BAgFwF30.css">
|
|
14
14
|
<link rel="prefetch" as="script" crossorigin href="/__nuxt-link-checker/_nuxt/Cj6XDDRd.js">
|
|
15
|
-
<script type="module" src="/__nuxt-link-checker/_nuxt/5p_Oaa9g.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},
|
|
16
|
-
<script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-link-checker",buildId:"
|
|
15
|
+
<script type="module" src="/__nuxt-link-checker/_nuxt/5p_Oaa9g.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1742303210600,false]</script>
|
|
16
|
+
<script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-link-checker",buildId:"958252ce-e0d2-4b11-8149-9f7e96bca1a3",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
|
package/dist/client/404.html
CHANGED
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
<link rel="prefetch" as="script" crossorigin href="/__nuxt-link-checker/_nuxt/BFWaZzNn.js">
|
|
13
13
|
<link rel="prefetch" as="style" crossorigin href="/__nuxt-link-checker/_nuxt/error-500.BAgFwF30.css">
|
|
14
14
|
<link rel="prefetch" as="script" crossorigin href="/__nuxt-link-checker/_nuxt/Cj6XDDRd.js">
|
|
15
|
-
<script type="module" src="/__nuxt-link-checker/_nuxt/5p_Oaa9g.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},
|
|
16
|
-
<script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-link-checker",buildId:"
|
|
15
|
+
<script type="module" src="/__nuxt-link-checker/_nuxt/5p_Oaa9g.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1742303210600,false]</script>
|
|
16
|
+
<script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-link-checker",buildId:"958252ce-e0d2-4b11-8149-9f7e96bca1a3",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
1
|
+
{"id":"958252ce-e0d2-4b11-8149-9f7e96bca1a3","timestamp":1742303206413}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id":"958252ce-e0d2-4b11-8149-9f7e96bca1a3","timestamp":1742303206413,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}
|
package/dist/client/index.html
CHANGED
|
@@ -12,5 +12,5 @@
|
|
|
12
12
|
<link rel="prefetch" as="script" crossorigin href="/__nuxt-link-checker/_nuxt/BFWaZzNn.js">
|
|
13
13
|
<link rel="prefetch" as="style" crossorigin href="/__nuxt-link-checker/_nuxt/error-500.BAgFwF30.css">
|
|
14
14
|
<link rel="prefetch" as="script" crossorigin href="/__nuxt-link-checker/_nuxt/Cj6XDDRd.js">
|
|
15
|
-
<script type="module" src="/__nuxt-link-checker/_nuxt/5p_Oaa9g.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},
|
|
16
|
-
<script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-link-checker",buildId:"
|
|
15
|
+
<script type="module" src="/__nuxt-link-checker/_nuxt/5p_Oaa9g.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1742303210600,false]</script>
|
|
16
|
+
<script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-link-checker",buildId:"958252ce-e0d2-4b11-8149-9f7e96bca1a3",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { useNuxt, extendPages, defineNuxtModule, createResolver, useLogger, addP
|
|
|
2
2
|
import { useSiteConfig, installNuxtSiteConfig } from 'nuxt-site-config/kit';
|
|
3
3
|
import { resolve, relative, dirname } from 'pathe';
|
|
4
4
|
import { readPackageJSON } from 'pkg-types';
|
|
5
|
+
import { provider } from 'std-env';
|
|
5
6
|
import { existsSync } from 'node:fs';
|
|
6
7
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
7
8
|
import { onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit';
|
|
@@ -13,7 +14,7 @@ import Fuse from 'fuse.js';
|
|
|
13
14
|
import { parse, walkSync, ELEMENT_NODE } from 'ultrahtml';
|
|
14
15
|
import { createStorage } from 'unstorage';
|
|
15
16
|
import fsDriver from 'unstorage/drivers/fs';
|
|
16
|
-
import { setLinkResponse, getLinkResponse, crawlFetch } from '../dist/runtime/shared/crawl.js';
|
|
17
|
+
import { setLinkResponse, getResolvedLinkResponses, getLinkResponse, crawlFetch } from '../dist/runtime/shared/crawl.js';
|
|
17
18
|
import { inspect } from '../dist/runtime/shared/inspect.js';
|
|
18
19
|
import { createFilter } from '../dist/runtime/shared/sharedUtils.js';
|
|
19
20
|
|
|
@@ -642,6 +643,53 @@ const htmlTemplate = html(_a || (_a = __template([`<!DOCTYPE html>
|
|
|
642
643
|
grid-template-columns: 1fr;
|
|
643
644
|
}
|
|
644
645
|
}
|
|
646
|
+
.issues-summary {
|
|
647
|
+
border-radius: var(--border-radius);
|
|
648
|
+
padding: 1.5rem;
|
|
649
|
+
margin-bottom: 2rem;
|
|
650
|
+
box-shadow: var(--box-shadow);
|
|
651
|
+
background-color: var(--color-bg-secondary);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.common-issues-list {
|
|
655
|
+
list-style: none;
|
|
656
|
+
padding: 0;
|
|
657
|
+
margin: 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.common-issue {
|
|
661
|
+
display: flex;
|
|
662
|
+
align-items: flex-start;
|
|
663
|
+
padding: 0.5rem 0;
|
|
664
|
+
border-bottom: 1px solid var(--color-border);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.common-issue:last-child {
|
|
668
|
+
border-bottom: none;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.common-issue.error {
|
|
672
|
+
color: var(--color-error);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.common-issue.warning {
|
|
676
|
+
color: var(--color-warning);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.issue-count {
|
|
680
|
+
font-weight: bold;
|
|
681
|
+
margin-right: 0.75rem;
|
|
682
|
+
min-width: 2em;
|
|
683
|
+
text-align: right;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.error-icon, .warning-icon {
|
|
687
|
+
margin-right: 0.5rem;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.issue-text {
|
|
691
|
+
flex: 1;
|
|
692
|
+
}
|
|
645
693
|
</style>
|
|
646
694
|
</head>
|
|
647
695
|
<body>
|
|
@@ -1148,6 +1196,53 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
1148
1196
|
grid-template-columns: 1fr;
|
|
1149
1197
|
}
|
|
1150
1198
|
}
|
|
1199
|
+
.issues-summary {
|
|
1200
|
+
border-radius: var(--border-radius);
|
|
1201
|
+
padding: 1.5rem;
|
|
1202
|
+
margin-bottom: 2rem;
|
|
1203
|
+
box-shadow: var(--box-shadow);
|
|
1204
|
+
background-color: var(--color-bg-secondary);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.common-issues-list {
|
|
1208
|
+
list-style: none;
|
|
1209
|
+
padding: 0;
|
|
1210
|
+
margin: 0;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.common-issue {
|
|
1214
|
+
display: flex;
|
|
1215
|
+
align-items: flex-start;
|
|
1216
|
+
padding: 0.5rem 0;
|
|
1217
|
+
border-bottom: 1px solid var(--color-border);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
.common-issue:last-child {
|
|
1221
|
+
border-bottom: none;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
.common-issue.error {
|
|
1225
|
+
color: var(--color-error);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.common-issue.warning {
|
|
1229
|
+
color: var(--color-warning);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
.issue-count {
|
|
1233
|
+
font-weight: bold;
|
|
1234
|
+
margin-right: 0.75rem;
|
|
1235
|
+
min-width: 2em;
|
|
1236
|
+
text-align: right;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.error-icon, .warning-icon {
|
|
1240
|
+
margin-right: 0.5rem;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.issue-text {
|
|
1244
|
+
flex: 1;
|
|
1245
|
+
}
|
|
1151
1246
|
</style>
|
|
1152
1247
|
</head>
|
|
1153
1248
|
<body>
|
|
@@ -1196,10 +1291,52 @@ async function generateReports(reports, ctx) {
|
|
|
1196
1291
|
});
|
|
1197
1292
|
}
|
|
1198
1293
|
}
|
|
1199
|
-
async function generateHtmlReport(reports, {
|
|
1294
|
+
async function generateHtmlReport(reports, {
|
|
1295
|
+
storage,
|
|
1296
|
+
storageFilepath,
|
|
1297
|
+
totalRoutes,
|
|
1298
|
+
version
|
|
1299
|
+
}) {
|
|
1200
1300
|
const timestamp = (/* @__PURE__ */ new Date()).toLocaleString();
|
|
1201
1301
|
const totalErrors = reports.reduce((sum, { reports: reports2 }) => sum + reports2.filter((r) => r.error?.length).length, 0);
|
|
1202
1302
|
const totalWarnings = reports.reduce((sum, { reports: reports2 }) => sum + reports2.filter((r) => r.warning?.length).length, 0);
|
|
1303
|
+
const issueFrequency = {};
|
|
1304
|
+
reports.forEach(({ reports: routeReports }) => {
|
|
1305
|
+
routeReports.forEach((report) => {
|
|
1306
|
+
report.error?.forEach((err) => {
|
|
1307
|
+
const key = `${err.name}: ${err.message}`;
|
|
1308
|
+
if (!issueFrequency[key]) {
|
|
1309
|
+
issueFrequency[key] = { count: 0, type: "error" };
|
|
1310
|
+
}
|
|
1311
|
+
issueFrequency[key].count++;
|
|
1312
|
+
});
|
|
1313
|
+
report.warning?.forEach((warning) => {
|
|
1314
|
+
const key = `${warning.name}: ${warning.message}`;
|
|
1315
|
+
if (!issueFrequency[key]) {
|
|
1316
|
+
issueFrequency[key] = { count: 0, type: "warning" };
|
|
1317
|
+
}
|
|
1318
|
+
issueFrequency[key].count++;
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
});
|
|
1322
|
+
const issuesList = Object.entries(issueFrequency).sort((a, b) => b[1].count - a[1].count).map(([issue, { count, type }]) => {
|
|
1323
|
+
const iconClass = type === "error" ? "error-icon" : "warning-icon";
|
|
1324
|
+
const icon = type === "error" ? "\u2716" : "\u26A0";
|
|
1325
|
+
return `
|
|
1326
|
+
<li class="common-issue ${type}">
|
|
1327
|
+
<span class="${iconClass}" aria-hidden="true">${icon}</span>
|
|
1328
|
+
<span class="issue-count">${count}</span>
|
|
1329
|
+
<span class="issue-text">${issue}</span>
|
|
1330
|
+
</li>
|
|
1331
|
+
`;
|
|
1332
|
+
}).join("");
|
|
1333
|
+
const issueSummary = issuesList ? `
|
|
1334
|
+
<div class="issues-summary">
|
|
1335
|
+
<ul class="common-issues-list">
|
|
1336
|
+
${issuesList}
|
|
1337
|
+
</ul>
|
|
1338
|
+
</div>
|
|
1339
|
+
` : "";
|
|
1203
1340
|
const reportMeta = `
|
|
1204
1341
|
<div class="report-meta">
|
|
1205
1342
|
<div class="version">Nuxt Link Checker v${version}</div>
|
|
@@ -1238,8 +1375,9 @@ async function generateHtmlReport(reports, { storage, storageFilepath, totalRout
|
|
|
1238
1375
|
warnings > 0 ? `${warnings} warning${warnings > 1 ? "s" : ""}` : ""
|
|
1239
1376
|
].filter(Boolean).join(", ");
|
|
1240
1377
|
return `<li class="${statusClass}">
|
|
1241
|
-
<a href="#route-${createAnchor(route)}">${statusEmoji} ${route}
|
|
1378
|
+
<a style="display: block;" href="#route-${createAnchor(route)}">${statusEmoji} ${route}
|
|
1242
1379
|
${statusString ? `<span class="toc-status">(${statusString})</span>` : ""}
|
|
1380
|
+
</a>
|
|
1243
1381
|
</li>`;
|
|
1244
1382
|
}).join("");
|
|
1245
1383
|
const reportHtml = reports.map(({ route, reports: reports2 }) => {
|
|
@@ -1292,8 +1430,8 @@ async function generateHtmlReport(reports, { storage, storageFilepath, totalRout
|
|
|
1292
1430
|
</section>`;
|
|
1293
1431
|
}).join("");
|
|
1294
1432
|
const tableOfContents = `
|
|
1295
|
-
<div id="toc" class="table-of-contents">
|
|
1296
1433
|
<h2>Table of Contents</h2>
|
|
1434
|
+
<div id="toc" class="table-of-contents">
|
|
1297
1435
|
<ul class="toc-list">
|
|
1298
1436
|
${tocHtml || "<li>No issues found</li>"}
|
|
1299
1437
|
</ul>
|
|
@@ -1301,6 +1439,7 @@ async function generateHtmlReport(reports, { storage, storageFilepath, totalRout
|
|
|
1301
1439
|
`;
|
|
1302
1440
|
const html = htmlTemplate.replace("<!-- REPORT -->", `${reportMeta}
|
|
1303
1441
|
${summary}
|
|
1442
|
+
${issueSummary}
|
|
1304
1443
|
${tableOfContents}
|
|
1305
1444
|
${reportHtml || '<div class="no-issues">All links are valid! \u{1F389}</div>'}`).replaceAll("<!-- SiteName -->", `Link Report - ${useSiteConfig()?.name || ""}`);
|
|
1306
1445
|
await storage.setItem("link-checker-report.html", html);
|
|
@@ -1414,6 +1553,32 @@ async function generateJsonReport(reports, { storage, storageFilepath }) {
|
|
|
1414
1553
|
return resolve(storageFilepath, "link-checker-report.json");
|
|
1415
1554
|
}
|
|
1416
1555
|
|
|
1556
|
+
async function runParallel(inputs, cb, opts) {
|
|
1557
|
+
const tasks = /* @__PURE__ */ new Set();
|
|
1558
|
+
function queueNext() {
|
|
1559
|
+
const route = inputs.values().next().value;
|
|
1560
|
+
if (!route) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
inputs.delete(route);
|
|
1564
|
+
const task = (opts.interval ? new Promise((resolve) => setTimeout(resolve, opts.interval)) : Promise.resolve()).then(() => cb(route)).catch((error) => {
|
|
1565
|
+
console.error(error);
|
|
1566
|
+
});
|
|
1567
|
+
tasks.add(task);
|
|
1568
|
+
return task.then(() => {
|
|
1569
|
+
tasks.delete(task);
|
|
1570
|
+
if (inputs.size > 0) {
|
|
1571
|
+
return refillQueue();
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
function refillQueue() {
|
|
1576
|
+
const workers = Math.min(opts.concurrency - tasks.size, inputs.size);
|
|
1577
|
+
return Promise.all(Array.from({ length: workers }, () => queueNext()));
|
|
1578
|
+
}
|
|
1579
|
+
await refillQueue();
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1417
1582
|
const { gray, yellow, dim, red, white } = colors;
|
|
1418
1583
|
const linkMap = {};
|
|
1419
1584
|
async function extractPayload(html, rootNodeId = "#__nuxt") {
|
|
@@ -1495,7 +1660,13 @@ function prerender(config, version, nuxt = useNuxt()) {
|
|
|
1495
1660
|
const route = decodeURI(ctx.route);
|
|
1496
1661
|
if (ctx.contents && !ctx.error && ctx.fileName?.endsWith(".html") && !route.endsWith(".html") && urlFilter(route))
|
|
1497
1662
|
linkMap[route] = await extractPayload(ctx.contents, nuxt.options.app.rootAttrs?.id || "");
|
|
1498
|
-
setLinkResponse(route, Promise.resolve({
|
|
1663
|
+
setLinkResponse(route, Promise.resolve({
|
|
1664
|
+
status: Number(ctx.error?.statusCode) || 200,
|
|
1665
|
+
statusText: ctx.error?.statusMessage || "",
|
|
1666
|
+
headers: {
|
|
1667
|
+
"Content-Type": ctx.contentType
|
|
1668
|
+
}
|
|
1669
|
+
}));
|
|
1499
1670
|
});
|
|
1500
1671
|
nitro.hooks.hook("prerender:done", async () => {
|
|
1501
1672
|
const payloads = Object.entries(linkMap);
|
|
@@ -1514,6 +1685,7 @@ function prerender(config, version, nuxt = useNuxt()) {
|
|
|
1514
1685
|
version,
|
|
1515
1686
|
storage,
|
|
1516
1687
|
storageFilepath,
|
|
1688
|
+
isPrerenderingAllRoutes: isNuxtGenerate(nuxt) || Boolean(nuxt.options.nitro.prerender?.crawlLinks),
|
|
1517
1689
|
totalRoutes: payloads.length
|
|
1518
1690
|
};
|
|
1519
1691
|
const { allReports, errorCount } = await runInspections(payloads, inspectionCtx);
|
|
@@ -1521,6 +1693,9 @@ function prerender(config, version, nuxt = useNuxt()) {
|
|
|
1521
1693
|
({ reports }) => reports.some((r) => r.error?.length || r.warning?.length)
|
|
1522
1694
|
);
|
|
1523
1695
|
await generateReports(reportsWithContent, inspectionCtx);
|
|
1696
|
+
if (config.debug) {
|
|
1697
|
+
await storage.setItem("debug-link-responses.json", JSON.stringify(await getResolvedLinkResponses()));
|
|
1698
|
+
}
|
|
1524
1699
|
if (errorCount > 0 && config.failOnError) {
|
|
1525
1700
|
nitro.logger.error(`Nuxt Link Checker found ${errorCount} errors, failing build.`);
|
|
1526
1701
|
nitro.logger.log(gray('You can disable this by setting "linkChecker: { failOnError: false }" in your nuxt.config.'));
|
|
@@ -1556,11 +1731,11 @@ async function runInspections(payloads, context) {
|
|
|
1556
1731
|
let warningCount = 0;
|
|
1557
1732
|
let routeWithIssuesCount = 0;
|
|
1558
1733
|
const totalRoutes = payloads.length;
|
|
1559
|
-
const batchSize = 10;
|
|
1560
1734
|
const allReports = [];
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1735
|
+
const inputs = new Set(payloads);
|
|
1736
|
+
await runParallel(
|
|
1737
|
+
inputs,
|
|
1738
|
+
async ([route, payload]) => {
|
|
1564
1739
|
const reports = await processRouteLinks(route, payload, context);
|
|
1565
1740
|
const routeErrors = reports.filter((r) => r.error?.length).length;
|
|
1566
1741
|
const routeWarnings = reports.filter((r) => r.warning?.length).length;
|
|
@@ -1572,11 +1747,11 @@ async function runInspections(payloads, context) {
|
|
|
1572
1747
|
logRouteIssues(route, reports, routeErrors, routeWarnings, nitro);
|
|
1573
1748
|
}
|
|
1574
1749
|
}
|
|
1575
|
-
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1750
|
+
allReports.push({ route, reports });
|
|
1751
|
+
},
|
|
1752
|
+
{ concurrency: 5, interval: 10 }
|
|
1753
|
+
// Process 5 routes in parallel with small delay between starts
|
|
1754
|
+
);
|
|
1580
1755
|
logSummary(
|
|
1581
1756
|
totalRoutes,
|
|
1582
1757
|
routeWithIssuesCount,
|
|
@@ -1589,13 +1764,15 @@ async function runInspections(payloads, context) {
|
|
|
1589
1764
|
async function processRouteLinks(route, payload, context) {
|
|
1590
1765
|
const { urlFilter, config, nuxt, siteConfig, pageSearcher } = context;
|
|
1591
1766
|
const links = payload.links || [];
|
|
1592
|
-
const linkBatchSize = 10;
|
|
1593
1767
|
const allReports = [];
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1768
|
+
const inputs = new Set(links);
|
|
1769
|
+
await runParallel(
|
|
1770
|
+
inputs,
|
|
1771
|
+
async ({ link, textContent }) => {
|
|
1772
|
+
if (!urlFilter(link) || !link) {
|
|
1773
|
+
allReports.push({ error: [], warning: [], link });
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1599
1776
|
const response = await getLinkResponse({
|
|
1600
1777
|
link,
|
|
1601
1778
|
timeout: config.fetchTimeout,
|
|
@@ -1604,7 +1781,7 @@ async function processRouteLinks(route, payload, context) {
|
|
|
1604
1781
|
return existsSync(resolve(nuxt.options.rootDir, nuxt.options.dir.public, withoutLeadingSlash(link)));
|
|
1605
1782
|
}
|
|
1606
1783
|
});
|
|
1607
|
-
|
|
1784
|
+
const report = inspect({
|
|
1608
1785
|
ids: linkMap[route].ids,
|
|
1609
1786
|
fromPath: route,
|
|
1610
1787
|
pageSearch: pageSearcher,
|
|
@@ -1614,13 +1791,11 @@ async function processRouteLinks(route, payload, context) {
|
|
|
1614
1791
|
response,
|
|
1615
1792
|
skipInspections: config.skipInspections
|
|
1616
1793
|
});
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1794
|
+
allReports.push(report);
|
|
1795
|
+
},
|
|
1796
|
+
{ concurrency: 5, interval: 5 }
|
|
1797
|
+
// Process 5 links in parallel with small delay
|
|
1798
|
+
);
|
|
1624
1799
|
return allReports;
|
|
1625
1800
|
}
|
|
1626
1801
|
function logSummary(totalRoutes, routesWithIssues, errorCount, warningCount, nitro) {
|
|
@@ -1711,18 +1886,19 @@ const module = defineNuxtModule({
|
|
|
1711
1886
|
},
|
|
1712
1887
|
configKey: "linkChecker"
|
|
1713
1888
|
},
|
|
1714
|
-
defaults
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1889
|
+
defaults(nuxt) {
|
|
1890
|
+
return {
|
|
1891
|
+
strictNuxtContentPaths: false,
|
|
1892
|
+
fetchRemoteUrls: nuxt.options._build && provider !== "stackblitz",
|
|
1893
|
+
runOnBuild: true,
|
|
1894
|
+
debug: false,
|
|
1895
|
+
showLiveInspections: false,
|
|
1896
|
+
enabled: true,
|
|
1897
|
+
fetchTimeout: 1e4,
|
|
1898
|
+
failOnError: false,
|
|
1899
|
+
excludeLinks: [],
|
|
1900
|
+
skipInspections: []
|
|
1901
|
+
};
|
|
1726
1902
|
},
|
|
1727
1903
|
async setup(config, nuxt) {
|
|
1728
1904
|
const { resolve } = createResolver(import.meta.url);
|
|
@@ -1735,7 +1911,8 @@ const module = defineNuxtModule({
|
|
|
1735
1911
|
}
|
|
1736
1912
|
await installNuxtSiteConfig();
|
|
1737
1913
|
if (config.fetchRemoteUrls) {
|
|
1738
|
-
|
|
1914
|
+
const { status } = await crawlFetch("https://nuxtseo.com/robots.txt").catch(() => ({ status: 404 }));
|
|
1915
|
+
config.fetchRemoteUrls = status < 400;
|
|
1739
1916
|
if (!config.fetchRemoteUrls)
|
|
1740
1917
|
logger.warn("Remote URL fetching is disabled because you appear to be offline.");
|
|
1741
1918
|
}
|
|
@@ -1796,10 +1973,6 @@ const module = defineNuxtModule({
|
|
|
1796
1973
|
setupDevToolsUI(config, resolve);
|
|
1797
1974
|
}
|
|
1798
1975
|
if (config.runOnBuild) {
|
|
1799
|
-
const isRenderingAllRoutes = isNuxtGenerate(nuxt) && !nuxt.options.nitro.prerender?.crawlLinks;
|
|
1800
|
-
if (!nuxt.options._prepare && !nuxt.options.dev && nuxt.options.build && !isRenderingAllRoutes) {
|
|
1801
|
-
config.skipInspections.push("no-error-response");
|
|
1802
|
-
}
|
|
1803
1976
|
prerender(config, version);
|
|
1804
1977
|
}
|
|
1805
1978
|
}
|
|
@@ -1,24 +1,28 @@
|
|
|
1
|
+
interface LinkResponse {
|
|
2
|
+
status: number;
|
|
3
|
+
statusText: string;
|
|
4
|
+
headers: Record<string, any>;
|
|
5
|
+
}
|
|
1
6
|
export declare function getLinkResponse({ link, timeout, fetchRemoteUrls, baseURL, isInStorage }: {
|
|
2
7
|
link: string;
|
|
3
8
|
baseURL?: string;
|
|
4
9
|
timeout?: number;
|
|
5
10
|
fetchRemoteUrls?: boolean;
|
|
6
11
|
isInStorage: () => boolean;
|
|
7
|
-
}): Promise<
|
|
8
|
-
status: number;
|
|
9
|
-
statusText: string;
|
|
10
|
-
headers: Record<string, any>;
|
|
11
|
-
}>;
|
|
12
|
+
}): Promise<LinkResponse | null>;
|
|
12
13
|
export declare function setLinkResponse(link: string, response: Promise<{
|
|
13
14
|
status: number;
|
|
14
15
|
statusText: string;
|
|
15
16
|
headers: Record<string, any>;
|
|
16
17
|
}>): void;
|
|
18
|
+
export declare function getResolvedLinkResponses(): Promise<Record<string, LinkResponse>>;
|
|
17
19
|
export declare function crawlFetch(link: string, options?: {
|
|
18
20
|
timeout?: number;
|
|
19
21
|
baseURL?: string;
|
|
20
22
|
}): Promise<{
|
|
21
23
|
status: number;
|
|
22
24
|
statusText: string;
|
|
23
|
-
headers:
|
|
25
|
+
headers: Record<string, string>;
|
|
26
|
+
time: number;
|
|
24
27
|
}>;
|
|
28
|
+
export {};
|
|
@@ -1,31 +1,48 @@
|
|
|
1
|
-
import { $fetch } from "ofetch";
|
|
2
1
|
import { isNonFetchableLink } from "./inspections/util.js";
|
|
3
2
|
const responses = {};
|
|
3
|
+
const MockSuccessResponse = Promise.resolve({ status: 200, statusText: "OK", headers: {} });
|
|
4
4
|
export async function getLinkResponse({ link, timeout, fetchRemoteUrls, baseURL, isInStorage }) {
|
|
5
5
|
if (link.includes("#") && !link.startsWith("#"))
|
|
6
6
|
link = link.split("#")[0];
|
|
7
7
|
link = decodeURI(link);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
if (link in responses) {
|
|
9
|
+
return responses[link];
|
|
10
|
+
}
|
|
11
|
+
if (isNonFetchableLink(link)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (isInStorage()) {
|
|
15
|
+
responses[link] = Promise.resolve({ status: 200, statusText: "OK", headers: { "X-Nuxt-Prerendered": true } });
|
|
16
|
+
return responses[link];
|
|
17
|
+
}
|
|
18
|
+
if (link.startsWith("http") || link.startsWith("//")) {
|
|
19
|
+
responses[link] = fetchRemoteUrls ? crawlFetch(link, { timeout, baseURL }) : MockSuccessResponse;
|
|
20
|
+
return responses[link];
|
|
15
21
|
}
|
|
22
|
+
responses[link] = crawlFetch(link, { timeout, baseURL });
|
|
16
23
|
return responses[link];
|
|
17
24
|
}
|
|
18
25
|
export function setLinkResponse(link, response) {
|
|
19
26
|
responses[link] = response;
|
|
20
27
|
}
|
|
28
|
+
export async function getResolvedLinkResponses() {
|
|
29
|
+
const data = {};
|
|
30
|
+
for (const link in responses) {
|
|
31
|
+
data[link] = await responses[link];
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
21
35
|
export async function crawlFetch(link, options = {}) {
|
|
22
36
|
const timeout = options.timeout || 5e3;
|
|
23
37
|
const timeoutController = new AbortController();
|
|
24
38
|
const abortRequestTimeout = setTimeout(() => timeoutController.abort(), timeout);
|
|
25
|
-
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
return await globalThis.$fetch.raw(encodeURI(link), {
|
|
26
41
|
baseURL: options.baseURL,
|
|
27
42
|
method: "HEAD",
|
|
28
43
|
signal: timeoutController.signal,
|
|
44
|
+
retry: 3,
|
|
45
|
+
retryDelay: 250,
|
|
29
46
|
headers: {
|
|
30
47
|
"user-agent": "Nuxt Link Checker"
|
|
31
48
|
}
|
|
@@ -33,5 +50,15 @@ export async function crawlFetch(link, options = {}) {
|
|
|
33
50
|
if (error.name === "AbortError")
|
|
34
51
|
return { status: 408, statusText: "Request Timeout", headers: {} };
|
|
35
52
|
return { status: 404, statusText: "Not Found", headers: {} };
|
|
36
|
-
}).finally(() => clearTimeout(abortRequestTimeout)).then((res) =>
|
|
53
|
+
}).finally(() => clearTimeout(abortRequestTimeout)).then((res) => {
|
|
54
|
+
let headersObj = {};
|
|
55
|
+
if (res.headers) {
|
|
56
|
+
if (typeof res.headers.entries === "function") {
|
|
57
|
+
headersObj = Object.fromEntries(Array.from(res.headers.entries()));
|
|
58
|
+
} else if (typeof res.headers === "object") {
|
|
59
|
+
headersObj = { ...res.headers };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { status: res.status, statusText: res.statusText, headers: headersObj, time: Date.now() - start };
|
|
63
|
+
});
|
|
37
64
|
}
|
|
@@ -4,7 +4,7 @@ export default function RuleNoErrorResponse() {
|
|
|
4
4
|
id: "no-error-response",
|
|
5
5
|
externalLinks: true,
|
|
6
6
|
test({ link, response, report, pageSearch }) {
|
|
7
|
-
if (!response
|
|
7
|
+
if (!response?.status || response.status.toString().startsWith("2") || response.status.toString().startsWith("3") || isNonFetchableLink(link))
|
|
8
8
|
return;
|
|
9
9
|
const payload = {
|
|
10
10
|
name: "no-error-response",
|
|
@@ -2,5 +2,6 @@ export function defineRule(rule) {
|
|
|
2
2
|
return rule;
|
|
3
3
|
}
|
|
4
4
|
export function isNonFetchableLink(link) {
|
|
5
|
-
|
|
5
|
+
const trimmedLink = link.trim().toLowerCase();
|
|
6
|
+
return trimmedLink.startsWith("javascript:") || trimmedLink.startsWith("blob:") || trimmedLink.startsWith("data:") || trimmedLink.startsWith("mailto:") || trimmedLink.startsWith("tel:") || trimmedLink.startsWith("vbscript:") || trimmedLink.startsWith("#");
|
|
6
7
|
}
|
|
@@ -3,7 +3,7 @@ export default function RuleRedirects() {
|
|
|
3
3
|
return defineRule({
|
|
4
4
|
id: "redirects",
|
|
5
5
|
test({ report, response }) {
|
|
6
|
-
if (response
|
|
6
|
+
if (response?.status !== 301 && response?.status !== 302)
|
|
7
7
|
return;
|
|
8
8
|
const payload = {
|
|
9
9
|
name: "redirects",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-link-checker",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.3.0",
|
|
5
5
|
"description": "Find and magically fix links that may be negatively effecting your Nuxt sites SEO.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"@nuxt/devtools-kit": "^2.3.0",
|
|
43
43
|
"@nuxt/kit": "^3.16.0",
|
|
44
44
|
"@vueuse/core": "^13.0.0",
|
|
45
|
-
"consola": "^3.4.
|
|
45
|
+
"consola": "^3.4.2",
|
|
46
46
|
"diff": "^7.0.0",
|
|
47
47
|
"fuse.js": "^7.1.0",
|
|
48
48
|
"magic-string": "^0.30.17",
|
|
49
|
-
"nuxt-site-config": "^3.1.
|
|
49
|
+
"nuxt-site-config": "^3.1.6",
|
|
50
50
|
"pathe": "^2.0.3",
|
|
51
51
|
"pkg-types": "^2.1.0",
|
|
52
52
|
"radix3": "^1.1.2",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"execa": "^9.5.2",
|
|
69
69
|
"nuxt": "^3.16.0",
|
|
70
70
|
"typescript": "5.8.2",
|
|
71
|
-
"vitest": "^3.0.
|
|
71
|
+
"vitest": "^3.0.9"
|
|
72
72
|
},
|
|
73
73
|
"resolutions": {
|
|
74
74
|
"nuxt-link-checker": "workspace:*",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"id":"cdbc357b-6180-4103-97aa-19c53e0b9c3c","timestamp":1741973552376,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}
|