@goflag/cli 0.2.11 → 0.2.12

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/index.d.ts CHANGED
@@ -379,6 +379,43 @@ interface ExtractionAsset {
379
379
  */
380
380
  sizes?: ExtractionAssetSize[];
381
381
  }
382
+ /** One tag, named the way the page named it. */
383
+ interface ExtractionHydratedMeta {
384
+ name?: string;
385
+ property?: string;
386
+ httpEquiv?: string;
387
+ content?: string;
388
+ }
389
+ /** One link, named the way the page named it. */
390
+ interface ExtractionHydratedLink {
391
+ rel: string;
392
+ href?: string;
393
+ hreflang?: string;
394
+ }
395
+ /**
396
+ * What client JavaScript added to, or took from, the `<head>`.
397
+ *
398
+ * The gap between the two passes, at tag granularity — not per `Fact`, which
399
+ * carries which *tag* produced a value and never which *pass*. A value present
400
+ * here is a value the browser shows and a non-JS crawler never sees: the
401
+ * failure mode every `og.*` rule is blind to, because each of them judges the
402
+ * declaration it is given and this is about which declaration that was.
403
+ *
404
+ * **Absent on most runs, and that is not "nothing changed".** The delta only
405
+ * exists where both bodies do: the auto-escalation path, where a static fetch
406
+ * looked empty and a headless pass followed. A `--static` run never renders,
407
+ * and an explicit `--headless` run keeps no static body to compare against.
408
+ */
409
+ interface ExtractionHydration {
410
+ titleChanged: boolean;
411
+ htmlLangChanged: boolean;
412
+ injectedMetas: ExtractionHydratedMeta[];
413
+ removedMetas: ExtractionHydratedMeta[];
414
+ injectedLinks: ExtractionHydratedLink[];
415
+ removedLinks: ExtractionHydratedLink[];
416
+ /** JSON-LD blocks that exist only after hydration. */
417
+ jsonLdBlocksAdded: number;
418
+ }
382
419
  /**
383
420
  * The per-page observation. Everything a page rule (or an agent judging a
384
421
  * prose rule) may read; nothing else.
@@ -402,6 +439,11 @@ interface Extraction {
402
439
  * manifest gets.
403
440
  */
404
441
  assets?: Record<string, ExtractionAsset>;
442
+ /**
443
+ * What hydration changed in the `<head>`, when both passes ran and can be
444
+ * compared. Absent means "not established" — never "nothing moved".
445
+ */
446
+ hydration?: ExtractionHydration;
405
447
  }
406
448
 
407
449
  /**
@@ -874,6 +916,26 @@ interface GoflagReport {
874
916
  * touch the summary counts, the verdict, or the exit code.
875
917
  */
876
918
  advisories?: ReportAdvisory[];
919
+ /**
920
+ * What each page declared in its `<head>`, as the rules saw it. Present
921
+ * only when asked for (`AuditOptions.extractions`, which `goflag preview`
922
+ * sets).
923
+ *
924
+ * Every other section of this report is a *judgment*: a rule fired, or it
925
+ * did not. This one is the observation the judgments were made from, and it
926
+ * is here because a share card cannot be drawn from a violations list — a
927
+ * page that passes every `og.*` rule still has a title, an image and a
928
+ * description that somebody wants to look at, and none of them appear
929
+ * anywhere else in this file.
930
+ *
931
+ * Keyed by nothing: each entry carries its own `http.finalUrl`, which is the
932
+ * same string `ReportPage.url` and `SeoIssue.pageUrl` use.
933
+ *
934
+ * Covers fewer pages than `pages`, for the same reason `conformance` does:
935
+ * only healthy HTML documents reach the rule layer, so an unreachable page,
936
+ * a non-HTML resource and a canonical-declared duplicate have no entry here.
937
+ */
938
+ extractions?: Extraction[];
877
939
  /** Comparison against a stored baseline, when `--baseline` was given. */
878
940
  diff?: ReportDiff;
