@goflag/cli 0.2.7 → 0.2.8
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 +26 -3
- package/dist/{chunk-UOKYUKHQ.js → chunk-SKX7UCGC.js} +169 -28
- package/dist/chunk-SKX7UCGC.js.map +1 -0
- package/dist/cli.js +542 -233
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +34 -0
- package/dist/index.js +1 -1
- package/flags.json +302 -0
- package/package.json +5 -2
- package/dist/chunk-UOKYUKHQ.js.map +0 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ on the site. A human cannot check those on 400 pages. That is the whole job.
|
|
|
16
16
|
## What it checks
|
|
17
17
|
|
|
18
18
|
- **Broken links** — scrapes every crawled page, dedupes targets globally (a footer link on 500 pages is probed once), and checks each with `HEAD`→`GET` fallback, redirect-chain/loop detection, soft-404 and anti-bot (403/429) triage. Broken links are mapped back to the pages that reference them.
|
|
19
|
-
- **Missing translation pages** — builds a `route × locale` matrix and flags every route that exists in one locale but is missing in another, plus hreflang reciprocity gaps (`A → B` with no `B → A`), missing `x-default`, and locale tags naming a language, script or region that does not exist. Rows are keyed by pathname, unless
|
|
19
|
+
- **Missing translation pages** — builds a `route × locale` matrix and flags every route that exists in one locale but is missing in another, plus hreflang reciprocity gaps (`A → B` with no `B → A`), missing `x-default`, and locale tags naming a language, script or region that does not exist. Rows are keyed by pathname, unless the site declares its translation clusters — either with `xhtml:link` in the sitemap, or with reciprocal `hreflang` between two crawled pages' `<head>`. Then those declarations decide which URLs are one page, whatever their slugs, and the run reports `diagnostics.declaredClusters`. The sitemap source survives `--coverage structural`, where the two locales of a slug-translating family are rarely both sampled; the `<head>` source needs both pages in hand and is what fixes the common site that declares properly there and nothing in its sitemap.
|
|
20
20
|
- **robots.txt policy** — flags a site that forbids crawling while its pages ask to be indexed. The two declarations cannot both hold, and robots.txt wins, so the meta tag is never even read.
|
|
21
21
|
- **SEO metadata** — lints each page's `<head>` for the handful of mistakes that actually hurt in search and social and are invisible in a browser: missing/oversized `<title>` and description, missing/relative canonical, missing `og:title`/`og:description`/`og:image`, missing viewport, and contradictory `robots`/`googlebot`/`X-Robots-Tag` directives.
|
|
22
22
|
- **Unreachable pages** — a page the crawl reached that answered non-2xx, or that never answered at all (timeout, reset, DNS failure — recorded as `status: 0`), is reported in `unreachablePages`. goflag asks a second time, from the back of the queue, before calling a page unreachable. Each one is an `error`-severity finding: it turns the verdict red, counts toward `--max-debt`, and fails the build even under `--fail-on error`. A page that silently dropped out of the audited set is what poisons a baseline, so it is reported rather than warned about.
|
|
@@ -146,6 +146,18 @@ authority nobody assigned. Write your consumer against that. The same document
|
|
|
146
146
|
ships inside the package as `rules.json`, if you would rather read it than run
|
|
147
147
|
anything.
|
|
148
148
|
|
|
149
|
+
The flag list below is data too:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
npx @goflag/cli flags > flags.json
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Same idea, one step further: `flags` prints the very table `goflag --help` is
|
|
156
|
+
rendered from and the argument parser dispatches on, so a flag cannot exist in
|
|
157
|
+
one and not the other. Each entry carries its long and short forms, its argument
|
|
158
|
+
placeholder, its default, the group it belongs to and any flag it requires. It
|
|
159
|
+
ships inside the package as `flags.json` as well.
|
|
160
|
+
|
|
149
161
|
It deliberately omits the message a finding prints — that is built at audit time
|
|
150
162
|
from what the page actually says, so a static copy would be a sample rather than
|
|
151
163
|
a fact, and a sample presented as the truth is how this project's own
|
|
@@ -424,6 +436,17 @@ goflag https://example.com --ignore-holes /legal --ignore-holes "/blog/**"
|
|
|
424
436
|
Suppressed gaps are counted in `diagnostics.ignoredHoles`, so a quiet report
|
|
425
437
|
never means "nothing was wrong" by accident.
|
|
426
438
|
|
|
439
|
+
The opposite failure is quieter still. A cell in the matrix can be filled by an
|
|
440
|
+
`hreflang` on another page and by nothing else — not crawled, not listed in your
|
|
441
|
+
sitemap — and a filled cell reads as a translation that exists. A page
|
|
442
|
+
advertising a translation the site does not serve therefore plugs the very gap
|
|
443
|
+
it should reveal. Those cells are counted in
|
|
444
|
+
`diagnostics.unverifiedAlternates`, and goflag deliberately does **not** act on
|
|
445
|
+
them: refusing to believe an alternate the sitemap omits would invent holes on
|
|
446
|
+
every site using `@goflag/next`'s `sitemap: false` on purpose. Read that number
|
|
447
|
+
next to `missingTranslations` — a `0` beside a non-zero count is the one place
|
|
448
|
+
this report can be quietly wrong.
|
|
449
|
+
|
|
427
450
|
## In CI
|
|
428
451
|
|
|
429
452
|
Two moments are worth auditing, and they answer different questions.
|
|
@@ -455,7 +478,7 @@ Three rules, whichever you use:
|
|
|
455
478
|
stages: [build, deploy, audit]
|
|
456
479
|
|
|
457
480
|
variables:
|
|
458
|
-
GOFLAG_VERSION: "0.2.
|
|
481
|
+
GOFLAG_VERSION: "0.2.8"
|
|
459
482
|
|
|
460
483
|
# Before the merge: build the branch, let goflag boot it, audit that.
|
|
461
484
|
seo:mr:
|
|
@@ -507,7 +530,7 @@ jobs:
|
|
|
507
530
|
- run: corepack enable && pnpm install --frozen-lockfile
|
|
508
531
|
- run: pnpm build
|
|
509
532
|
- run: |
|
|
510
|
-
npx --yes @goflag/cli@0.2.
|
|
533
|
+
npx --yes @goflag/cli@0.2.8 http://localhost:3000 \
|
|
511
534
|
--start "pnpm start" --static --no-external \
|
|
512
535
|
--baseline .goflag/baseline.json --regressions-only --max-debt 41 \
|
|
513
536
|
--report goflag-report.json
|
|
@@ -1856,13 +1856,18 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1856
1856
|
const grid = /* @__PURE__ */ new Map();
|
|
1857
1857
|
const locales = /* @__PURE__ */ new Set();
|
|
1858
1858
|
const routes = /* @__PURE__ */ new Set();
|
|
1859
|
+
const declaredSet = new Set(options.declaredUrls ?? []);
|
|
1859
1860
|
for (const locale of options.locales ?? []) {
|
|
1860
1861
|
const tag = locale.trim().toLowerCase();
|
|
1861
1862
|
if (tag) locales.add(tag);
|
|
1862
1863
|
}
|
|
1863
|
-
function
|
|
1864
|
+
function rowOf2(url, pathnameRoute) {
|
|
1864
1865
|
return options.clusterRouteOf?.(url) ?? pathnameRoute;
|
|
1865
1866
|
}
|
|
1867
|
+
function sourceOf(url) {
|
|
1868
|
+
if (inspectedByUrl.has(url)) return "crawled";
|
|
1869
|
+
return declaredSet.has(url) ? "sitemap" : "alternate";
|
|
1870
|
+
}
|
|
1866
1871
|
function record(route, locale, url) {
|
|
1867
1872
|
routes.add(route);
|
|
1868
1873
|
locales.add(locale);
|
|
@@ -1872,13 +1877,13 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1872
1877
|
grid.set(route, row);
|
|
1873
1878
|
}
|
|
1874
1879
|
if (!row.has(locale)) {
|
|
1875
|
-
row.set(locale, { url, inspected: inspectedByUrl.has(url) });
|
|
1880
|
+
row.set(locale, { url, inspected: inspectedByUrl.has(url), source: sourceOf(url) });
|
|
1876
1881
|
}
|
|
1877
1882
|
}
|
|
1878
1883
|
for (const page of pages) {
|
|
1879
1884
|
const url = new URL(page.fetch.finalUrl);
|
|
1880
1885
|
const { route: selfRoute, locale: selfLocale } = splitRoute(url.pathname);
|
|
1881
|
-
record(
|
|
1886
|
+
record(rowOf2(page.fetch.finalUrl, selfRoute), selfLocale, page.fetch.finalUrl);
|
|
1882
1887
|
for (const alt of page.links.alternates) {
|
|
1883
1888
|
let altUrl;
|
|
1884
1889
|
try {
|
|
@@ -1888,7 +1893,7 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1888
1893
|
}
|
|
1889
1894
|
const { route } = splitRoute(altUrl.pathname);
|
|
1890
1895
|
const locale = alt.isXDefault ? "x-default" : localeIdentity(alt.hreflang);
|
|
1891
|
-
record(
|
|
1896
|
+
record(rowOf2(altUrl.toString(), route || selfRoute), locale, altUrl.toString());
|
|
1892
1897
|
}
|
|
1893
1898
|
}
|
|
1894
1899
|
for (const declared of options.declaredUrls ?? []) {
|
|
@@ -1899,7 +1904,7 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1899
1904
|
continue;
|
|
1900
1905
|
}
|
|
1901
1906
|
const { route, locale } = splitRoute(url.pathname);
|
|
1902
|
-
record(
|
|
1907
|
+
record(rowOf2(url.toString(), route), locale, url.toString());
|
|
1903
1908
|
}
|
|
1904
1909
|
const sortedLocales = sortLocales([...locales]);
|
|
1905
1910
|
const sortedRoutes = [...routes].sort((a, b) => a.localeCompare(b));
|
|
@@ -1908,11 +1913,20 @@ function buildI18nMatrix(pages, options = {}) {
|
|
|
1908
1913
|
cells[route] = {};
|
|
1909
1914
|
for (const locale of sortedLocales) {
|
|
1910
1915
|
const slot = grid.get(route)?.get(locale);
|
|
1911
|
-
cells[route][locale] = slot ? { url: slot.url, inspected: slot.inspected } : { url: null, inspected: false };
|
|
1916
|
+
cells[route][locale] = slot ? { url: slot.url, inspected: slot.inspected, source: slot.source } : { url: null, inspected: false };
|
|
1912
1917
|
}
|
|
1913
1918
|
}
|
|
1914
1919
|
return { locales: sortedLocales, routes: sortedRoutes, cells };
|
|
1915
1920
|
}
|
|
1921
|
+
function countUnverifiedAlternates(matrix) {
|
|
1922
|
+
let count2 = 0;
|
|
1923
|
+
for (const route of matrix.routes) {
|
|
1924
|
+
for (const locale of matrix.locales) {
|
|
1925
|
+
if (matrix.cells[route]?.[locale]?.source === "alternate") count2++;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
return count2;
|
|
1929
|
+
}
|
|
1916
1930
|
function reciprocityIssues(pages) {
|
|
1917
1931
|
const issues = [];
|
|
1918
1932
|
const byUrl = /* @__PURE__ */ new Map();
|
|
@@ -2013,6 +2027,9 @@ function declaredLocales(page) {
|
|
|
2013
2027
|
}
|
|
2014
2028
|
return out;
|
|
2015
2029
|
}
|
|
2030
|
+
function rowOf(site, url, pathname) {
|
|
2031
|
+
return site.clusterRouteOf?.(url) ?? splitRoute(pathname).route;
|
|
2032
|
+
}
|
|
2016
2033
|
function sitemapLocalesByRoute(site) {
|
|
2017
2034
|
const byRoute = /* @__PURE__ */ new Map();
|
|
2018
2035
|
for (const entry of site.discovery?.urls ?? []) {
|
|
@@ -2022,8 +2039,9 @@ function sitemapLocalesByRoute(site) {
|
|
|
2022
2039
|
} catch {
|
|
2023
2040
|
continue;
|
|
2024
2041
|
}
|
|
2025
|
-
const {
|
|
2042
|
+
const { locale } = splitRoute(pathname);
|
|
2026
2043
|
if (locale === "x-default") continue;
|
|
2044
|
+
const route = rowOf(site, entry.loc, pathname);
|
|
2027
2045
|
const set = byRoute.get(route) ?? /* @__PURE__ */ new Set();
|
|
2028
2046
|
set.add(locale.toLowerCase());
|
|
2029
2047
|
byRoute.set(route, set);
|
|
@@ -2087,7 +2105,7 @@ var hreflangSitemapMismatch = {
|
|
|
2087
2105
|
} catch {
|
|
2088
2106
|
continue;
|
|
2089
2107
|
}
|
|
2090
|
-
const
|
|
2108
|
+
const route = rowOf(site, page.fetch.finalUrl, pathname);
|
|
2091
2109
|
const inSitemap = bySitemap.get(route);
|
|
2092
2110
|
if (!inSitemap || inSitemap.size === 0) continue;
|
|
2093
2111
|
const onlyInSitemap = sorted(new Set([...inSitemap].filter((l) => !head.has(l))));
|
|
@@ -2588,39 +2606,140 @@ function normalise(raw) {
|
|
|
2588
2606
|
return `${url.origin}${path}${url.search}`;
|
|
2589
2607
|
}
|
|
2590
2608
|
var X_DEFAULT = "x-default";
|
|
2609
|
+
function clusterCount(rows) {
|
|
2610
|
+
return new Set(rows.values()).size;
|
|
2611
|
+
}
|
|
2612
|
+
function indexOf(rows, conflicts, refused) {
|
|
2613
|
+
return {
|
|
2614
|
+
rows,
|
|
2615
|
+
routeOf: (url) => {
|
|
2616
|
+
const key = normalise(url);
|
|
2617
|
+
return key === null ? void 0 : rows.get(key);
|
|
2618
|
+
},
|
|
2619
|
+
size: clusterCount(rows),
|
|
2620
|
+
conflicts,
|
|
2621
|
+
refused
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2591
2624
|
function buildClusterIndex(entries) {
|
|
2592
|
-
const
|
|
2625
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2593
2626
|
const conflicts = [];
|
|
2594
|
-
|
|
2627
|
+
let refused = 0;
|
|
2595
2628
|
for (const entry of entries) {
|
|
2596
2629
|
const alternates = entry.alternates;
|
|
2597
2630
|
if (!alternates || alternates.length === 0) continue;
|
|
2598
2631
|
const xDefault = alternates.find((a) => a.hreflang.trim().toLowerCase() === X_DEFAULT);
|
|
2599
|
-
if (!xDefault)
|
|
2632
|
+
if (!xDefault) {
|
|
2633
|
+
refused++;
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2600
2636
|
const anchorUrl = normalise(xDefault.href);
|
|
2601
|
-
if (!anchorUrl)
|
|
2637
|
+
if (!anchorUrl) {
|
|
2638
|
+
refused++;
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2602
2641
|
const { route: anchorRoute } = splitRoute(new URL(anchorUrl).pathname);
|
|
2603
|
-
anchors.add(anchorRoute);
|
|
2604
2642
|
const members = [entry.loc, ...alternates.map((a) => a.href)];
|
|
2605
2643
|
for (const member of members) {
|
|
2606
2644
|
const url = normalise(member);
|
|
2607
2645
|
if (!url) continue;
|
|
2608
|
-
const existing =
|
|
2646
|
+
const existing = rows.get(url);
|
|
2609
2647
|
if (existing === void 0) {
|
|
2610
|
-
|
|
2648
|
+
rows.set(url, anchorRoute);
|
|
2611
2649
|
continue;
|
|
2612
2650
|
}
|
|
2613
2651
|
if (existing !== anchorRoute && !conflicts.includes(url)) conflicts.push(url);
|
|
2614
2652
|
}
|
|
2615
2653
|
}
|
|
2616
|
-
return
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2654
|
+
return indexOf(rows, conflicts, refused);
|
|
2655
|
+
}
|
|
2656
|
+
function readHeads(pages) {
|
|
2657
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2658
|
+
for (const page of pages) {
|
|
2659
|
+
const self = normalise(page.fetch.finalUrl);
|
|
2660
|
+
if (self === null || seen.has(self)) continue;
|
|
2661
|
+
const peers = /* @__PURE__ */ new Set();
|
|
2662
|
+
const xDefaults = /* @__PURE__ */ new Set();
|
|
2663
|
+
for (const alt of page.links.alternates) {
|
|
2664
|
+
const href = normalise(alt.href);
|
|
2665
|
+
if (href === null) continue;
|
|
2666
|
+
if (alt.isXDefault) xDefaults.add(href);
|
|
2667
|
+
else if (href !== self) peers.add(href);
|
|
2668
|
+
}
|
|
2669
|
+
seen.set(self, { peers, xDefault: xDefaults.size === 1 ? [...xDefaults][0] : null });
|
|
2670
|
+
}
|
|
2671
|
+
return seen;
|
|
2672
|
+
}
|
|
2673
|
+
function buildHeadClusterIndex(pages) {
|
|
2674
|
+
const seen = readHeads(pages);
|
|
2675
|
+
const parent = /* @__PURE__ */ new Map();
|
|
2676
|
+
for (const url of seen.keys()) parent.set(url, url);
|
|
2677
|
+
const find = (start) => {
|
|
2678
|
+
let root = start;
|
|
2679
|
+
while (parent.get(root) !== root) root = parent.get(root);
|
|
2680
|
+
let cursor = start;
|
|
2681
|
+
while (parent.get(cursor) !== root) {
|
|
2682
|
+
const next = parent.get(cursor);
|
|
2683
|
+
parent.set(cursor, root);
|
|
2684
|
+
cursor = next;
|
|
2685
|
+
}
|
|
2686
|
+
return root;
|
|
2687
|
+
};
|
|
2688
|
+
for (const [url, decl] of seen) {
|
|
2689
|
+
for (const peer of decl.peers) {
|
|
2690
|
+
if (!seen.get(peer)?.peers.has(url)) continue;
|
|
2691
|
+
const [a, b] = [find(url), find(peer)];
|
|
2692
|
+
if (a !== b) parent.set(a, b);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
const components = /* @__PURE__ */ new Map();
|
|
2696
|
+
for (const url of seen.keys()) {
|
|
2697
|
+
const root = find(url);
|
|
2698
|
+
const members = components.get(root);
|
|
2699
|
+
if (members) members.push(url);
|
|
2700
|
+
else components.set(root, [url]);
|
|
2701
|
+
}
|
|
2702
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2703
|
+
let refused = 0;
|
|
2704
|
+
for (const members of components.values()) {
|
|
2705
|
+
if (members.length < 2) continue;
|
|
2706
|
+
const declared = new Set(
|
|
2707
|
+
members.map((m) => seen.get(m)?.xDefault).filter((x) => Boolean(x))
|
|
2708
|
+
);
|
|
2709
|
+
if (declared.size !== 1) {
|
|
2710
|
+
refused++;
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
const anchor = [...declared][0];
|
|
2714
|
+
if (!members.includes(anchor)) {
|
|
2715
|
+
refused++;
|
|
2716
|
+
continue;
|
|
2717
|
+
}
|
|
2718
|
+
const { route } = splitRoute(new URL(anchor).pathname);
|
|
2719
|
+
for (const member of members) rows.set(member, route);
|
|
2720
|
+
}
|
|
2721
|
+
return indexOf(rows, [], refused);
|
|
2722
|
+
}
|
|
2723
|
+
function combineClusterIndexes(primary, secondary) {
|
|
2724
|
+
const rows = new Map(secondary.rows);
|
|
2725
|
+
const conflicts = [...primary.conflicts];
|
|
2726
|
+
const add = (url) => {
|
|
2727
|
+
if (!conflicts.includes(url)) conflicts.push(url);
|
|
2623
2728
|
};
|
|
2729
|
+
for (const url of secondary.conflicts) add(url);
|
|
2730
|
+
for (const [url, route] of primary.rows) {
|
|
2731
|
+
const claimed = rows.get(url);
|
|
2732
|
+
if (claimed !== void 0 && claimed !== route) add(url);
|
|
2733
|
+
rows.set(url, route);
|
|
2734
|
+
}
|
|
2735
|
+
return indexOf(rows, conflicts, primary.refused + secondary.refused);
|
|
2736
|
+
}
|
|
2737
|
+
function clustersOnlyFrom(primary, secondary) {
|
|
2738
|
+
const routes = /* @__PURE__ */ new Set();
|
|
2739
|
+
for (const [url, route] of secondary.rows) {
|
|
2740
|
+
if (!primary.rows.has(url)) routes.add(route);
|
|
2741
|
+
}
|
|
2742
|
+
return routes.size;
|
|
2624
2743
|
}
|
|
2625
2744
|
|
|
2626
2745
|
// src/lib/core/coverage.ts
|
|
@@ -3626,7 +3745,7 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3626
3745
|
crawlFallback: false
|
|
3627
3746
|
}).catch(() => void 0);
|
|
3628
3747
|
const allSitemapUrls = (discovery?.urls ?? []).map((u) => u.loc);
|
|
3629
|
-
const
|
|
3748
|
+
const sitemapClusters = buildClusterIndex(discovery?.urls ?? []);
|
|
3630
3749
|
const coverageMode = options.coverage ?? (allSitemapUrls.length > 0 ? "structural" : "all");
|
|
3631
3750
|
const selection = coverageMode === "structural" && allSitemapUrls.length > 0 ? selectByStructure(allSitemapUrls, { locales: options.locales }) : void 0;
|
|
3632
3751
|
const sitemapUrls = selection?.urls ?? allSitemapUrls;
|
|
@@ -3729,6 +3848,9 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3729
3848
|
);
|
|
3730
3849
|
}
|
|
3731
3850
|
if (!ICU_KNOWS_LANGUAGES) warnings.push(ICU_UNAVAILABLE_WARNING);
|
|
3851
|
+
const headClusters = buildHeadClusterIndex(htmlPages);
|
|
3852
|
+
const clusters = combineClusterIndexes(sitemapClusters, headClusters);
|
|
3853
|
+
const clustersFromHead = clustersOnlyFrom(sitemapClusters, headClusters);
|
|
3732
3854
|
const matrix = buildI18nMatrix(htmlPages, {
|
|
3733
3855
|
declaredUrls: sitemapUrls,
|
|
3734
3856
|
locales: localeAxis.locales,
|
|
@@ -3736,7 +3858,13 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3736
3858
|
});
|
|
3737
3859
|
if (clusters.conflicts.length > 0) {
|
|
3738
3860
|
warnings.push(
|
|
3739
|
-
`${clusters.conflicts.length} URL(s) are declared in two different hreflang clusters by the sitemap
|
|
3861
|
+
`${clusters.conflicts.length} URL(s) are declared in two different hreflang clusters by the sitemap and/or the pages' \`<head>\`; the sitemap's declaration was kept: ${clusters.conflicts.slice(0, 3).join(", ")}${clusters.conflicts.length > 3 ? `, +${clusters.conflicts.length - 3} more` : ""}.`
|
|
3862
|
+
);
|
|
3863
|
+
}
|
|
3864
|
+
const unverifiedAlternates = countUnverifiedAlternates(matrix);
|
|
3865
|
+
if (unverifiedAlternates > 0) {
|
|
3866
|
+
warnings.push(
|
|
3867
|
+
`${unverifiedAlternates} translation${unverifiedAlternates === 1 ? "" : "s"} in the matrix ${unverifiedAlternates === 1 ? "is" : "are"} vouched for only by an \`hreflang\` on another page \u2014 not crawled, and not in the sitemap. They are counted as present, so a page advertised but not served reads as translated rather than missing.`
|
|
3740
3868
|
);
|
|
3741
3869
|
}
|
|
3742
3870
|
const holes = localeAxis.multilingual ? deriveTranslationHoles(matrix, options.ignoreHoles) : [];
|
|
@@ -3757,7 +3885,11 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3757
3885
|
matrix,
|
|
3758
3886
|
localeAxis,
|
|
3759
3887
|
discovery,
|
|
3760
|
-
robots
|
|
3888
|
+
robots,
|
|
3889
|
+
// Same index the matrix was built from, so a rule that groups by route
|
|
3890
|
+
// and the grid that reports holes cannot disagree about which URLs are
|
|
3891
|
+
// one page.
|
|
3892
|
+
clusterRouteOf: clusters.routeOf
|
|
3761
3893
|
};
|
|
3762
3894
|
const siteOccurrence = /* @__PURE__ */ new Map();
|
|
3763
3895
|
const siteIssues = lintSite(siteContext).map((draft) => {
|
|
@@ -3883,13 +4015,22 @@ async function runAudit(inputUrl, options = {}) {
|
|
|
3883
4015
|
},
|
|
3884
4016
|
...ignoredHoles > 0 ? { ignoredHoles } : {},
|
|
3885
4017
|
...duplicatePages > 0 ? { duplicatePages } : {},
|
|
4018
|
+
...unverifiedAlternates > 0 ? { unverifiedAlternates } : {},
|
|
3886
4019
|
// Only when the site declared clusters. A merge that changes which rows
|
|
3887
4020
|
// exist must be visible: the alternative is a hole count that moved for
|
|
3888
4021
|
// a reason nothing in the report explains.
|
|
3889
|
-
...clusters.size > 0 ? {
|
|
4022
|
+
...clusters.size > 0 || clusters.refused > 0 ? {
|
|
3890
4023
|
declaredClusters: {
|
|
3891
4024
|
count: clusters.size,
|
|
3892
|
-
...clusters.conflicts.length > 0 ? { conflicts: clusters.conflicts.length } : {}
|
|
4025
|
+
...clusters.conflicts.length > 0 ? { conflicts: clusters.conflicts.length } : {},
|
|
4026
|
+
// Which source did the work. On a site that declares both ways
|
|
4027
|
+
// the two indexes describe the same clusters, so this counts the
|
|
4028
|
+
// rows only the `<head>` supplied rather than a difference of
|
|
4029
|
+
// totals that would claim a gain that is not there.
|
|
4030
|
+
...clustersFromHead > 0 ? { fromHead: clustersFromHead } : {},
|
|
4031
|
+
// Declared and not used, for want of an anchor to name the row
|
|
4032
|
+
// after. A declaration we saw and did not act on has to be said.
|
|
4033
|
+
...clusters.refused > 0 ? { refused: clusters.refused } : {}
|
|
3893
4034
|
}
|
|
3894
4035
|
} : {},
|
|
3895
4036
|
...discovery ? {
|
|
@@ -4566,4 +4707,4 @@ export {
|
|
|
4566
4707
|
summarize,
|
|
4567
4708
|
Logger
|
|
4568
4709
|
};
|
|
4569
|
-
//# sourceMappingURL=chunk-
|
|
4710
|
+
//# sourceMappingURL=chunk-SKX7UCGC.js.map
|