@nuxtseo/cli 0.4.1 → 0.5.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 CHANGED
@@ -239,10 +239,14 @@ a different dataset: `vitals summary` and `vitals trend` read CrUX field p75,
239
239
  and `vitals findings` names the DOM element behind each failing vital.
240
240
  `status` reads the Site assessment: the verdict, the one ranked Next Action, and
241
241
  what changed. Start there. `status`, `page issues` and `search cohorts` refuse
242
- with exit `4` or `5` on an archived or paused Site.
242
+ with exit `4` or `5` on a paused Site. Removed Sites return `not_found` with exit `8`.
243
243
  The four live `backlinks` reads need the Site to have a URL. Without one they
244
244
  exit `2`, and retrying will not help; set the Site URL first.
245
- `search cohorts` returns two lists. Only `established` is proven; `ranked` is
245
+ `status` reports `assessment.scope`: `site` when the headline covers the whole
246
+ Site, `route-family` when one family leads. `assessment.aggregate` always holds
247
+ the whole-Site verdict.
248
+ `search cohorts` returns `analysed: false` with `reason: sampled-index-state`
249
+ when the not-indexed list is a sample. Otherwise it returns two lists. Only `established` is proven; `ranked` is
246
250
  ordered by raw rate with no significance test and is leads only.
247
251
  `backlinks summary`, `backlinks referring-domains`, `backlinks anchors`,
248
252
  `backlinks history`, `research domain-traffic` and `research domain-availability`
@@ -250,7 +254,7 @@ can use the Team research limit. Each response reports
250
254
  cache use in `evidence`; a cached response spends nothing. `backlinks recoverable`
251
255
  reads retained rows and spends nothing.
252
256
  `search status` reads stored connection state. It never waits for Google.
253
- `search indexing summary` reads retained URL Inspection coverage.
257
+ `search indexing summary` reads retained URL Inspection coverage. `asOf` and `oldestCheckAt` bound the age of its verdicts.
254
258
  `search index-history` dates an indexing change against known releases.
255
259
  `search inspect <url>` reads Google's own verdict for one URL.
256
260
  `page inspect <url>` reads the Nuxt SEO observation store for the same URL.
@@ -331,3 +335,21 @@ and what to do for each exit code.
331
335
  The CLI prints the exact server error code. When available, stderr also includes
332
336
  the request ID, retry delay, rate limit, reset time, and structured details.
333
337
  The CLI never falls back to MCP, a private route, or cached feature data.
338
+
339
+ ### Claim response
340
+
341
+ `actions resolve` saves your Claim. It returns before a requested scan finishes starting.
342
+ `status: "open"` stays unchanged until the engine verifies the Action.
343
+ Human output shows `Claimed, verifying` and the suggested check-back time.
344
+ Claimed Actions leave `actions list`. Use `actions show <action-id>` to check them.
345
+
346
+ Both `resolve` and `show` return these additive fields:
347
+
348
+ | Field | Meaning |
349
+ | --- | --- |
350
+ | `markedAt` | ISO timestamp when your Claim was saved, or `null` before a Claim. |
351
+ | `checkBackAt` | Suggested ISO timestamp to check again, or `null` when no automated check applies. |
352
+
353
+ A check-back time does not promise a completed check.
354
+ `scan-queued` can mean a re-assessment using scheduled crawl results, without a new scan.
355
+ A self-resolved Claim returns `status: "verified"` and `verification: "user-resolved"`.
package/dist/cli-entry.js CHANGED
@@ -12,6 +12,7 @@ function outputError(cause) {
12
12
  }
13
13
  process.once('SIGINT', abort);
14
14
  process.once('SIGTERM', abort);
15
+ // stdout writes can finish after runCli returns. Keep this handler for the process lifetime.
15
16
  process.stdout.once('error', outputError);
