@nuxtseo/cli 0.4.1 → 0.5.1
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 +25 -3
- package/dist/cli-entry.js +1 -1
- package/dist/commands.js +14 -7
- package/dist/failures.js +9 -3
- package/dist/pull.js +5 -0
- package/dist/render.d.ts +9 -4
- package/dist/render.js +87 -9
- package/dist/skill.js +16 -0
- package/package.json +3 -3
- package/skills/nuxtseo-cli/SKILL.md +95 -16
- package/skills/nuxtseo-cli/references/commands.md +59 -20
- package/skills/nuxtseo-cli/references/indexing.md +12 -6
- package/skills/nuxtseo-cli/references/protocol.md +1 -1
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
|
|
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
|
-
`
|
|
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
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
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')
|
|
@@ -1222,7 +1223,11 @@ async function actionDismiss(runtime, globals, args) {
|
|
|
1222
1223
|
return fail(EXIT_CODE.invalidInput, 'Pass the action id to dismiss.', undefined, 'invalid_cli_input');
|
|
1223
1224
|
if (!args.artifactVersion)
|
|
1224
1225
|
return fail(EXIT_CODE.invalidInput, 'Pass --artifact-version, the artifactVersion returned with this action.', undefined, 'invalid_cli_input');
|
|
1225
|
-
|
|
1226
|
+
if (args.note !== undefined && (!args.note.trim() || args.note.trim().length > 2000))
|
|
1227
|
+
return fail(EXIT_CODE.invalidInput, 'Pass --note with 1 to 2000 characters.', undefined, 'invalid_cli_input');
|
|
1228
|
+
const confirmed = await confirmMutation(runtime, globals, args.note
|
|
1229
|
+
? `Dismiss action ${args.actionId}? Your note records this decision. No fix is verified.`
|
|
1230
|
+
: `Dismiss action ${args.actionId}? The page stays removed.`);
|
|
1226
1231
|
if (confirmed._tag === 'Err')
|
|
1227
1232
|
return confirmed;
|
|
1228
1233
|
if (!confirmed.value) {
|
|
@@ -1234,7 +1239,7 @@ async function actionDismiss(runtime, globals, args) {
|
|
|
1234
1239
|
return resolved;
|
|
1235
1240
|
const response = await withSpinner(runtime, 'Dismissing action', () => resolved.value.api.client.actions.dismiss({
|
|
1236
1241
|
params: { siteId: resolved.value.siteId, actionId: args.actionId },
|
|
1237
|
-
body: { artifactVersion: args.artifactVersion },
|
|
1242
|
+
body: { artifactVersion: args.artifactVersion, ...(args.note !== undefined ? { note: args.note.trim() } : {}) },
|
|
1238
1243
|
}, { signal: runtime.requestSignal }));
|
|
1239
1244
|
return present(runtime, globals, response, value => renderActionDismissal(value.data));
|
|
1240
1245
|
}
|
|
@@ -1558,14 +1563,16 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
1558
1563
|
run: ({ args }) => capture(execution, () => actionsResolve(runtime, globals, args.actionId), args._, 1)(),
|
|
1559
1564
|
}),
|
|
1560
1565
|
dismiss: defineCommand({
|
|
1561
|
-
meta: { name: 'dismiss', description: 'Dismiss a
|
|
1566
|
+
meta: { name: 'dismiss', description: 'Dismiss reviewed work with a note, or confirm an unlinked page removal' },
|
|
1562
1567
|
args: {
|
|
1563
1568
|
'actionId': positional('action-id', 'Action ID'),
|
|
1564
1569
|
'artifact-version': { type: 'string', description: 'The artifactVersion returned with this action' },
|
|
1570
|
+
'note': { type: 'string', description: 'Why this action is intentional or incorrect, 1 to 2000 characters' },
|
|
1565
1571
|
},
|
|
1566
1572
|
run: ({ args }) => capture(execution, () => actionDismiss(runtime, globals, {
|
|
1567
1573
|
actionId: args.actionId,
|
|
1568
1574
|
artifactVersion: args['artifact-version'],
|
|
1575
|
+
note: args.note,
|
|
1569
1576
|
}), args._, 1)(),
|
|
1570
1577
|
}),
|
|
1571
1578
|
},
|
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/pull.js
CHANGED
|
@@ -325,6 +325,11 @@ export async function runPull(runtime, globals, args, resolve, catalogue = PULL_
|
|
|
325
325
|
if (outcome.exitCode === EXIT_CODE.authentication || outcome.exitCode === EXIT_CODE.interrupted)
|
|
326
326
|
stopped = true;
|
|
327
327
|
writeDiagnostic(runtime, `[${completed}/${operations.length}] ${candidate.name} ${outcome.failure ? `failed: ${outcome.failure.code}` : 'ok'}`);
|
|
328
|
+
if (outcome.failure?.code === 'request_timeout' && timeoutMs < 300_000) {
|
|
329
|
+
const retryTimeout = Math.min(timeoutMs * 2, 300_000);
|
|
330
|
+
// Retry only this read. Repeating a full paid pull can spend twice.
|
|
331
|
+
writeDiagnostic(runtime, `Retry: nuxtseo pull --site ${resolved.value.siteId} --include ${JSON.stringify(candidate.name)} --period ${period.value} --timeout-ms ${retryTimeout}${candidate.spendsResearch ? ' --with-research' : ''} --json`);
|
|
332
|
+
}
|
|
328
333
|
});
|
|
329
334
|
const failed = exitCodes.filter(code => code !== EXIT_CODE.success).length;
|
|
330
335
|
writeDiagnostic(runtime, [
|
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 {
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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');
|
|
@@ -437,7 +445,7 @@ export function renderIndexingDiagnostics(data) {
|
|
|
437
445
|
if (data.urls.length === 0)
|
|
438
446
|
return 'No Search Console indexing URLs match these filters.';
|
|
439
447
|
return [
|
|
440
|
-
`Search Console indexing URLs (${data.urls.length} of ${data.total})`,
|
|
448
|
+
`Search Console indexing URLs (${data.urls.length} of ${data.totalSemantics === 'lower-bound' ? 'at least ' : ''}${data.total})`,
|
|
441
449
|
...data.urls.map(row => `${row.verdict ?? 'unknown'} ${row.coverageState ?? 'unknown'}\n ${row.url}`),
|
|
442
450
|
data.hasMore ? `More URLs available after offset ${data.offset + data.limit}.` : '',
|
|
443
451
|
].filter(Boolean).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
|
-
|
|
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',
|
|
626
|
-
['Referring domains',
|
|
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
|
+
"version": "0.5.1",
|
|
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.
|
|
37
|
-
"@nuxtseo/sdk": "^0.
|
|
36
|
+
"@nuxtseo/protocol": "^0.5.1",
|
|
37
|
+
"@nuxtseo/sdk": "^0.5.1",
|
|
38
38
|
"citty": "^0.2.2",
|
|
39
39
|
"pathe": "^2.0.3"
|
|
40
40
|
},
|
|
@@ -28,13 +28,23 @@ Read the matching file before you act on its subject:
|
|
|
28
28
|
pnpm add -g @nuxtseo/cli # global install, Node 22+
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Inside the Nuxt SEO monorepo,
|
|
32
|
-
|
|
31
|
+
Inside the Nuxt SEO monorepo, build the workspace dependencies in order
|
|
32
|
+
before using `node packages/cli/dist/cli-entry.js`:
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
--
|
|
37
|
-
|
|
34
|
+
```sh
|
|
35
|
+
pnpm --filter @nuxtseo/protocol build
|
|
36
|
+
pnpm --filter @nuxtseo/sdk build
|
|
37
|
+
pnpm --filter @nuxtseo/cli build
|
|
38
|
+
node packages/cli/dist/cli-entry.js --version --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use a task-owned worktree. Missing `dist` files and missing named exports
|
|
42
|
+
can mean stale builds. Rebuild all three packages before diagnosing source.
|
|
43
|
+
|
|
44
|
+
2. **Skill version.** Run `nuxtseo --version --json`. Compare the CLI version
|
|
45
|
+
with this file's frontmatter version. If the CLI is older, update the CLI
|
|
46
|
+
first. If the skill is older, run `nuxtseo skill install --agent claude` and
|
|
47
|
+
re-read this file. An older skill hides commands. If
|
|
38
48
|
`NUXTSEO_NO_UPDATE_CHECK` is set, the line never appears; compare this
|
|
39
49
|
file's frontmatter `version` with the binary instead.
|
|
40
50
|
|
|
@@ -107,10 +117,25 @@ nuxtseo status --site <site-id> --json
|
|
|
107
117
|
```
|
|
108
118
|
|
|
109
119
|
- `available: false` means the first assessment has not run.
|
|
120
|
+
- Read `assessment.scope` before you quote `assessment.summary`. `site` means
|
|
121
|
+
the headline describes the whole Site. `route-family` means it describes one
|
|
122
|
+
Route family: a lead to check, never the Site verdict. `assessment.aggregate`
|
|
123
|
+
is always the whole-Site verdict. If `healthStage` is `quality_rejection`,
|
|
124
|
+
the whole Site is affected, whatever a route family shows.
|
|
125
|
+
`quality_rejection` is a broad indexing stage. It includes discovery gaps.
|
|
126
|
+
Read the summary and Google coverage reasons before blaming content quality.
|
|
127
|
+
It does not prove that Google crawled and rejected the pages.
|
|
128
|
+
The stored assessment and retained indexing summary have separate clocks.
|
|
129
|
+
Compare `assessment.assessedAt` with indexing `asOf` before comparing their counts.
|
|
110
130
|
- Read `dataQuality.status` before you quote the verdict. `degraded` means
|
|
111
131
|
proofs were missing, so the verdict is Provisional. `unavailable` means it
|
|
112
132
|
has not run.
|
|
113
133
|
- If `nextAction.kind` is `action`, pass its `actionId` to step 3.
|
|
134
|
+
- `openCount` counts open and in-progress Actions. `actions list` returns the
|
|
135
|
+
admitted, unclaimed worklist. These counts can differ. A stored Next Action
|
|
136
|
+
can still be readable through `actions show` when the worklist omits it.
|
|
137
|
+
- A `candidate:` finding with diagnosis `Shadow observation` is a canary,
|
|
138
|
+
not a verified repair cause. Inspect its details before proposing work.
|
|
114
139
|
|
|
115
140
|
For the whole Site instead of the next action, run one `pull`. It performs
|
|
116
141
|
every spend-free Site read and writes one envelope per line:
|
|
@@ -119,6 +144,18 @@ every spend-free Site read and writes one envelope per line:
|
|
|
119
144
|
nuxtseo pull --site <site-id> --json > site.ndjson
|
|
120
145
|
```
|
|
121
146
|
|
|
147
|
+
Inspect every line's `command` and `error`. If one read times out, retry that
|
|
148
|
+
read with the suggested timeout. Do not repeat successful or paid reads.
|
|
149
|
+
If the longer request returns a provider failure, report the request ID.
|
|
150
|
+
Retained `page issues` rows can name URLs, but do not replace Google's verdict.
|
|
151
|
+
|
|
152
|
+
For full analytics exports, use `--all --limit 100`. Each run stops after
|
|
153
|
+
50 requests. If it exits with `paging_cap_reached`, preserve the arguments
|
|
154
|
+
and continue with the printed resume argument. Save each run separately.
|
|
155
|
+
Check row keys for repeats across pages. Repeated keyword rows can indicate
|
|
156
|
+
unstable upstream ordering. Report that gap instead of claiming complete
|
|
157
|
+
keyword coverage. Use Site timeseries totals for the Site-wide comparison.
|
|
158
|
+
|
|
122
159
|
**2. Pick a ranked action.**
|
|
123
160
|
|
|
124
161
|
```sh
|
|
@@ -128,6 +165,10 @@ nuxtseo actions list --site <site-id> --json
|
|
|
128
165
|
Keep server order. Check `evidence.freshness` on each row. If `verdict` is
|
|
129
166
|
`aged` with a large `ageHours`, live-check a cheap sample before you fix. The
|
|
130
167
|
Site may have changed since the observation.
|
|
168
|
+
If freshness `confidence` is `reasserted`, its age dates the repeated finding.
|
|
169
|
+
It does not date the underlying page observation. Read evidence group source
|
|
170
|
+
clocks in `actions show`. Live-check affected pages when those clocks are
|
|
171
|
+
unknown or old, even when the finding was recently reasserted.
|
|
131
172
|
|
|
132
173
|
**3. Read the evidence.**
|
|
133
174
|
|
|
@@ -137,7 +178,11 @@ nuxtseo actions show <action-id> --site <site-id> --json
|
|
|
137
178
|
|
|
138
179
|
It names the pages, the finding type, and when it was observed. If
|
|
139
180
|
`data.workPrompt` is not null, it is the Work Prompt: the task for this action,
|
|
140
|
-
grounded in its evidence. Start the fix from it.
|
|
181
|
+
grounded in its evidence. Start the fix from it.
|
|
182
|
+
The Work Prompt serves several clients and may name MCP alternatives.
|
|
183
|
+
Use its CLI commands in this workflow. Never switch to MCP as a fallback.
|
|
184
|
+
|
|
185
|
+
Two reads go deeper:
|
|
141
186
|
|
|
142
187
|
- `page issues --action-id <action-id>`: the raw observations, with URLs,
|
|
143
188
|
status codes, redirect targets, and Lighthouse selectors. This is the
|
|
@@ -170,8 +215,14 @@ from `actions show`, then:
|
|
|
170
215
|
nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --yes --json
|
|
171
216
|
```
|
|
172
217
|
|
|
173
|
-
|
|
174
|
-
410 evidence and no internal referrers.
|
|
218
|
+
Without a note, the server admits a dismiss only for a broken-page action with
|
|
219
|
+
complete 404 or 410 evidence and no internal referrers.
|
|
220
|
+
|
|
221
|
+
If the owner confirms an action is intentional or incorrect, record that
|
|
222
|
+
decision with `--note "<reason>"`. Review the exact scope first. A note dismisses
|
|
223
|
+
the whole action, so never use it to hide a mix of intentional and broken URLs.
|
|
224
|
+
Dismissal keeps the observations and does not verify a fix. The dashboard can
|
|
225
|
+
restore the action later.
|
|
175
226
|
|
|
176
227
|
**7. Mark the day.**
|
|
177
228
|
|
|
@@ -190,7 +241,7 @@ you got:
|
|
|
190
241
|
| --- | --- |
|
|
191
242
|
| `page inspect` | `observations.coverage` |
|
|
192
243
|
| `page issues` | `availableKeys` |
|
|
193
|
-
| `search cohorts` | `analysed: false`: no completed crawl,
|
|
244
|
+
| `search cohorts` | `analysed: false`: no completed crawl, no Google index state, or `reason: "sampled-index-state"` |
|
|
194
245
|
| `vitals summary` | `available: false` |
|
|
195
246
|
| `research keywords`, `research rankings`, `research domain-*`, `page issues`, `search cohorts`, `vitals findings` | `data.message`, and `data.tip` for the repair |
|
|
196
247
|
|
|
@@ -200,15 +251,23 @@ a seed over three words, still exits `0`, and returns `keywords: []` with
|
|
|
200
251
|
|
|
201
252
|
**A refusal is not an empty Site.** Exit `4` is a plan, scope, or entitlement
|
|
202
253
|
blocker, for example `entitlement_required`. Exit `5` on `status`, `page
|
|
203
|
-
issues`, or `search cohorts` can mean
|
|
204
|
-
|
|
205
|
-
|
|
254
|
+
issues`, or `search cohorts` can mean a paused Site.
|
|
255
|
+
Removed Sites return `not_found` with exit `8`. Report the blocker and stop.
|
|
256
|
+
Do not read it as "no data" or "nothing wrong". Do not retry unchanged,
|
|
257
|
+
and do not route around it with `pull` or another read.
|
|
206
258
|
|
|
207
259
|
**Quote the count the result ships.** Never quote the length of a list. Use
|
|
208
260
|
`data.total` from `page issues`, and `coverage.accessibilityChecksTotal` and
|
|
209
261
|
`coverage.bestPracticesChecksTotal` from `scans show`. `possiblyTruncated:
|
|
210
262
|
true` marks the list as a floor.
|
|
211
263
|
|
|
264
|
+
For `search indexing urls`, read `totalSemantics` before quoting `total`.
|
|
265
|
+
`lower-bound` means the provider used lookahead instead of counting every URL.
|
|
266
|
+
The total can rise between pages without a filter change. Follow `hasMore` or
|
|
267
|
+
use `--all`. A nonempty terminal page reports `exact` for that retained URL population.
|
|
268
|
+
An empty page beyond offset zero reports `unknown`; its total can equal the requested offset.
|
|
269
|
+
Do not compare this population with a summary or cohort from another capture.
|
|
270
|
+
|
|
212
271
|
**Keep these datasets apart:**
|
|
213
272
|
|
|
214
273
|
- `performance` and `scans *` read Lighthouse **lab** Scans. `vitals summary`
|
|
@@ -224,20 +283,40 @@ true` marks the list as a floor.
|
|
|
224
283
|
`search inspect` read retained Search Console evidence.
|
|
225
284
|
- An indexed count comes from `search indexing summary`, never from analytics
|
|
226
285
|
rows. It counts retained URL Inspection evidence as of `data.asOf`, not
|
|
227
|
-
Google's live index. Name that source, quote `asOf
|
|
228
|
-
figure current or "right now",
|
|
286
|
+
Google's live index. Name that source, quote `asOf` with `oldestCheckAt`
|
|
287
|
+
(the evidence range), and never label the figure current or "right now",
|
|
288
|
+
even in a summary line. See
|
|
229
289
|
[references/indexing.md](references/indexing.md).
|
|
230
290
|
|
|
231
291
|
**Route families: `established` versus `ranked`.** Read `search cohorts`
|
|
232
|
-
before you list single not-indexed URLs.
|
|
292
|
+
before you list single not-indexed URLs. If it returns `analysed: false` with
|
|
293
|
+
`reason: "sampled-index-state"`, the not-indexed list is a capped sample of the
|
|
294
|
+
count Search Console reports. No family rate exists. Read `search indexing
|
|
295
|
+
summary` instead, and never infer that unlisted URLs are indexed. Never merge
|
|
296
|
+
its two lists:
|
|
233
297
|
|
|
234
298
|
- `established` holds only families whose not-indexed rate is significant
|
|
235
299
|
against the rest of the Site, after a Bonferroni correction. Only these rows
|
|
236
300
|
are a finding or a cause.
|
|
301
|
+
- `coverage.notIndexedNotInCrawl` counts not-indexed URLs that the Nuxt SEO
|
|
302
|
+
crawl did not reach. It does not count pages Google never crawled.
|
|
237
303
|
- `ranked` orders every family by raw rate, with no significance test. A row
|
|
238
304
|
with `statisticallyEstablished: false` is a lead to verify, for example by
|
|
239
305
|
inspecting its URLs. Presenting it as a cause is a hard failure.
|
|
240
306
|
|
|
307
|
+
**Compare equal windows.** Read `search analytics timeseries` for the Site's
|
|
308
|
+
current and previous totals. `audit content-decay` ships its own `window`.
|
|
309
|
+
It may span three months even when `pull --period 28d` selects 28-day analytics.
|
|
310
|
+
Quote that window separately. Never present its losses as the 28-day decline.
|
|
311
|
+
Content decay and duplicate reports return fixed shortlists. If `total` exceeds the returned
|
|
312
|
+
row count, disclose both counts, even when `page.hasMore` is false. That flag
|
|
313
|
+
does not promise the shortlist includes every Finding.
|
|
314
|
+
|
|
315
|
+
**Check alternate formats.** A `.md` URL can serve the HTML page's content
|
|
316
|
+
with a `Link: <html-url>; rel="canonical"` response header. Compare HTML
|
|
317
|
+
siblings with HTML siblings. Do not remove a valid header canonical or add
|
|
318
|
+
`noindex` just to clear a missing HTML canonical warning.
|
|
319
|
+
|
|
241
320
|
## Guardrails
|
|
242
321
|
|
|
243
322
|
- **Get consent before a mutation.** `page scan`, `actions resolve`, `actions
|
|
@@ -120,19 +120,19 @@ 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.
|
|
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 |
|
|
127
|
-
| `actions dismiss <action-id>` | Mutation.
|
|
127
|
+
| `actions dismiss <action-id>` | Mutation. Needs `--artifact-version`, read from `actions show`. `--note` records why the owner considers the action intentional or incorrect. Without a note, complete unlinked 404 or 410 evidence is required. Dismissal preserves observations and never verifies a fix |
|
|
128
128
|
|
|
129
129
|
## Pages, Scans, and lab performance
|
|
130
130
|
|
|
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.
|
|
135
|
-
| `page scan <url>` | Mutation.
|
|
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
|
+
| `page scan <url>` | Mutation. Adds or re-enables the URL as a monitored Page, then starts mobile and desktop Scans |
|
|
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) |
|
|
138
138
|
| `scans show <scan-id>` | One Scan, and the evidence behind its scores: wasteful assets, repeat-visit cache policy, third-party scripts, the LCP element, and every failing accessibility and best-practices check with its CSS selectors and DOM snippets. Read `coverage` before quoting a total |
|
|
@@ -141,6 +141,17 @@ lines.
|
|
|
141
141
|
`page inspect` and `page scan` take an absolute URL, for example
|
|
142
142
|
`https://example.com/about`.
|
|
143
143
|
|
|
144
|
+
`page scan` enables recurring monitoring.
|
|
145
|
+
Read `scans pages` before scanning many new URLs. The server refuses a new
|
|
146
|
+
Page when five monitored Pages are already enabled. That limit comes from
|
|
147
|
+
the Page scan service, not the Team plan.
|
|
148
|
+
|
|
149
|
+
`pageCreated: true` means a monitored Page was created. The URL may already
|
|
150
|
+
have retained Scans. Automatic homepage assessment Scans use a separate
|
|
151
|
+
slot without a monitored Page. A manual homepage Scan can therefore create
|
|
152
|
+
a monitored Page while older homepage assessment Scans exist.
|
|
153
|
+
The monitored Page then follows its own recurring schedule.
|
|
154
|
+
|
|
144
155
|
## Field Core Web Vitals
|
|
145
156
|
|
|
146
157
|
| Command | What it returns, and its flags |
|
|
@@ -158,7 +169,7 @@ lines.
|
|
|
158
169
|
| `search indexing <summary\|urls>` | Retained URL Inspection coverage. `summary` returns indexed counts. `urls` supports `--issue`, `--status`, `--limit 1..500` (default 50), `--offset` |
|
|
159
170
|
| `search index-history` | Retained indexing changes and candidate causes. `--url`, `--field`, `--days 1..730`. Dates a regression against a release |
|
|
160
171
|
| `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).
|
|
172
|
+
| `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
173
|
| `sitemaps list` | Search Console sitemap snapshot: errors, warnings, URL counts, sync status |
|
|
163
174
|
| `sitemaps urls` | Sitemap URL membership. `--generation-id`, `--feedpath`, `--cursor`, `--limit 1..1000` |
|
|
164
175
|
| `sitemaps submit <sitemap-url>` | Mutation. Submits one sitemap through the Site's stored Search Console credential |
|
|
@@ -177,23 +188,33 @@ different numbers. Read the one that answers your question.
|
|
|
177
188
|
| Field | What it counts |
|
|
178
189
|
| --- | --- |
|
|
179
190
|
| `total` | The number of rows the query matched, not clicks or impressions |
|
|
180
|
-
| `totals` |
|
|
191
|
+
| `totals` | Upstream period totals from a separate read without grouping dimensions. These can include anonymized queries. Read `totalsScope`. `null` means unavailable |
|
|
181
192
|
| `representedTotals` | Clicks and impressions for the rows in this response only, after local filtering. `representedTotalsScope` says so in words |
|
|
182
193
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
`
|
|
194
|
+
Raising `--limit` changes `representedTotals` while `totals` covers the full window.
|
|
195
|
+
Grouped Page impressions can exceed whole-Site impressions when several Pages appear in one search.
|
|
196
|
+
Do not assume `representedTotals` is at or below `totals`.
|
|
197
|
+
Use `totals` for traffic totals, and `representedTotals` for the returned rows.
|
|
198
|
+
Neither row filters nor pagination make `totals` a sum of the returned rows.
|
|
199
|
+
|
|
200
|
+
Keyword lists omit `site:` and `inurl:` search operators after upstream paging.
|
|
201
|
+
`total` and `page` still count upstream rows.
|
|
202
|
+
Even `--all` can return fewer keyword rows than `total`.
|
|
186
203
|
|
|
187
|
-
###
|
|
204
|
+
### Read keyword coverage separately
|
|
188
205
|
|
|
189
|
-
Google suppresses query rows below a privacy threshold.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
always at or below page-dimension totals for the same Site and period. Never
|
|
193
|
-
compare the two, and never report one as the Site total.
|
|
206
|
+
Google suppresses query rows below a privacy threshold.
|
|
207
|
+
Keyword rows therefore omit some traffic, even after `--all`.
|
|
208
|
+
Their `representedTotals` measure only the returned keywords.
|
|
194
209
|
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
The engine can read dimensionless totals that include anonymized traffic.
|
|
211
|
+
Then `keywords.totals` can equal `pages.totals` for the same Site and period.
|
|
212
|
+
Do not assume `keywords.totals` is the sum of visible keyword rows.
|
|
213
|
+
|
|
214
|
+
Read `timeseries.totals` for whole-Site traffic, `pages` rows for landing pages,
|
|
215
|
+
and `keywords` rows for visible terms. Use `prevTotals` to compare Site traffic
|
|
216
|
+
across periods.
|
|
217
|
+
A missing query row is never evidence of no demand.
|
|
197
218
|
|
|
198
219
|
## Web Analytics
|
|
199
220
|
|
|
@@ -215,9 +236,9 @@ research boundary in SKILL.md first.
|
|
|
215
236
|
| `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
237
|
| `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
238
|
| `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 |
|
|
239
|
+
| `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 |
|
|
240
|
+
| `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 |
|
|
241
|
+
| `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
242
|
| `backlinks history` | A monthly inbound link series. `--from YYYY-MM-DD`. Defaults to about twelve months. Needs a Site URL |
|
|
222
243
|
| `backlinks recoverable` | Stored recoverable backlinks. `--limit 1..200`, `--offset`. Reads retained rows |
|
|
223
244
|
| `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 +275,21 @@ nuxtseo actions list --help --json
|
|
|
254
275
|
|
|
255
276
|
Use it instead of guessing a flag. Prefer the tables above for anything they
|
|
256
277
|
already answer.
|
|
278
|
+
|
|
279
|
+
### Claim response
|
|
280
|
+
|
|
281
|
+
`actions resolve` saves your Claim. It returns before a requested scan finishes starting.
|
|
282
|
+
`status: "open"` stays unchanged until the engine verifies the Action.
|
|
283
|
+
Human output shows `Claimed, verifying` and the suggested check-back time.
|
|
284
|
+
Claimed Actions leave `actions list`. Use `actions show <action-id>` to check them.
|
|
285
|
+
|
|
286
|
+
Both `resolve` and `show` return these additive fields:
|
|
287
|
+
|
|
288
|
+
| Field | Meaning |
|
|
289
|
+
| --- | --- |
|
|
290
|
+
| `markedAt` | ISO timestamp when your Claim was saved, or `null` before a Claim. |
|
|
291
|
+
| `checkBackAt` | Suggested ISO timestamp to check again, or `null` when no automated check applies. |
|
|
292
|
+
|
|
293
|
+
A check-back time does not promise a completed check.
|
|
294
|
+
`scan-queued` can mean a re-assessment using scheduled crawl results, without a new scan.
|
|
295
|
+
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`,
|
|
15
|
-
the retained URL Inspection evidence
|
|
16
|
-
|
|
17
|
-
`data.
|
|
18
|
-
|
|
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
|