@goflag/cli 0.2.12 → 0.2.13
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
|
@@ -5,6 +5,12 @@ A CLI that audits a site for broken links, missing translation pages, a robots.t
|
|
|
5
5
|
[](https://github.com/tancredesimonin/goflag/blob/main/package.json)
|
|
6
6
|
[](#license)
|
|
7
7
|
|
|
8
|
+
[](https://goflag.tech/docs/report)
|
|
9
|
+
|
|
10
|
+
<sub>The first eleven lines of a real run. Rendered from the same transcript the
|
|
11
|
+
documentation site paints, which is compared to the renderers byte for byte —
|
|
12
|
+
so this picture cannot drift from what the command prints.</sub>
|
|
13
|
+
|
|
8
14
|
Goflag crawls a site once and judges what it found. It is deliberately small: no dashboard, no config system, no social-preview gallery. Just the findings, as a machine-readable report you can pipe, diff, or gate CI on.
|
|
9
15
|
|
|
10
16
|
It is built for the mistakes that are invisible while browsing and expensive in
|
|
@@ -61,7 +67,7 @@ fine one.
|
|
|
61
67
|
|
|
62
68
|
## Where it runs
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
Three packages ship to npm from a tag, published by GitLab CI over OIDC with no stored credential: `@goflag/cli` (`v*`), `@goflag/next` (`next-v*`) and `@goflag/og` (`og-v*`). The documentation site `apps/website` runs at [goflag.tech](https://goflag.tech), with `develop.goflag.tech` alongside it, both deployed with Kamal onto the shared OVH host described in the `infrastructure` repository.
|
|
65
71
|
|
|
66
72
|
## Your first audit
|
|
67
73
|
|
|
@@ -469,6 +475,11 @@ same confidence would be six unearned claims, so the file says which is which.
|
|
|
469
475
|
It never gates — it exits 0 unless the run itself failed — and it prints the
|
|
470
476
|
path it wrote, so `open "$(goflag preview http://localhost:3000)"` opens it.
|
|
471
477
|
|
|
478
|
+
An example of what it writes — a real audit, frozen, so its footer names the date it was
|
|
479
|
+
taken — is served at <https://goflag.tech/assets/example-preview.html>. The URL is absolute
|
|
480
|
+
because `prepack` copies this file into the package and npm renders it, where a relative link
|
|
481
|
+
is dead.
|
|
482
|
+
|
|
472
483
|
### Multilingual sites
|
|
473
484
|
|
|
474
485
|
Discovery is seeded from the sitemap, not just from links. That matters because
|
|
@@ -547,7 +558,7 @@ Three rules, whichever you use:
|
|
|
547
558
|
stages: [build, deploy, audit]
|
|
548
559
|
|
|
549
560
|
variables:
|
|
550
|
-
GOFLAG_VERSION: "0.2.
|
|
561
|
+
GOFLAG_VERSION: "0.2.13"
|
|
551
562
|
|
|
552
563
|
# Before the merge: build the branch, let goflag boot it, audit that.
|
|
553
564
|
seo:mr:
|
|
@@ -599,7 +610,7 @@ jobs:
|
|
|
599
610
|
- run: corepack enable && pnpm install --frozen-lockfile
|
|
600
611
|
- run: pnpm build
|
|
601
612
|
- run: |
|
|
602
|
-
npx --yes @goflag/cli@0.2.
|
|
613
|
+
npx --yes @goflag/cli@0.2.13 http://localhost:3000 \
|
|
603
614
|
--start "pnpm start" --static --no-external \
|
|
604
615
|
--baseline .goflag/baseline.json --regressions-only --max-debt 41 \
|
|
605
616
|
--report goflag-report.json
|
|
@@ -621,6 +632,25 @@ Audit external links on a schedule instead, where nobody is waiting.
|
|
|
621
632
|
|
|
622
633
|
### Reading a red build
|
|
623
634
|
|
|
635
|
+
A red build says what moved, not what is wrong. The diff lists rule ids and
|
|
636
|
+
absolute page URLs; the messages and the fixes are in the report:
|
|
637
|
+
|
|
638
|
+
<!-- goflag:transcript gate -->
|
|
639
|
+
|
|
640
|
+
```plaintext
|
|
641
|
+
goflag --regressions-only
|
|
642
|
+
REGRESSION 1 new · 13 known findings NOT gating this build · 1 resolved
|
|
643
|
+
baseline https://example.com/ — taken 2026-07-21T09:14:02.881Z (14 days ago)
|
|
644
|
+
|
|
645
|
+
New findings
|
|
646
|
+
+ error seo canonical.absolute on https://example.com/pricing
|
|
647
|
+
|
|
648
|
+
Resolved
|
|
649
|
+
- warn seo og.image.missing on https://example.com/about
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
<!-- /goflag:transcript -->
|
|
653
|
+
|
|
624
654
|
The report artefact is the thing to open. It is the same JSON as `--json`, and
|
|
625
655
|
it is kept whether the job passed or failed, because it is most wanted when it
|
|
626
656
|
is red.
|
|
@@ -683,7 +713,7 @@ finding unrepresentable. Full documentation at
|
|
|
683
713
|
Two of this catalogue's rules had no remedy to point at. `og.image.missing`
|
|
684
714
|
fired 24 times on one site and `og.image.alt` 46 times on another, and the fix
|
|
685
715
|
for both is "either an asset or a route you have to write" — which is how a rule
|
|
686
|
-
becomes permanent debt. [`@goflag/og`](packages/og) is that route, written once.
|
|
716
|
+
becomes permanent debt. [`@goflag/og`](https://github.com/tancredesimonin/goflag/tree/main/packages/og) is that route, written once.
|
|
687
717
|
|
|
688
718
|
```tsx
|
|
689
719
|
// app/[locale]/opengraph-image.tsx
|
|
@@ -701,7 +731,7 @@ embeds satori — turns into a PNG at build time, so nothing installs a second
|
|
|
701
731
|
renderer and a card can be unit-tested with no framework build. It also packs
|
|
702
732
|
the `favicon.ico` **no Next convention emits**, guarded so that a generated file
|
|
703
733
|
living in git is not dirtied by every commit. API reference in
|
|
704
|
-
[`packages/og/README.md`](packages/og/README.md).
|
|
734
|
+
[`packages/og/README.md`](https://github.com/tancredesimonin/goflag/blob/main/packages/og/README.md).
|
|
705
735
|
|
|
706
736
|
## Develop locally
|
|
707
737
|
|
|
@@ -6768,6 +6768,57 @@ function translationsPanel(current, siblings) {
|
|
|
6768
6768
|
</table></div>
|
|
6769
6769
|
<p class="foot">Lengths are graphemes, which is what a card's size ladder counts. The ladder is the site's own \u2014 <code>@goflag/og</code> ships none \u2014 so this reports the length and not the step.</p></section>`;
|
|
6770
6770
|
}
|
|
6771
|
+
function pathSegments(path) {
|
|
6772
|
+
const cut = path.indexOf("?");
|
|
6773
|
+
const pathname = cut === -1 ? path : path.slice(0, cut);
|
|
6774
|
+
const search = cut === -1 ? "" : path.slice(cut);
|
|
6775
|
+
const segments2 = pathname.split("/").filter(Boolean);
|
|
6776
|
+
if (search === "") return segments2;
|
|
6777
|
+
return segments2.length === 0 ? [search] : [...segments2.slice(0, -1), `${segments2.at(-1)}${search}`];
|
|
6778
|
+
}
|
|
6779
|
+
function buildRouteTree(paths) {
|
|
6780
|
+
const root = { segments: [], children: /* @__PURE__ */ new Map(), count: 0 };
|
|
6781
|
+
paths.forEach((path, index) => {
|
|
6782
|
+
let node = root;
|
|
6783
|
+
node.count++;
|
|
6784
|
+
for (const segment of pathSegments(path)) {
|
|
6785
|
+
const child = node.children.get(segment) ?? {
|
|
6786
|
+
segments: [segment],
|
|
6787
|
+
children: /* @__PURE__ */ new Map(),
|
|
6788
|
+
count: 0
|
|
6789
|
+
};
|
|
6790
|
+
node.children.set(segment, child);
|
|
6791
|
+
child.count++;
|
|
6792
|
+
node = child;
|
|
6793
|
+
}
|
|
6794
|
+
node.index ??= index;
|
|
6795
|
+
});
|
|
6796
|
+
return { ...root, children: foldChildren(root.children) };
|
|
6797
|
+
}
|
|
6798
|
+
function foldChildren(children) {
|
|
6799
|
+
return new Map([...children].map(([key, node]) => [key, foldChain(node)]));
|
|
6800
|
+
}
|
|
6801
|
+
function foldChain(node) {
|
|
6802
|
+
let current = node;
|
|
6803
|
+
while (current.index === void 0 && current.children.size === 1) {
|
|
6804
|
+
const only = [...current.children.values()][0];
|
|
6805
|
+
current = { ...only, segments: [...current.segments, ...only.segments] };
|
|
6806
|
+
}
|
|
6807
|
+
return { ...current, children: foldChildren(current.children) };
|
|
6808
|
+
}
|
|
6809
|
+
function routeList(node, open, className = "") {
|
|
6810
|
+
const items = [...node.children.values()].map((child) => {
|
|
6811
|
+
const label = esc(child.segments.map((segment) => `/${segment}`).join(""));
|
|
6812
|
+
if (child.children.size === 0) {
|
|
6813
|
+
return child.index === void 0 ? `<li class="leaf"><span>${label}</span></li>` : `<li class="leaf"><a href="#page-${child.index}">${label}</a></li>`;
|
|
6814
|
+
}
|
|
6815
|
+
const self = child.index === void 0 ? `<span class="go"></span>` : `<a class="go" href="#page-${child.index}" title="the page at this path">\u2197</a>`;
|
|
6816
|
+
return `<li><details${open ? " open" : ""}><summary><span class="seg">${label}</span><span class="n">${child.count}</span>${self}</summary>
|
|
6817
|
+
${routeList(child, open)}</details></li>`;
|
|
6818
|
+
});
|
|
6819
|
+
return `<ul${className ? ` class="${className}"` : ""}>${items.join("\n")}</ul>`;
|
|
6820
|
+
}
|
|
6821
|
+
var TREE_OPEN_MAX = 20;
|
|
6771
6822
|
function tagLabel(tag) {
|
|
6772
6823
|
const key = tag.property ?? tag.name ?? (tag.httpEquiv ? `http-equiv=${tag.httpEquiv}` : void 0);
|
|
6773
6824
|
if (key) return `${key}${tag.content ? ` = ${clip(tag.content, 120)}` : ""}`;
|
|
@@ -6837,9 +6888,21 @@ h1,h2,h3,.badge,.chip,.rid,.src,.from,.mono,code,pre,dt,.pin,.nav{font-family:va
|
|
|
6837
6888
|
h1{font-size:22px;letter-spacing:-.02em;margin:0 0 6px}
|
|
6838
6889
|
.lede{color:var(--ink2);margin:0;max-width:70ch}
|
|
6839
6890
|
header.doc{padding:44px 0 24px;border-bottom:1px solid var(--line)}
|
|
6840
|
-
.nav{
|
|
6841
|
-
.
|
|
6842
|
-
.
|
|
6891
|
+
.nav{margin:22px 0 0;max-width:620px}
|
|
6892
|
+
.navnote{margin:0 0 8px;font-family:var(--ui);font-size:12px;color:var(--ink3)}
|
|
6893
|
+
.tree,.tree ul{list-style:none;margin:0;padding:0;font-size:12px}
|
|
6894
|
+
.tree ul{margin-left:6px;padding-left:12px;border-left:1px solid var(--line)}
|
|
6895
|
+
.tree summary{display:flex;align-items:center;gap:10px;padding:3px 7px;border-radius:3px;color:var(--ink2);cursor:pointer;list-style:none}
|
|
6896
|
+
.tree summary::-webkit-details-marker{display:none}
|
|
6897
|
+
.tree summary::before{content:"\u25B8";flex:none;width:1ch;font-size:9px;color:var(--ink3)}
|
|
6898
|
+
.tree details[open]>summary::before{content:"\u25BE"}
|
|
6899
|
+
.tree summary:hover,.tree summary:focus-visible{background:var(--wash);color:var(--ink)}
|
|
6900
|
+
.tree .seg{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
6901
|
+
.tree .n{margin-left:auto;flex:none;font-size:10px;color:var(--ink3);font-variant-numeric:tabular-nums}
|
|
6902
|
+
.tree .go{flex:none;width:1.2em;text-align:right;font-size:11px;text-decoration:none;opacity:.55}
|
|
6903
|
+
.tree .go:hover,.tree .go:focus-visible{opacity:1}
|
|
6904
|
+
.tree .leaf a,.tree .leaf span{display:block;padding:3px 7px 3px 24px;color:var(--ink2);text-decoration:none;border-radius:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
6905
|
+
.tree .leaf a:hover,.tree .leaf a:focus-visible{background:var(--wash);color:var(--brand)}
|
|
6843
6906
|
.page{padding-top:52px}
|
|
6844
6907
|
.pagehead h2{font-size:16px;margin:0 0 4px}
|
|
6845
6908
|
.pagehead .url{margin:0;font-size:13px;color:var(--ink2);word-break:break-all}
|
|
@@ -6978,9 +7041,14 @@ function renderPreview(report, options = {}) {
|
|
|
6978
7041
|
byRoute.get(splitRoute(pathOf(extraction.http.finalUrl)).route) ?? []
|
|
6979
7042
|
)
|
|
6980
7043
|
).join("\n");
|
|
6981
|
-
const nav = extractions.length > 1 ? `<
|
|
6982
|
-
|
|
6983
|
-
|
|
7044
|
+
const nav = extractions.length > 1 ? `<nav class="nav">
|
|
7045
|
+
<p class="navnote">${extractions.length} pages, by path. A folder folds; the \u2197 beside one opens the page at that path itself.</p>
|
|
7046
|
+
${routeList(
|
|
7047
|
+
buildRouteTree(extractions.map((extraction) => pathOf(extraction.http.finalUrl))),
|
|
7048
|
+
extractions.length <= TREE_OPEN_MAX,
|
|
7049
|
+
"tree"
|
|
7050
|
+
)}
|
|
7051
|
+
</nav>` : "";
|
|
6984
7052
|
return `<!doctype html>
|
|
6985
7053
|
<html lang="en">
|
|
6986
7054
|
<head>
|
|
@@ -7294,4 +7362,4 @@ export {
|
|
|
7294
7362
|
summarize,
|
|
7295
7363
|
Logger
|
|
7296
7364
|
};
|
|
7297
|
-
//# sourceMappingURL=chunk-
|
|
7365
|
+
//# sourceMappingURL=chunk-57NVXEJS.js.map
|