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.
@@ -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},1741973557583,false]</script>
16
- <script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-link-checker",buildId:"cdbc357b-6180-4103-97aa-19c53e0b9c3c",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
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>
@@ -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},1741973557584,false]</script>
16
- <script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-link-checker",buildId:"cdbc357b-6180-4103-97aa-19c53e0b9c3c",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
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":"cdbc357b-6180-4103-97aa-19c53e0b9c3c","timestamp":1741973552376}
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":[]}
@@ -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},1741973557584,false]</script>
16
- <script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/__nuxt-link-checker",buildId:"cdbc357b-6180-4103-97aa-19c53e0b9c3c",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script></body></html>
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
@@ -5,7 +5,7 @@
5
5
  "bridge": false
6
6
  },
7
7
  "configKey": "linkChecker",
8
- "version": "4.1.1",
8
+ "version": "4.2.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "0.8.4",
11
11
  "unbuild": "2.0.0"
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, { storage, storageFilepath, totalRoutes, version }) {
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}</a>
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({ status: Number(ctx.error?.statusCode) || 200, statusText: ctx.error?.statusMessage || "", headers: {} }));
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
- for (let i = 0; i < payloads.length; i += batchSize) {
1562
- const batch = payloads.slice(i, i + batchSize);
1563
- const batchReports = await Promise.all(batch.map(async ([route, payload]) => {
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
- return { route, reports };
1576
- }));
1577
- allReports.push(...batchReports);
1578
- await new Promise((resolve2) => setTimeout(resolve2, 0));
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
- for (let i = 0; i < links.length; i += linkBatchSize) {
1595
- const linkBatch = links.slice(i, i + linkBatchSize);
1596
- let batchReports = await Promise.all(linkBatch.map(async ({ link, textContent }) => {
1597
- if (!urlFilter(link) || !link)
1598
- return { error: [], warning: [], link };
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
- return inspect({
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
- allReports.push(...batchReports);
1619
- batchReports = null;
1620
- if (links.length > linkBatchSize) {
1621
- await new Promise((resolve2) => setTimeout(resolve2, 10));
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
- strictNuxtContentPaths: false,
1716
- fetchRemoteUrls: false,
1717
- // provider !== 'stackblitz',
1718
- runOnBuild: true,
1719
- debug: false,
1720
- showLiveInspections: false,
1721
- enabled: true,
1722
- fetchTimeout: 1e4,
1723
- failOnError: false,
1724
- excludeLinks: [],
1725
- skipInspections: []
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
- config.fetchRemoteUrls = (await crawlFetch("https://google.com")).status === 200;
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: 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
- const response = responses[link];
9
- if (!response) {
10
- if (isNonFetchableLink(link) || link.startsWith("http") && !fetchRemoteUrls || isInStorage()) {
11
- responses[link] = Promise.resolve({ status: 200, statusText: "OK", headers: {} });
12
- } else {
13
- responses[link] = crawlFetch(link, { timeout, baseURL });
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
- return await $fetch.raw(encodeURI(link), {
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) => ({ status: res.status, statusText: res.statusText, headers: res.headers }));
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.status || response.status.toString().startsWith("2") || response.status.toString().startsWith("3") || isNonFetchableLink(link))
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
- return link.startsWith("javascript:") || link.startsWith("blob:") || link.startsWith("data:") || link.startsWith("mailto:") || link.startsWith("tel:") || link.startsWith("#");
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.status !== 301 && response.status !== 302)
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.2.0",
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.0",
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.5",
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.8"
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":[]}