@goflag/cli 0.1.4 → 0.2.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/README.md +310 -22
- package/dist/{chunk-3777NRM4.js → chunk-GFM3YN5N.js} +753 -198
- package/dist/chunk-GFM3YN5N.js.map +1 -0
- package/dist/cli.js +45 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +548 -1
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/dist/chunk-3777NRM4.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
# Goflag
|
|
2
2
|
|
|
3
3
|
> **A lean CLI that flags the site problems humans can't catch at scale.**
|
|
4
|
-
> Point it at a URL and get
|
|
4
|
+
> Point it at a URL and get four things: broken links, missing translation pages, a robots.txt that contradicts your pages, and missing or misconfigured SEO metadata. JSON-first.
|
|
5
5
|
|
|
6
|
-
[](./package.json)
|
|
7
7
|
[](#license)
|
|
8
8
|
|
|
9
|
-
Goflag crawls a site once and
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
It is built for the mistakes that are invisible while browsing and expensive in
|
|
12
|
+
search: a `hreflang` that points at a 404, a canonical that silently
|
|
13
|
+
de-indexes a page, a `robots.txt` that contradicts every `<meta name="robots">`
|
|
14
|
+
on the site. A human cannot check those on 400 pages. That is the whole job.
|
|
10
15
|
|
|
11
16
|
## What it checks
|
|
12
17
|
|
|
@@ -17,37 +22,88 @@ Goflag crawls a site once and runs three focused static checks over it. It is de
|
|
|
17
22
|
|
|
18
23
|
Pages that declare a `<link rel="canonical">` pointing at another crawled page are excluded from the checks — the site has said they are duplicates, so linting them would multiply every finding by the number of filtered variants. The count is reported as `diagnostics.duplicatePages`.
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
**Client-rendered pages.** By default, a page whose `<head>` looks empty — no
|
|
26
|
+
title, no description, no canonical, no OG, no JSON-LD — is re-rendered in
|
|
27
|
+
headless Chromium before being judged, so a SPA is not reported as missing
|
|
28
|
+
everything. `--static` turns that off, including the detection: with it, a
|
|
29
|
+
client-rendered page is judged on its unhydrated shell.
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
That direction is deliberate. Static mode over-reports rather than under-reports
|
|
32
|
+
— the metadata is genuinely absent from the HTML a crawler receives — so it
|
|
33
|
+
fails loudly rather than passing quietly. It is also why `--static` is the right
|
|
34
|
+
default in CI: it needs no browser, and it cannot mistake a broken page for a
|
|
35
|
+
fine one.
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Requires Node `>=22`.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npx @goflag/cli https://example.com # one-off, nothing installed
|
|
43
|
+
pnpm add -D @goflag/cli # then: pnpm goflag https://example.com
|
|
27
44
|
```
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
(`packages/*`, `apps/*`) are ready for them, and empty stubs for things that do
|
|
32
|
-
not exist are how a repository accumulates code nobody calls.
|
|
46
|
+
Install it as a dev dependency once you run it more than once — in a script, in
|
|
47
|
+
CI, in a git hook. `npx` is for trying it.
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
The package is `@goflag/cli`; the command it installs is `goflag`. Everything
|
|
50
|
+
under the brand lives in the `@goflag` scope; the bare `goflag` name on npm is a
|
|
51
|
+
deprecated signpost pointing here and carries no code.
|
|
52
|
+
|
|
53
|
+
Headless rendering needs Chromium, which is **not** installed with goflag —
|
|
54
|
+
`playwright` is an optional peer dependency. Add it only if you audit
|
|
55
|
+
client-rendered pages without `--static`:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pnpm add -D playwright && pnpm exec playwright install chromium
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Getting started
|
|
37
62
|
|
|
38
|
-
|
|
63
|
+
Five steps, in this order. Each one is explained in full further down; this is
|
|
64
|
+
the sequence.
|
|
39
65
|
|
|
40
|
-
|
|
66
|
+
**1. Look at your site.** No flags, no gate — find out what is there.
|
|
41
67
|
|
|
42
68
|
```sh
|
|
43
69
|
npx @goflag/cli https://example.com
|
|
44
70
|
```
|
|
45
71
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
Expect it to be red. Almost every site that has never been audited has a
|
|
73
|
+
backlog, and that is not a reason to fix everything before continuing.
|
|
74
|
+
|
|
75
|
+
**2. Decide what is real.** Some findings are deliberate — a legal page that
|
|
76
|
+
exists in one jurisdiction, a route you chose not to translate. Tell goflag,
|
|
77
|
+
rather than living with noise:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
npx @goflag/cli https://example.com --ignore-holes /legal
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**3. Capture a baseline.** This freezes today's findings as "known", so the gate
|
|
84
|
+
can ask _did this change make it worse?_ instead of _is this site perfect?_
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
npx @goflag/cli https://example.com --static --no-external \
|
|
88
|
+
--baseline .goflag/baseline.json --update-baseline
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It prints how many findings it grandfathered and the `--max-debt` to set next.
|
|
92
|
+
Commit `.goflag/baseline.json`.
|
|
93
|
+
|
|
94
|
+
**4. Gate CI on regressions.** See [In CI](#in-ci) for a job you can copy.
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
goflag https://example.com --static --no-external \
|
|
98
|
+
--baseline .goflag/baseline.json --regressions-only --max-debt 41
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**5. Lower the ratchet.** Fix a finding, drop `--max-debt` by one, commit both.
|
|
102
|
+
This is the only part that makes the backlog shrink; without it a baseline
|
|
103
|
+
fossilises behind a passing build.
|
|
104
|
+
|
|
105
|
+
Steps 3 to 5 are the whole method. The rest of this README is why each flag
|
|
106
|
+
behaves the way it does.
|
|
51
107
|
|
|
52
108
|
You get a coloured terminal report and an exit code:
|
|
53
109
|
|
|
@@ -55,6 +111,11 @@ You get a coloured terminal report and an exit code:
|
|
|
55
111
|
- `1` — findings present (yellow/red flag) — use this as a CI gate
|
|
56
112
|
- `2` — fatal error (bad URL, unexpected failure)
|
|
57
113
|
|
|
114
|
+
## Using it
|
|
115
|
+
|
|
116
|
+
Everything below is reference: what each flag does and why it behaves that way.
|
|
117
|
+
The five steps above are the path; this is the map.
|
|
118
|
+
|
|
58
119
|
### JSON is the source of truth
|
|
59
120
|
|
|
60
121
|
```sh
|
|
@@ -77,6 +138,12 @@ The terminal view is just a render of that JSON.
|
|
|
77
138
|
--ignore-holes <glob> Route deliberately not translated everywhere
|
|
78
139
|
(repeatable).
|
|
79
140
|
--no-sitemap Do not discover the sitemap; crawl from <url> only.
|
|
141
|
+
--profile <name> Policy overlay on the rule set: default, strict,
|
|
142
|
+
spec-only, marketing.
|
|
143
|
+
--conformance Report every rule's status on every page, not just
|
|
144
|
+
the violations.
|
|
145
|
+
--advisories Attach the prose rules — the judgment calls goflag
|
|
146
|
+
refuses to fake — with the facts to judge them by.
|
|
80
147
|
--fail-on <level> Exit 1 at or above this severity: warning (default),
|
|
81
148
|
error, or never.
|
|
82
149
|
--regressions-only Weaken the gate: fail only on NEW findings relative
|
|
@@ -101,6 +168,100 @@ The terminal view is just a render of that JSON.
|
|
|
101
168
|
|
|
102
169
|
Headless mode needs Chromium. `playwright` is an optional peer dependency; if it's missing, install it with `npx playwright install chromium` (or just run with `--static`).
|
|
103
170
|
|
|
171
|
+
### Profiles: how much each rule matters to you
|
|
172
|
+
|
|
173
|
+
Every rule records how authoritative it is — whether a published spec requires
|
|
174
|
+
it, a vendor documents it, or it is widely-repeated folklore. That is a fact
|
|
175
|
+
about the rule, and goflag never fudges it. What your build should _do_ about
|
|
176
|
+
each one is a separate question, and that is what `--profile` answers:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
goflag https://example.com --profile spec-only # only what a spec backs
|
|
180
|
+
goflag https://example.com --profile strict # spec-backed rules fail the build
|
|
181
|
+
goflag https://example.com --profile marketing # metadata gaps are errors
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
| Profile | What it does |
|
|
185
|
+
| ----------- | ------------------------------------------------------------------------------ |
|
|
186
|
+
| `default` | Each rule's own severity. No overlay. |
|
|
187
|
+
| `strict` | Every spec-backed rule becomes an `error`. Heuristics stay warnings. |
|
|
188
|
+
| `spec-only` | Heuristic rules (title/description length) are switched off entirely. |
|
|
189
|
+
| `marketing` | The snippet and unfurl metadata — description, `og:title`, `og:image` — error. |
|
|
190
|
+
|
|
191
|
+
A profile only changes how loudly a rule fires and whether it runs at all. It
|
|
192
|
+
never changes what a rule observes, and it never rewrites a rule's stated
|
|
193
|
+
authority — so `strict` can make a finding fail your build, but it cannot turn
|
|
194
|
+
folklore into a spec requirement.
|
|
195
|
+
|
|
196
|
+
The report records which profile produced it (`"profile": "strict"`), and the
|
|
197
|
+
terminal output names any non-default one. A run under `spec-only` reporting
|
|
198
|
+
zero issues means "nothing a spec backs is wrong here", not "nothing is wrong".
|
|
199
|
+
|
|
200
|
+
That record is load-bearing: comparing a run against a baseline captured under
|
|
201
|
+
a different profile still works, but "0 new findings" no longer means what it
|
|
202
|
+
appears to, so goflag says so rather than letting the number speak for itself:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
REGRESSION GATE 0 new · 19 known findings NOT gating this build
|
|
206
|
+
baseline https://example.com — taken 2026-08-06 (today)
|
|
207
|
+
note: baseline was captured under profile `strict`, this run used `spec-only` — the two are not like-for-like.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
It is a warning, never a gate — a `--profile spec-only` investigation against a
|
|
211
|
+
`strict` baseline is a legitimate thing to do.
|
|
212
|
+
|
|
213
|
+
### Where do we stand? (`--conformance`)
|
|
214
|
+
|
|
215
|
+
A list of violations cannot tell you the difference between a rule that
|
|
216
|
+
passes everywhere and a rule that never applied to a single page. Both look
|
|
217
|
+
like silence. `--conformance` reports every rule's status on every page:
|
|
218
|
+
|
|
219
|
+
```sh
|
|
220
|
+
goflag https://example.com --conformance
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
Conformance
|
|
225
|
+
6 pages × every rule
|
|
226
|
+
canonical.absolute 6 fail [vendor-spec]
|
|
227
|
+
canonical.missing 6 pass [vendor-spec]
|
|
228
|
+
description.length 6 n/a [heuristic]
|
|
229
|
+
title.missing 6 pass [spec-required]
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The terminal shows the tally; `--json` carries the full rule × page grid,
|
|
233
|
+
with each rule's rigor and sources in a legend rather than repeated per
|
|
234
|
+
cell. `pass + fail + warn + n/a + crashed` always equals the page count, so
|
|
235
|
+
the coverage claim is checkable.
|
|
236
|
+
|
|
237
|
+
### The questions goflag will not answer for you (`--advisories`)
|
|
238
|
+
|
|
239
|
+
Whether a title _describes_ the page is not a thing a linter can decide.
|
|
240
|
+
goflag could fake it — count words, match boilerplate, emit a confident
|
|
241
|
+
verdict — and the result would be unfalsifiable noise. So it states the
|
|
242
|
+
question, cites what makes it a real requirement, attaches the observed
|
|
243
|
+
facts, and stops:
|
|
244
|
+
|
|
245
|
+
```sh
|
|
246
|
+
goflag https://example.com --advisories --json
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"ruleId": "description.accurate",
|
|
252
|
+
"prose": "Does the description accurately summarize this page's content, and is it written for this page rather than copied across the site?",
|
|
253
|
+
"rigor": "guideline",
|
|
254
|
+
"sources": ["google-snippet", "moz-meta-description"],
|
|
255
|
+
"evidence": { "meta.description": { "value": "…", "origin": { "kind": "meta" } } },
|
|
256
|
+
"verdict": "needs-judgment"
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Advisories are asked only where the subject exists — no question about a
|
|
261
|
+
description on a page that has none, because `description.missing` already
|
|
262
|
+
says that. They never count toward the summary, the verdict, or the exit
|
|
263
|
+
code: nobody has judged them yet.
|
|
264
|
+
|
|
104
265
|
### Gate on regressions, not on perfection
|
|
105
266
|
|
|
106
267
|
A plain run fails on any finding, which is unusable on a site that is not clean
|
|
@@ -206,6 +367,117 @@ goflag https://example.com --ignore-holes /legal --ignore-holes "/blog/**"
|
|
|
206
367
|
Suppressed gaps are counted in `diagnostics.ignoredHoles`, so a quiet report
|
|
207
368
|
never means "nothing was wrong" by accident.
|
|
208
369
|
|
|
370
|
+
## In CI
|
|
371
|
+
|
|
372
|
+
Two moments are worth auditing, and they answer different questions.
|
|
373
|
+
|
|
374
|
+
| When | Against | Answers |
|
|
375
|
+
| -------------------- | ------------------------------------------- | ---------------------------------------- |
|
|
376
|
+
| On the merge request | the branch's own build, booted by `--start` | does **this change** regress? |
|
|
377
|
+
| After deploying | the running environment | is what is **actually serving** correct? |
|
|
378
|
+
|
|
379
|
+
Neither replaces the other. Only the deployed run sees what the environment
|
|
380
|
+
injects — the real base URL, whatever the proxy serves for `robots.txt`,
|
|
381
|
+
redirects. A canonical broken by a misconfigured environment variable is
|
|
382
|
+
invisible in a local build.
|
|
383
|
+
|
|
384
|
+
Three rules, whichever you use:
|
|
385
|
+
|
|
386
|
+
- **Pin the version.** A floating one turns someone else's release into a red
|
|
387
|
+
pipeline on a commit that touched nothing, and the job is only worth having if
|
|
388
|
+
red means "you broke something".
|
|
389
|
+
- **Commit the baseline.** Grandfathering a finding then shows up in a diff
|
|
390
|
+
somebody reviews, instead of vanishing into a cache.
|
|
391
|
+
- **Build the same site the baseline was captured against.** Frameworks bake
|
|
392
|
+
build-time environment into the output; a build that differs produces
|
|
393
|
+
differences that are real in the report and meaningless as findings.
|
|
394
|
+
|
|
395
|
+
### GitLab CI
|
|
396
|
+
|
|
397
|
+
```yaml
|
|
398
|
+
stages: [build, deploy, audit]
|
|
399
|
+
|
|
400
|
+
variables:
|
|
401
|
+
GOFLAG_VERSION: "0.1.4"
|
|
402
|
+
|
|
403
|
+
# Before the merge: build the branch, let goflag boot it, audit that.
|
|
404
|
+
seo:mr:
|
|
405
|
+
stage: audit
|
|
406
|
+
image: node:24-alpine
|
|
407
|
+
rules:
|
|
408
|
+
- if: $CI_MERGE_REQUEST_IID
|
|
409
|
+
script:
|
|
410
|
+
- corepack enable && pnpm install --frozen-lockfile
|
|
411
|
+
- pnpm build
|
|
412
|
+
- npx --yes "@goflag/cli@$GOFLAG_VERSION" http://localhost:3000
|
|
413
|
+
--start "pnpm start" --static --no-external
|
|
414
|
+
--baseline .goflag/baseline.json --regressions-only --max-debt 41
|
|
415
|
+
artifacts:
|
|
416
|
+
when: always
|
|
417
|
+
paths: [goflag-report.json]
|
|
418
|
+
|
|
419
|
+
# After the deploy: audit what is serving.
|
|
420
|
+
seo:
|
|
421
|
+
stage: audit
|
|
422
|
+
image: node:24-alpine
|
|
423
|
+
needs: [{ job: deploy, artifacts: false }]
|
|
424
|
+
rules:
|
|
425
|
+
- if: $CI_COMMIT_BRANCH == "develop"
|
|
426
|
+
script:
|
|
427
|
+
- npx --yes "@goflag/cli@$GOFLAG_VERSION" https://develop.example.com
|
|
428
|
+
--static --no-external
|
|
429
|
+
--baseline .goflag/baseline.json --regressions-only --max-debt 41
|
|
430
|
+
--report goflag-report.json
|
|
431
|
+
artifacts:
|
|
432
|
+
when: always
|
|
433
|
+
paths: [goflag-report.json]
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### GitHub Actions
|
|
437
|
+
|
|
438
|
+
```yaml
|
|
439
|
+
name: seo
|
|
440
|
+
on: [pull_request]
|
|
441
|
+
|
|
442
|
+
jobs:
|
|
443
|
+
audit:
|
|
444
|
+
runs-on: ubuntu-latest
|
|
445
|
+
steps:
|
|
446
|
+
- uses: actions/checkout@v4
|
|
447
|
+
- uses: actions/setup-node@v4
|
|
448
|
+
with: { node-version: 24 }
|
|
449
|
+
- run: corepack enable && pnpm install --frozen-lockfile
|
|
450
|
+
- run: pnpm build
|
|
451
|
+
- run: |
|
|
452
|
+
npx --yes @goflag/cli@0.1.4 http://localhost:3000 \
|
|
453
|
+
--start "pnpm start" --static --no-external \
|
|
454
|
+
--baseline .goflag/baseline.json --regressions-only --max-debt 41
|
|
455
|
+
- uses: actions/upload-artifact@v4
|
|
456
|
+
if: always()
|
|
457
|
+
with:
|
|
458
|
+
name: goflag-report
|
|
459
|
+
path: goflag-report.json
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Why `--static --no-external` in CI
|
|
463
|
+
|
|
464
|
+
`--static` needs no browser, so the job runs on a plain Node image and cannot
|
|
465
|
+
be slowed by a Chromium download. `--no-external` skips off-origin links, which
|
|
466
|
+
are the ones you cannot fix and whose failures are somebody else's outage — a
|
|
467
|
+
gate that goes red because a third party is down teaches people to ignore it.
|
|
468
|
+
|
|
469
|
+
Audit external links on a schedule instead, where nobody is waiting.
|
|
470
|
+
|
|
471
|
+
### Reading a red build
|
|
472
|
+
|
|
473
|
+
The report artefact is the thing to open. It is the same JSON as `--json`, and
|
|
474
|
+
it is kept whether the job passed or failed, because it is most wanted when it
|
|
475
|
+
is red.
|
|
476
|
+
|
|
477
|
+
If the finding is one you have decided to accept, refresh the baseline with the
|
|
478
|
+
same command minus `--regressions-only`, plus `--update-baseline`. It will tell
|
|
479
|
+
you what it accepted, and that number belongs in the commit message.
|
|
480
|
+
|
|
209
481
|
## Programmatic API
|
|
210
482
|
|
|
211
483
|
```ts
|
|
@@ -217,6 +489,22 @@ console.log(report.summary); // { brokenLinks, missingTranslations, seoIssues, v
|
|
|
217
489
|
|
|
218
490
|
`runAudit` returns the same `GoflagReport` object the CLI emits.
|
|
219
491
|
|
|
492
|
+
## Repository layout
|
|
493
|
+
|
|
494
|
+
```
|
|
495
|
+
packages/cli/ @goflag/cli — the CLI (installs the `goflag` command)
|
|
496
|
+
tools/name-holder/ the bare `goflag` name on npm, a signpost to @goflag/cli
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
That is the whole tree. `@goflag/next` (the Next.js library) and the
|
|
500
|
+
documentation site are planned but deliberately absent: the workspace globs
|
|
501
|
+
(`packages/*`, `apps/*`) are ready for them, and empty stubs for things that do
|
|
502
|
+
not exist are how a repository accumulates code nobody calls.
|
|
503
|
+
|
|
504
|
+
When they land, they must not import from the CLI: the two products stay
|
|
505
|
+
independently useful, and an ESLint rule enforces it rather than trusting
|
|
506
|
+
memory.
|
|
507
|
+
|
|
220
508
|
## Develop locally
|
|
221
509
|
|
|
222
510
|
Requires pnpm `11.18.0` (pinned via `packageManager` — `corepack enable` picks
|