879
941
  diagnostics: {
@@ -1054,6 +1116,20 @@ interface AuditOptions {
1054
1116
  * carry no verdict and never affect the gate.
1055
1117
  */
1056
1118
  advisories?: boolean;
1119
+ /**
1120
+ * Emit each page's extraction — what its `<head>` declared, as the rules
1121
+ * read it. Off by default: it is the observation, not a finding, and on a
1122
+ * large crawl it is the biggest section of the report.
1123
+ *
1124
+ * `goflag preview` sets it, because a share card is drawn from the
1125
+ * declarations and not from the verdicts about them.
1126
+ *
1127
+ * Unlike `conformance` and `advisories`, this section stops at the report:
1128
+ * `summarize()` does not carry it — a summary of an observation is the
1129
+ * observation with the useful parts removed — and no terminal renderer
1130
+ * prints it. Its one reader is `renderPreview`.
1131
+ */
1132
+ extractions?: boolean;
1057
1133
  }
1058
1134
  /**
1059
1135
  * Routes present in some locales but absent in others. A "hole" is exactly
@@ -1225,6 +1301,50 @@ interface RenderOptions {
1225
1301
  }
1226
1302
  declare function renderSummaryTerminal(summary: GoflagSummary, options?: RenderOptions): string;
1227
1303
 
1304
+ /**
1305
+ * The share-card preview — one self-contained HTML document.
1306
+ *
1307
+ * `renderPreview` is a pure function of a `GoflagReport`, exactly like the
1308
+ * three terminal renderers beside it. It reads `report.extractions`, which is
1309
+ * the only section that carries what a page *declared* rather than what a rule
1310
+ * *decided*: a page can pass every `og.*` rule and still unfurl badly, and no
1311
+ * violations list can show that.
1312
+ *
1313
+ * Two things this file refuses to do, and they are the point.
1314
+ *
1315
+ * **It draws no geometry it cannot source.** Every surface carries the same
1316
+ * rigor scale the rule catalogue uses, because the surfaces are not equally
1317
+ * documented: Facebook, LinkedIn and WhatsApp publish real numbers; Slack calls
1318
+ * its own rendering a "micro-approximation"; Discord publishes nothing at all;
1319
+ * X's card documentation is not reachable, and its shape changed twice since
1320
+ * 2023. A preview that renders all seven identically states seven things with
1321
+ * one confidence, and six of them are not earned.
1322
+ *
1323
+ * **It escapes everything.** The input is a site under audit, which is to say
1324
+ * untrusted text, and the output is a file a human opens in a browser. Values
1325
+ * go through `esc`; URLs that are not `http(s)` never become an `src`.
1326
+ *
1327
+ * Opening the document asks for the image URLs the pages declared, which is
1328
+ * what makes it a preview rather than a table. Those URLs are the site's own
1329
+ * choice and need not point at the audited origin, so the document ships a CSP
1330
+ * that allows images and nothing else — no script, no frame, no fetch — and
1331
+ * every image is `referrerpolicy="no-referrer"`. Rendering itself never touches
1332
+ * the network.
1333
+ */
1334
+
1335
+ interface RenderPreviewOptions {
1336
+ /** Document `<title>`. Defaults to the audited origin. */
1337
+ title?: string;
1338
+ }
1339
+ /**
1340
+ * Render the whole preview.
1341
+ *
1342
+ * A report without `extractions` renders a document that says so rather than an
1343
+ * empty one: the caller asked for a preview and got a report that cannot make
1344
+ * one, and that is a fact about the run, not an error to swallow.
1345
+ */
1346
+ declare function renderPreview(report: GoflagReport, options?: RenderPreviewOptions): string;
1347
+
1228
1348
  /**
1229
1349
  * Stable finding fingerprints.
1230
1350
  *
@@ -1328,4 +1448,4 @@ declare class Logger {
1328
1448
  private truncate;
1329
1449
  }
1330
1450
 
1331
- export { type AuditOptions, type AuditPhase, type BrokenLink, type GoflagReport, type GoflagSummary, type LogMode, Logger, type LoggerOptions, type ProgressEvent, type ReportPage, type ReportReciprocityIssue, type RollupLink, type RollupReciprocity, type RollupSeo, SAMPLE_LIMIT, type SeoIssue, type TranslationHole, type UnreachablePage, type Verdict, deriveTranslationHoles, exitCode, fingerprint, renderSummaryTerminal, renderTerminal, routeKey, runAudit, summarize, targetKey };
1451
+ export { type AuditOptions, type AuditPhase, type BrokenLink, type GoflagReport, type GoflagSummary, type LogMode, Logger, type LoggerOptions, type ProgressEvent, type RenderPreviewOptions, type ReportPage, type ReportReciprocityIssue, type RollupLink, type RollupReciprocity, type RollupSeo, SAMPLE_LIMIT, type SeoIssue, type TranslationHole, type UnreachablePage, type Verdict, deriveTranslationHoles, exitCode, fingerprint, renderPreview, renderSummaryTerminal, renderTerminal, routeKey, runAudit, summarize, targetKey };
package/dist/index.js CHANGED
@@ -5,19 +5,21 @@ import {
5
5
  deriveTranslationHoles,
6
6
  exitCode,
7
7
  fingerprint,
8
+ renderPreview,
8
9
  renderSummaryTerminal,
9
10
  renderTerminal,
10
11
  routeKey,
11
12
  runAudit,
12
13
  summarize,
13
14
  targetKey
14
- } from "./chunk-CWEJ6EXT.js";
15
+ } from "./chunk-QKXUKHXB.js";
15
16
  export {
16
17
  Logger,
17
18
  SAMPLE_LIMIT,
18
19
  deriveTranslationHoles,
19
20
  exitCode,
20
21
  fingerprint,
22
+ renderPreview,
21
23
  renderSummaryTerminal,
22
24
  renderTerminal,
23
25
  routeKey,
package/flags.json CHANGED
@@ -241,7 +241,7 @@
241
241
  "default": null,
242
242
  "requires": null,
243
243
  "group": "output",
244
- "description": "Roll findings up (dedup by link/rule/code). Pairs with --json for a compact, agent-friendly payload."
244
+ "description": "Roll findings up (dedup by link/rule/code). Pairs with --json for a compact, agent-friendly payload. Not available with --baseline: there the diff is the answer, and --report writes the full report."
245
245
  },
246
246
  {
247
247
  "name": "--timeout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goflag/cli",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
5
  "description": "Audits a site for broken links, missing translation pages, a robots.txt that contradicts your pages, and missing or misconfigured SEO metadata. JSON-first.",
6
6
  "keywords": [
@@ -66,7 +66,7 @@
66
66
  "generate:help-fixture": "tsx scripts/generate-help-fixture.ts",
67
67
  "prepack": "node ../../scripts/stage-package-docs.mjs",
68
68
  "prepublishOnly": "pnpm build",
69
- "release": "commit-and-tag-version --path . --skip.tag"
69
+ "release": "commit-and-tag-version --path . --skip.tag --tag-prefix v"
70
70
  },
71
71
  "dependencies": {
72
72
  "cheerio": "^1.2.0"
package/rules.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "counts": {
3
- "page": 23,
3
+ "page": 25,
4
4
  "site": 28,
5
5
  "prose": 5
6
6
  },
@@ -579,6 +579,34 @@
579
579
  "language": "tsx"
580
580
  }
581
581
  },
582
+ {
583
+ "id": "og.image.alt.caption",
584
+ "scope": "page",
585
+ "category": "opengraph",
586
+ "severity": "info",
587
+ "kind": "boolean",
588
+ "summary": "`og:image:alt` describes the image, it does not repeat the title",
589
+ "why": "The protocol asks for a description of what is in the image, and says in the same breath that it is not a caption. A copy of the title is the one value that is always available and never describes anything: a reader who cannot see the card is handed the same words twice and learns nothing about the picture. It also satisfies `og.image.alt` forever, which is why it needs an id of its own.",
590
+ "rigor": "guideline",
591
+ "sources": [
592
+ "ogp"
593
+ ],
594
+ "reads": [
595
+ "openGraph.images",
596
+ "openGraph.title",
597
+ "document.title"
598
+ ],
599
+ "relates": [
600
+ "og.image.alt",
601
+ "og.image.representative"
602
+ ],
603
+ "expected": "an `og:image:alt` that says something the title does not",
604
+ "fix": {
605
+ "title": "Say what the card shows, not what the page is called",
606
+ "snippet": "// The card is a title on a background. The alt says so — once, from a\n// template, so every page gets one and none of them is the title alone.\nconst ogAlt = (title: string) => `The title “${title}” on a dark preview card.`;",
607
+ "language": "ts"
608
+ }
609
+ },
582
610
  {
583
611
  "id": "og.image.dimensions",
584
612
  "scope": "page",
@@ -711,6 +739,35 @@
711
739
  ],
712
740
  "prose": "Does the og:image represent what this page is about, rather than being a site-wide default, and does its subject survive being cropped to the 1.91:1 aspect ratio consumers render?"
713
741
  },
742
+ {
743
+ "id": "og.image.sizes-mismatch",
744
+ "scope": "page",
745
+ "category": "opengraph",
746
+ "severity": "warning",
747
+ "kind": "boolean",
748
+ "summary": "`og:image:width` and `og:image:height` must be the image's real size",
749
+ "why": "The declared size is what a crawler lays the card out with before it has the file, which is the whole reason `og.image.dimensions` asks for it. A wrong one is therefore worse than none: the crawler reserves a shape the image does not have, and the card is letterboxed or cropped on the first share — the one that matters most.\n\nIt is also the declaration that blinds every other check. `og.image.ratio` reads these two numbers and refuses to fetch, so an invented 1200×630 scores 1.9 and passes over an image that is square. Found on a site whose cover art is 1024×1024 and one of whose covers is a 337-byte 1×1 placeholder, all three declared 1200×630 by a library that had never looked at them.",
750
+ "rigor": "guideline",
751
+ "sources": [
752
+ "ogp",
753
+ "meta-og-sharing"
754
+ ],
755
+ "reads": [
756
+ "openGraph.images",
757
+ "assets"
758
+ ],
759
+ "relates": [
760
+ "og.image.dimensions",
761
+ "og.image.ratio",
762
+ "og.image.reachable"
763
+ ],
764
+ "expected": "declared dimensions equal to the file's own",
765
+ "fix": {
766
+ "title": "Measure the image, or let the file convention do it",
767
+ "snippet": "// app/…/opengraph-image.tsx — `size` is the truth and the declaration at once\nexport const size = { width: 1200, height: 630 };\n\n// Naming a file instead? Then the numbers are that file's, not a default.\nopenGraph: { images: [{ url: cover.url, width: cover.width, height: cover.height }] }",
768
+ "language": "tsx"
769
+ }
770
+ },
714
771
  {
715
772
  "id": "og.locale.alternates",
716
773
  "scope": "page",