16
17
  async function readStdin() {
17
18
  process.stdin.setEncoding('utf8');
@@ -38,4 +39,3 @@ process.exitCode = await runCli(process.argv.slice(2), {
38
39
  });
39
40
  process.removeListener('SIGINT', abort);
40
41
  process.removeListener('SIGTERM', abort);
41
- process.stdout.removeListener('error', outputError);
package/dist/commands.js CHANGED
@@ -175,10 +175,11 @@ async function login(runtime, globals, options) {
175
175
  const apiUrl = await resolveApiUrl({ apiUrl: globals.apiUrl, env: runtime.env, paths: runtime.paths });
176
176
  if (apiUrl._tag === 'Err')
177
177
  return fromStateError(apiUrl.error);
178
- // Pairing needs a browser and a person. Without an interactive terminal the
179
- // old paste path is the only one that can complete, so CI keeps working
180
- // unchanged.
181
- const token = options.withToken || !runtime.interactive
178
+ // Pairing needs a person to approve it, and `--no-browser` says that person
179
+ // will open the printed URL themselves, so it is available on a headless
180
+ // machine too. Without the flag a non-interactive terminal keeps the paste
181
+ // path, so CI keeps working unchanged.
182
+ const token = options.withToken || (!runtime.interactive && !options.noBrowser)
182
183
  ? await readLoginToken(runtime)
183
184
  : await pairInBrowser(runtime, apiUrl.value.apiUrl, { openBrowser: !options.noBrowser });
184
185
  if (token._tag === 'Err')
package/dist/failures.js CHANGED
@@ -41,7 +41,7 @@ function defaultFailureCode(exitCode) {
41
41
  export function fail(exitCode, message, cause, code = defaultFailureCode(exitCode)) {
42
42
  return { _tag: 'Err', error: { _tag: 'CliFailure', code, exitCode, message, cause } };
43
43
  }
44
- function apiExitCode(code, retryable) {
44
+ function apiExitCode(code, retryable, details = {}) {
45
45
  switch (code) {
46
46
  case 'invalid_request':
47
47
  return EXIT_CODE.invalidInput;
@@ -57,8 +57,14 @@ function apiExitCode(code, retryable) {
57
57
  return EXIT_CODE.conflict;
58
58
  case 'rate_limited':
59
59
  case 'quota_exhausted':
60
- case 'provider_unavailable':
61
60
  return EXIT_CODE.retryable;
61
+ case 'provider_unavailable':
62
+ // A provider that refuses to serve until its own billing is settled
63
+ // cannot be retried into working, so the caller must not treat it as a
64
+ // transient outage.
65
+ return details.providerCode === 'provider_payment_required'
66
+ ? EXIT_CODE.authorization
67
+ : EXIT_CODE.retryable;
62
68
  case 'not_found':
63
69
  return EXIT_CODE.notFound;
64
70
  case 'contract_violation':
@@ -201,7 +207,7 @@ export function fromSdkFailure(error) {
201
207
  error: {
202
208
  _tag: 'CliFailure',
203
209
  code: error.code,
204
- exitCode: apiExitCode(error.code, error.retryable),
210
+ exitCode: apiExitCode(error.code, error.retryable, error.details),
205
211
  message: [
206
212
  `${error.code}: ${error.message}`,
207
213
  ...metadataLines(error),
package/dist/render.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { AccountTokenData } from '@nuxtseo/protocol/v1/account';
3
3
  import type { ActionDismiss, ActionList, ActionResolve, ActionShow } from '@nuxtseo/protocol/v1/actions';
4
4
  import type { AnalyticsQueryData, AnalyticsView } from '@nuxtseo/protocol/v1/analytics';
5
5
  import type { AuditChanges, AuditContentDecay, AuditDuplicateClusters, AuditLinkOpportunities, AuditLinkStructure } from '@nuxtseo/protocol/v1/audit';
6
- import type { BacklinkAnchors, BacklinksHistory, BacklinksSummary, MentionsData, RecoverableBacklinksData, ReferringDomains } from '@nuxtseo/protocol/v1/backlinks';
6
+ import type { BacklinkAnchorsData, BacklinksHistory, BacklinksSummaryData, MentionsData, RecoverableBacklinksData, ReferringDomainsData } from '@nuxtseo/protocol/v1/backlinks';
7
7
  import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, SearchAnalyticsData, SearchStatusData, UrlInspectionData } from '@nuxtseo/protocol/v1/gsc';
8
8
  import type { PageInspectData, PageIssues, PageScan } from '@nuxtseo/protocol/v1/pages';
9
9
  import type { FieldVitalFindings, FieldVitals, MonitoredPages, PerformanceScanDetail, PerformanceScans, SitePerformanceOverview } from '@nuxtseo/protocol/v1/performance';
@@ -46,9 +46,14 @@ export declare function renderRecoverableBacklinks(data: RecoverableBacklinksDat
46
46
  export declare function renderMentions(data: MentionsData): string;
47
47
  export declare function renderFieldVitals(data: FieldVitals): string;
48
48
  export declare function renderFieldVitalFindings(data: FieldVitalFindings): string;
49
- export declare function renderBacklinksSummary(data: BacklinksSummary): string;
50
- export declare function renderReferringDomains(data: ReferringDomains): string;
51
- export declare function renderBacklinkAnchors(data: BacklinkAnchors): string;
49
+ /**
50
+ * With Link Spam Networks, the two profile counts print without them, and each
51
+ * count says how many more the networks hold. `linkSpamNetworks` is absent from
52
+ * an API that predates it, and the summary then prints as it always did.
53
+ */
54
+ export declare function renderBacklinksSummary(data: BacklinksSummaryData): string;
55
+ export declare function renderReferringDomains(data: ReferringDomainsData): string;
56
+ export declare function renderBacklinkAnchors(data: BacklinkAnchorsData): string;
52
57
  export declare function renderBacklinksHistory(data: BacklinksHistory): string;
53
58
  export declare function renderSiteStatus(data: SiteStatus): string;
54
59
  export declare function renderActionDismissal(data: ActionDismiss): string;
package/dist/render.js CHANGED
@@ -58,7 +58,9 @@ export function renderAction(data) {
58
58
  return [
59
59
  fields([
60
60
  ['Action', data.id],
61
- ['Status', data.status],
61
+ ['Status', data.markedAt && data.status === 'open' ? 'Claimed, verifying' : data.status],
62
+ ['Claimed at', data.markedAt ?? ''],
63
+ ['Check back at', data.checkBackAt ?? ''],
62
64
  ['Diagnosis', data.diagnosis],
63
65
  ['Do', data.action],
64
66
  ['Finding', data.findingType],
@@ -70,7 +72,9 @@ export function renderAction(data) {
70
72
  export function renderActionResolution(data) {
71
73
  return fields([
72
74
  ['Action', data.actionId],
73
- ['Status', data.status],
75
+ ['Status', data.markedAt && data.status === 'open' ? 'Claimed, verifying' : data.status],
76
+ ['Claimed at', data.markedAt ?? ''],
77
+ ['Check back at', data.checkBackAt ?? ''],
74
78
  ['Verification', data.verification],
75
79
  ]);
76
80
  }
@@ -429,7 +433,11 @@ export function renderIndexingDiagnostics(data) {
429
433
  ['Indexed', data.indexed],
430
434
  ['Not indexed', Math.max(0, data.totalUrls - data.indexed)],
431
435
  ['Indexed rate', indexedPercent],
436
+ ['Oldest check', data.oldestCheckAt],
432
437
  ['As of', data.asOf],
438
+ ['As of source', data.asOfSource],
439
+ ['Verdicts older than 7d', data.verdictFreshness ? `${data.verdictFreshness.olderThan7dPercent}%` : null],
440
+ ['Verdicts older than 30d', data.verdictFreshness ? `${data.verdictFreshness.olderThan30dPercent}%` : null],
433
441
  ]),
434
442
  ...(issues.length > 0 ? ['Issues', ...issues] : []),
435
443
  ].join('\n');
@@ -618,12 +626,53 @@ export function renderFieldVitalFindings(data) {
618
626
  function spendLine(evidence) {
619
627
  return ['Source', `${evidence._tag} (${evidence.spend.units} unit)`];
620
628
  }
629
+ function counted(count, one, many) {
630
+ return `${count} ${count === 1 ? one : many}`;
631
+ }
632
+ /**
633
+ * The anchor is third-party text. JSON quoting keeps a network on one line and
634
+ * prints a control character, such as a terminal escape, as an escape code.
635
+ */
636
+ function linkSpamNetworkLine(network) {
637
+ return [
638
+ counted(network.referringDomains, 'domain', 'domains'),
639
+ JSON.stringify(network.anchor),
640
+ network.linkingPath === null ? '' : `from ${network.linkingPath}`,
641
+ network.otherSites > 0 ? `on ${counted(network.otherSites, 'other Site', 'other Sites')}` : '',
642
+ ].filter(Boolean).join(' ');
643
+ }
644
+ /**
645
+ * The two profile counts without the networks, each followed by the part of the
646
+ * total the networks hold, so the two numbers add up to the total. The server
647
+ * never subtracts below the stored domains outside every network, so that part
648
+ * can be smaller than the networks' own counts.
649
+ */
650
+ function countsWithoutNetworks(data, without, networks) {
651
+ const width = Math.max(String(without.backlinks).length, String(without.referringDomains).length);
652
+ const inNetworks = counted(networks.length, 'link spam network', 'link spam networks');
653
+ const count = (total, own, networkCount) => {
654
+ const held = total === null ? networkCount : Math.max(total - own, 0);
655
+ return `${String(own).padEnd(width)} (${held} more in ${inNetworks})`;
656
+ };
657
+ return {
658
+ backlinks: count(data.backlinks, without.backlinks, networks.reduce((sum, network) => sum + network.backlinks, 0)),
659
+ referringDomains: count(data.referringDomains, without.referringDomains, networks.reduce((sum, network) => sum + network.referringDomains, 0)),
660
+ };
661
+ }
662
+ /**
663
+ * With Link Spam Networks, the two profile counts print without them, and each
664
+ * count says how many more the networks hold. `linkSpamNetworks` is absent from
665
+ * an API that predates it, and the summary then prints as it always did.
666
+ */
621
667
  export function renderBacklinksSummary(data) {
622
- return fields([
668
+ const networks = data.linkSpamNetworks ?? [];
669
+ const without = networks.length > 0 ? data.withoutLinkSpamNetworks ?? null : null;
670
+ const counts = without ? countsWithoutNetworks(data, without, networks) : data;
671
+ const totals = fields([
623
672
  ['Target', data.target],
624
673
  ['Domain rank', data.rank],
625
- ['Backlinks', data.backlinks],
626
- ['Referring domains', data.referringDomains],
674
+ ['Backlinks', counts.backlinks],
675
+ ['Referring domains', counts.referringDomains],
627
676
  ['Referring main domains', data.referringMainDomains],
628
677
  ['Referring pages', data.referringPages],
629
678
  ['Broken backlinks', data.brokenBacklinks],
@@ -631,6 +680,30 @@ export function renderBacklinksSummary(data) {
631
680
  ['Spam score', data.spamScore],
632
681
  spendLine(data.evidence),
633
682
  ]);
683
+ if (networks.length === 0)
684
+ return totals;
685
+ return [
686
+ totals,
687
+ [`Link spam networks (${networks.length})`, ...networks.map(network => ` ${linkSpamNetworkLine(network)}`)].join('\n'),
688
+ ].join('\n\n');
689
+ }
690
+ const LINK_SPAM_NETWORK_MARK = ' (link spam network)';
691
+ /**
692
+ * A row the server tagged as a Link Spam Network member ends with a marker, and
693
+ * one line under the list counts them. An untagged row is not known to be in a
694
+ * network, which is never "verified clean". An API that predates the field
695
+ * tags nothing, and the list prints as it always did.
696
+ */
697
+ function networkMark(item) {
698
+ return item.linkSpamNetworkId ? LINK_SPAM_NETWORK_MARK : '';
699
+ }
700
+ function linkSpamNetworkFooter(items) {
701
+ const tagged = items.filter(item => item.linkSpamNetworkId).length;
702
+ if (tagged === 0)
703
+ return [];
704
+ return [tagged === 1
705
+ ? '1 row is in a link spam network. Google ignores most links like these.'
706
+ : `${tagged} rows are in link spam networks. Google ignores most links like these.`];
634
707
  }
635
708
  export function renderReferringDomains(data) {
636
709
  if (data.items.length === 0)
@@ -638,17 +711,17 @@ export function renderReferringDomains(data) {
638
711
  return [
639
712
  `Referring domains for ${data.target} (${data.items.length} of ${data.total})`,
640
713
  ...data.items.map(item => [
641
- `${item.domain}`,
714
+ `${item.domain}${networkMark(item)}`,
642
715
  ` ${fields([
643
716
  ['rank', item.rank],
644
717
  ['backlinks', item.backlinks],
645
718
  ['referring pages', item.referringPages],
646
719
  ['broken backlinks', item.brokenBacklinks],
647
- ['spam score', item.spamScore],
648
720
  ['sessions (90d)', item.sessions],
649
721
  ['first seen', item.firstSeen],
650
722
  ]).split('\n').join('\n ')}`,
651
723
  ].join('\n')),
724
+ ...linkSpamNetworkFooter(data.items),
652
725
  fields([spendLine(data.evidence)]),
653
726
  ].join('\n');
654
727
  }
@@ -658,7 +731,7 @@ export function renderBacklinkAnchors(data) {
658
731
  return [
659
732
  `Backlink anchors for ${data.target} (${data.items.length} of ${data.total})`,
660
733
  ...data.items.map(item => [
661
- `${item.anchor || '(empty anchor)'}`,
734
+ `${item.anchor || '(empty anchor)'}${networkMark(item)}`,
662
735
  ` ${fields([
663
736
  ['backlinks', item.backlinks],
664
737
  ['referring domains', item.referringDomains],
@@ -667,6 +740,7 @@ export function renderBacklinkAnchors(data) {
667
740
  ['first seen', item.firstSeen],
668
741
  ]).split('\n').join('\n ')}`,
669
742
  ].join('\n')),
743
+ ...linkSpamNetworkFooter(data.items),
670
744
  fields([spendLine(data.evidence)]),
671
745
  ].join('\n');
672
746
  }
@@ -687,12 +761,16 @@ export function renderSiteStatus(data) {
687
761
  blocks.push([
688
762
  data.assessment.summary,
689
763
  ` ${data.assessment.recommendation}`,
764
+ ...(data.assessment.scope === 'route-family' && data.assessment.aggregate
765
+ ? [`Whole Site: ${data.assessment.aggregate.summary}`]
766
+ : []),
690
767
  ].join('\n'));
691
768
  }
692
769
  blocks.push(fields([
693
770
  ['Site', data.site],
694
771
  ['Reach', data.assessment?.reachStage],
695
772
  ['Health', data.assessment?.healthStage],
773
+ ['Headline scope', data.assessment?.scope],
696
774
  ['Assessed', data.assessment?.assessedAt],
697
775
  ['Data quality', data.dataQuality.status === 'complete'
698
776
  ? 'complete'
package/dist/skill.js CHANGED
@@ -211,6 +211,22 @@ export function skillNoticeFor(installed, current, agent) {
211
211
  }
212
212
  export function skillNoticeLine(notice) {
213
213
  const installed = notice.installed === null ? 'skill version unknown' : `skill ${notice.installed} installed`;
214
+ if (notice.installed !== null) {
215
+ const installedParts = /^(\d+)\.(\d+)\.(\d+)$/.exec(notice.installed);
216
+ const currentParts = /^(\d+)\.(\d+)\.(\d+)$/.exec(notice.current);
217
+ if (installedParts && currentParts) {
218
+ for (let part = 1; part <= 3; part++) {
219
+ const difference = Number(installedParts[part]) - Number(currentParts[part]);
220
+ if (difference > 0)
221
+ return `${installed}, CLI ${notice.current}. Update CLI before refreshing the skill: pnpm add -g @nuxtseo/cli`;
222
+ if (difference < 0)
223
+ break;
224
+ }
225
+ }
226
+ else {
227
+ return `${installed}, CLI ${notice.current}. Check which version is newer before refreshing the skill.`;
228
+ }
229
+ }
214
230
  return `${installed}, CLI ${notice.current}. Refresh with: nuxtseo skill install --agent ${notice.agent}`;
215
231
  }
216
232
  export function parseSkillCheckCache(content) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtseo/cli",
3
3
  "type": "module",
4
- "version": "0.4.1",
4
+ "version": "0.5.0",
5
5
  "description": "Command line interface for the Nuxt SEO public API.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://nuxtseo.com/pro",
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^1.8.0",
36
- "@nuxtseo/protocol": "^0.4.1",
37
- "@nuxtseo/sdk": "^0.4.1",
36
+ "@nuxtseo/protocol": "^0.5.0",
37
+ "@nuxtseo/sdk": "^0.5.0",
38
38
  "citty": "^0.2.2",
39
39
  "pathe": "^2.0.3"
40
40
  },
@@ -31,10 +31,10 @@ Read the matching file before you act on its subject:
31
31
  Inside the Nuxt SEO monorepo, run `node packages/cli/dist/cli-entry.js`
32
32
  instead.
33
33
 
34
- 2. **Skill version.** Run `nuxtseo --version --json`. If stderr carries a line
35
- like `skill 0.3.0 installed, CLI 0.4.0. Refresh with: nuxtseo skill install
36
- --agent claude`, run that command and re-read this file. A stale skill hides
37
- commands, so a missing command reads as a missing feature. If
34
+ 2. **Skill version.** Run `nuxtseo --version --json`. Compare the CLI version
35
+ with this file's frontmatter version. If the CLI is older, update the CLI
36
+ first. If the skill is older, run `nuxtseo skill install --agent claude` and
37
+ re-read this file. An older skill hides commands. If
38
38
  `NUXTSEO_NO_UPDATE_CHECK` is set, the line never appears; compare this
39
39
  file's frontmatter `version` with the binary instead.
40
40
 
@@ -107,6 +107,11 @@ nuxtseo status --site <site-id> --json
107
107
  ```
108
108
 
109
109
  - `available: false` means the first assessment has not run.
110
+ - Read `assessment.scope` before you quote `assessment.summary`. `site` means
111
+ the headline describes the whole Site. `route-family` means it describes one
112
+ Route family: a lead to check, never the Site verdict. `assessment.aggregate`
113
+ is always the whole-Site verdict. If `healthStage` is `quality_rejection`,
114
+ the whole Site is affected, whatever a route family shows.
110
115
  - Read `dataQuality.status` before you quote the verdict. `degraded` means
111
116
  proofs were missing, so the verdict is Provisional. `unavailable` means it
112
117
  has not run.
@@ -190,7 +195,7 @@ you got:
190
195
  | --- | --- |
191
196
  | `page inspect` | `observations.coverage` |
192
197
  | `page issues` | `availableKeys` |
193
- | `search cohorts` | `analysed: false`: no completed crawl, or no Google index state |
198
+ | `search cohorts` | `analysed: false`: no completed crawl, no Google index state, or `reason: "sampled-index-state"` |
194
199
  | `vitals summary` | `available: false` |
195
200
  | `research keywords`, `research rankings`, `research domain-*`, `page issues`, `search cohorts`, `vitals findings` | `data.message`, and `data.tip` for the repair |
196
201
 
@@ -200,9 +205,10 @@ a seed over three words, still exits `0`, and returns `keywords: []` with
200
205
 
201
206
  **A refusal is not an empty Site.** Exit `4` is a plan, scope, or entitlement
202
207
  blocker, for example `entitlement_required`. Exit `5` on `status`, `page
203
- issues`, or `search cohorts` can mean an archived or paused Site. Report the
204
- blocker and stop. Do not read it as "no data" or "nothing wrong". Do not
205
- retry unchanged, and do not route around it with `pull` or another read.
208
+ issues`, or `search cohorts` can mean a paused Site.
209
+ Removed Sites return `not_found` with exit `8`. Report the blocker and stop.
210
+ Do not read it as "no data" or "nothing wrong". Do not retry unchanged,
211
+ and do not route around it with `pull` or another read.
206
212
 
207
213
  **Quote the count the result ships.** Never quote the length of a list. Use
208
214
  `data.total` from `page issues`, and `coverage.accessibilityChecksTotal` and
@@ -224,16 +230,23 @@ true` marks the list as a floor.
224
230
  `search inspect` read retained Search Console evidence.
225
231
  - An indexed count comes from `search indexing summary`, never from analytics
226
232
  rows. It counts retained URL Inspection evidence as of `data.asOf`, not
227
- Google's live index. Name that source, quote `asOf`, and never label the
228
- figure current or "right now", even in a summary line. See
233
+ Google's live index. Name that source, quote `asOf` with `oldestCheckAt`
234
+ (the evidence range), and never label the figure current or "right now",
235
+ even in a summary line. See
229
236
  [references/indexing.md](references/indexing.md).
230
237
 
231
238
  **Route families: `established` versus `ranked`.** Read `search cohorts`
232
- before you list single not-indexed URLs. Never merge its two lists:
239
+ before you list single not-indexed URLs. If it returns `analysed: false` with
240
+ `reason: "sampled-index-state"`, the not-indexed list is a capped sample of the
241
+ count Search Console reports. No family rate exists. Read `search indexing
242
+ summary` instead, and never infer that unlisted URLs are indexed. Never merge
243
+ its two lists:
233
244
 
234
245
  - `established` holds only families whose not-indexed rate is significant
235
246
  against the rest of the Site, after a Bonferroni correction. Only these rows
236
247
  are a finding or a cause.
248
+ - `coverage.notIndexedNotInCrawl` counts not-indexed URLs that the Nuxt SEO
249
+ crawl did not reach. It does not count pages Google never crawled.
237
250
  - `ranked` orders every family by raw rate, with no significance test. A row
238
251
  with `statisticallyEstablished: false` is a lead to verify, for example by
239
252
  inspecting its URLs. Presenting it as a cause is a hard failure.
@@ -120,7 +120,7 @@ lines.
120
120
 
121
121
  | Command | What it returns, and its flags |
122
122
  | --- | --- |
123
- | `status` | The Site assessment in one read: verdict, the single ranked Next Action, open and verified counts, degraded pipeline steps, recent changes, Receipts. `available: false` means the first assessment has not run. If the Site is archived or paused, this exits `4` or `5` rather than answering |
123
+ | `status` | The Site assessment in one read: verdict, the single ranked Next Action, open and verified counts, degraded pipeline steps, recent changes, Receipts. `assessment.scope` says whether the headline covers the whole Site (`site`) or one route family (`route-family`); `assessment.aggregate` is always the whole-Site verdict. `available: false` means the first assessment has not run. Paused Sites exit `4` or `5`. Removed Sites return `not_found` with exit `8` |
124
124
  | `actions list` | Server-ranked issues and opportunities. `--limit 1..25` (default 10), `--offset`. Each row carries `evidence.freshness`; `verdict: "aged"` means the observation is over a day old, so live-check before fixing |
125
125
  | `actions show <action-id>` | One issue or opportunity, plus its evidence. `--limit 1..100` (default 50), `--group-id`, `--cursor` |
126
126
  | `actions resolve <action-id>` | Mutation. Claims the issue or opportunity and starts server verification. Reads the action first and sends `artifactVersion` for you |
@@ -131,7 +131,7 @@ lines.
131
131
  | Command | What it returns, and its flags |
132
132
  | --- | --- |
133
133
  | `page inspect <url>` | Stored observations, Lighthouse rows and keywords for one Page. `--limit 1..200`, `--offset`, `--include-resolved`. Carries `observations.coverage`: `never-scanned` means no source recorded this Page, `scanned-clear` means recorded and all clear |
134
- | `page issues` | Raw Page Issue rows. Pass `--action-id`, or pass both `--source` and `--issue`. Passing both selectors exits `2`. `--url`, `--path-prefix`, `--include-resolved`, `--limit 1..200`, `--offset`. When the selector matches nothing, `availableKeys` lists the pairs this Site does have. If the Site is archived or paused, this exits `4` or `5` rather than answering |
134
+ | `page issues` | Raw Page Issue rows. Pass `--action-id`, or pass both `--source` and `--issue`. Passing both selectors exits `2`. `--url`, `--path-prefix`, `--include-resolved`, `--limit 1..200`, `--offset`. When the selector matches nothing, `availableKeys` lists the pairs this Site does have. Paused Sites exit `4` or `5`. Removed Sites return `not_found` with exit `8` |
135
135
  | `page scan <url>` | Mutation. Starts a mobile Scan and a desktop Scan |
136
136
  | `performance` | Stored Lighthouse lab overview for the Site: medians for perf, a11y, SEO, LCP, TBT, CLS. Lab Scans, never real-user field data |
137
137
  | `scans list` | Retained Lighthouse Scans, newest first, with scores, lab vitals, and the error on a failed Scan. `--limit 1..100` (default 25) |
@@ -158,7 +158,7 @@ lines.
158
158
  | `search indexing <summary\|urls>` | Retained URL Inspection coverage. `summary` returns indexed counts. `urls` supports `--issue`, `--status`, `--limit 1..500` (default 50), `--offset` |
159
159
  | `search index-history` | Retained indexing changes and candidate causes. `--url`, `--field`, `--days 1..730`. Dates a regression against a release |
160
160
  | `search inspect <url>` | Google's own verdict for one URL. Takes an absolute URL or a site-relative path |
161
- | `search cohorts` | Not-indexed rate per route family. `--limit 1..50` (default 12), `--min-pages 1..500` (default 5). If the Site is archived or paused, this exits `4` or `5` rather than answering |
161
+ | `search cohorts` | Not-indexed rate per route family. Returns `analysed: false` when the not-indexed list is a sample (`reason: sampled-index-state`). `--limit 1..50` (default 12), `--min-pages 1..500` (default 5). Paused Sites exit `4` or `5`. Removed Sites return `not_found` with exit `8` |
162
162
  | `sitemaps list` | Search Console sitemap snapshot: errors, warnings, URL counts, sync status |
163
163
  | `sitemaps urls` | Sitemap URL membership. `--generation-id`, `--feedpath`, `--cursor`, `--limit 1..1000` |
164
164
  | `sitemaps submit <sitemap-url>` | Mutation. Submits one sitemap through the Site's stored Search Console credential |
@@ -215,9 +215,9 @@ research boundary in SKILL.md first.
215
215
  | `research rankings <domain>` | Live domain rankings: current keywords and domain metrics. `--limit 1..100` (default 50), `--min-position 1..100` (default 1), `--max-position 1..100` (default 20), `--location-code` (default 2840), `--order position\|traffic` (default `position`). So a default run returns only the top 20 positions. If `--max-position` is under `--min-position`, the run exits `2`. An unusable domain also exits `2`, with `invalid_request`. An empty `data.keywords` with exit `0` carries the reason in `data.message`. `data.cached` reports cache use |
216
216
  | `research domain-traffic <domain>` | A live organic traffic estimate for any domain: trend, top pages, top countries. The domain does not have to be one of your Sites |
217
217
  | `research domain-availability <domains>` | Registration status. One comma-separated positional, up to 10 domains |
218
- | `backlinks summary` | Whole-domain inbound link totals for the Site's own domain: `backlinks`, `referringDomains`, `referringPages`, `brokenBacklinks`, `brokenPages`, `crawledPages`, `spamScore`, `rank`. Takes no flags; it always reads the registered Site URL, so use `research domain-traffic` for any other domain. Every count is nullable. A `null` count means the provider returned no value. Never read it as zero. `evidence` reports cache use. Needs a Site URL |
219
- | `backlinks referring-domains` | Domains that link to the Site. `--limit 1..1000` (default 100). `sessions` is 90-day referral traffic, or null when no Web Analytics property is linked. Needs a Site URL |
220
- | `backlinks anchors` | Anchor text distribution of inbound links. `--limit 1..1000` (default 100). Needs a Site URL |
218
+ | `backlinks summary` | Whole-domain inbound link totals for the Site's own domain: `backlinks`, `referringDomains`, `referringPages`, `brokenBacklinks`, `brokenPages`, `crawledPages`, `spamScore`, `rank`. Takes no flags; it always reads the registered Site URL, so use `research domain-traffic` for any other domain. Every count is nullable. A `null` count means the provider returned no value. Never read it as zero. `linkSpamNetworks` groups referring domains that each place one generated anchor with the Site's host in it. Google ignores most links like these. `withoutLinkSpamNetworks` gives `referringDomains` and `backlinks` without them, or `null` when there is no network or no total. `evidence` reports cache use. Needs a Site URL |
219
+ | `backlinks referring-domains` | Domains that link to the Site. `--limit 1..1000` (default 100). `sessions` is 90-day referral traffic, or null when no Web Analytics property is linked. `linkSpamNetworkId` names the link spam network of a known member domain; null means not known, never verified clean. Needs a Site URL |
220
+ | `backlinks anchors` | Anchor text distribution of inbound links. `--limit 1..1000` (default 100). `linkSpamNetworkId` names the link spam network whose generated anchor the row is; null means not known, never verified clean. Needs a Site URL |
221
221
  | `backlinks history` | A monthly inbound link series. `--from YYYY-MM-DD`. Defaults to about twelve months. Needs a Site URL |
222
222
  | `backlinks recoverable` | Stored recoverable backlinks. `--limit 1..200`, `--offset`. Reads retained rows |
223
223
  | `mentions list` | Stored mentions. `--limit 1..200` (default 100), `--include-filtered` to keep the Mentions AI triage marked a false positive. Reads retained rows |
@@ -254,3 +254,21 @@ nuxtseo actions list --help --json
254
254
 
255
255
  Use it instead of guessing a flag. Prefer the tables above for anything they
256
256
  already answer.
257
+
258
+ ### Claim response
259
+
260
+ `actions resolve` saves your Claim. It returns before a requested scan finishes starting.
261
+ `status: "open"` stays unchanged until the engine verifies the Action.
262
+ Human output shows `Claimed, verifying` and the suggested check-back time.
263
+ Claimed Actions leave `actions list`. Use `actions show <action-id>` to check them.
264
+
265
+ Both `resolve` and `show` return these additive fields:
266
+
267
+ | Field | Meaning |
268
+ | --- | --- |
269
+ | `markedAt` | ISO timestamp when your Claim was saved, or `null` before a Claim. |
270
+ | `checkBackAt` | Suggested ISO timestamp to check again, or `null` when no automated check applies. |
271
+
272
+ A check-back time does not promise a completed check.
273
+ `scan-queued` can mean a re-assessment using scheduled crawl results, without a new scan.
274
+ A self-resolved Claim returns `status: "verified"` and `verification: "user-resolved"`.
@@ -11,11 +11,17 @@ Start with the retained URL Inspection summary:
11
11
  nuxtseo search indexing summary --site <site-id> --json
12
12
  ```
13
13
 
14
- Report `data.totalUrls`, `data.indexed`, and `data.asOf`. These are exact for
15
- the retained URL Inspection evidence at that timestamp. They are not a live
16
- count of Google's whole index. State that boundary with the result. If
17
- `data.asOf` is null, say the retained snapshot time is unavailable. Never call
18
- that result current or live.
14
+ Report `data.totalUrls`, `data.indexed`, `data.oldestCheckAt`, and
15
+ `data.asOf`. These are exact for the retained URL Inspection evidence in that
16
+ date range. `data.asOf` is when gscdump counted the verdicts, or the newest
17
+ check (`data.asOfSource` names it: `capture`, `rollup`, or `newest-check`). The
18
+ verdicts run back to `data.oldestCheckAt`, so quote both. When
19
+ `data.verdictFreshness` is set, quote it too: a fresh count can rest on old
20
+ verdicts, and `olderThan30dPercent` says how many. They are not a live
21
+ count of Google's whole index. The retained set can be far smaller than the
22
+ Search Console UI totals. State that boundary with the result. If `data.asOf`
23
+ is null, say the evidence age is unknown. Never call that result current or
24
+ live.
19
25
 
20
26
  Use the URL view when the user asks which URLs hold a verdict:
21
27
 
@@ -54,7 +60,7 @@ Keep these reads separate:
54
60
 
55
61
  | Question | Command | Report |
56
62
  | --- | --- | --- |
57
- | What is the indexed count for retained URLs? | `search indexing summary` | `totalUrls`, `indexed`, `asOf` |
63
+ | What is the indexed count for retained URLs? | `search indexing summary` | `totalUrls`, `indexed`, `oldestCheckAt`, `asOf` |
58
64
  | Which retained URLs are indexed or not indexed? | `search indexing urls` | `total`, then the complete `urls` stream |
59
65
  | Which URLs appeared in the current period? | `search analytics pages` | Rows with `impressions > 0`, then each `url` |
60
66
  | What does Google say about one URL now? | `search inspect <url>` | The returned URL Inspection verdict |
@@ -34,7 +34,7 @@ These fields carry the reason:
34
34
  | `research domain-traffic` | `data.message` |
35
35
  | `research domain-availability` | `data.message` |
36
36
  | `page issues` | `data.message`, plus `availableKeys` |
37
- | `search cohorts` | `data.message`, plus `reason` |
37
+ | `search cohorts` | `data.message`, plus `reason` (`no-completed-crawl`, `no-inspection-join`, or `sampled-index-state`) |
38
38
  | `vitals findings` | `data.message`, plus `source` |
39
39
 
40
40
  `evidence._tag: "no-provider"` does not prove the data is absent. On `research