@nuxtseo/cli 0.5.7 → 0.5.9
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/cli.js +1 -1
- package/dist/commands.js +73 -10
- package/dist/render.d.ts +5 -2
- package/dist/render.js +95 -16
- package/package.json +3 -3
- package/skills/nuxtseo-cli/SKILL.md +15 -11
- package/skills/nuxtseo-cli/references/commands.md +21 -5
- package/skills/nuxtseo-cli/references/indexing.md +16 -1
- package/skills/nuxtseo-cli/references/protocol.md +3 -0
package/dist/cli.js
CHANGED
|
@@ -376,7 +376,7 @@ async function interactiveBare(runtime, globals) {
|
|
|
376
376
|
message: 'Account and setup',
|
|
377
377
|
options: [
|
|
378
378
|
{ value: 'site', label: 'Switch Site', hint: config.value.siteId ?? 'none selected' },
|
|
379
|
-
{ value: 'usage', label: '
|
|
379
|
+
{ value: 'usage', label: 'Team usage' },
|
|
380
380
|
{ value: 'whoami', label: 'Credential and scopes' },
|
|
381
381
|
{ value: 'config', label: 'Configuration' },
|
|
382
382
|
{ value: 'logout', label: 'Log out' },
|
package/dist/commands.js
CHANGED
|
@@ -9,7 +9,7 @@ import { EXIT_CODE, fail, fromSdkFailure, ok } from './failures.js';
|
|
|
9
9
|
import { awaitPairingApproval, createCliPairingClient, startPairing } from './pairing.js';
|
|
10
10
|
import { parseAbsolutePageUrl, parseChoice, parseInteger, parsePageUrlOrPath, parseRatio } from './parse.js';
|
|
11
11
|
import { PULL_PERIODS, runPull } from './pull.js';
|
|
12
|
-
import { renderAccountToken, renderAction, renderActionDismissal, renderActionResolution, renderActions, renderAnalytics, renderAnnotation, renderAnnotationDeleted, renderAnnotations, renderAuditChanges, renderBacklinkAnchors, renderBacklinksHistory, renderBacklinksSummary, renderContentDecay, renderDomainAvailability, renderDomainTraffic, renderDuplicateClusters, renderFieldVitalFindings, renderFieldVitals, renderIndexCohorts, renderIndexingDiagnostics, renderIndexingHistory, renderKeywordResearch, renderLinkOpportunities, renderLinkStructure, renderMentions, renderMonitoredPages, renderPageInspection, renderPageIssues, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderReferringDomains, renderResearchOverview, renderScanDetail, renderScans, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSitemapAction, renderSitemaps, renderSitemapUrls, renderSites, renderSiteStatus, renderTimeline, renderUrlInspection, renderUrlInspectionRefresh, renderUsage, } from './render.js';
|
|
12
|
+
import { renderAccountToken, renderAction, renderActionDismissal, renderActionResolution, renderActions, renderAnalytics, renderAnnotation, renderAnnotationDeleted, renderAnnotations, renderAuditChanges, renderBacklinkAnchors, renderBacklinksHistory, renderBacklinksSummary, renderContentDecay, renderDomainAvailability, renderDomainTraffic, renderDuplicateClusters, renderFieldVitalFindings, renderFieldVitals, renderIndexCohorts, renderIndexingDiagnostics, renderIndexingHistory, renderIndexingWatches, renderKeywordResearch, renderLinkOpportunities, renderLinkStructure, renderMentions, renderMonitoredPages, renderPageInspection, renderPageIssues, renderPageScan, renderPerformance, renderRankingsResearch, renderRecoverableBacklinks, renderReferringDomains, renderResearchOverview, renderScanDetail, renderScans, renderSearchAnalytics, renderSearchStatus, renderSerpResearch, renderSitemapAction, renderSitemapEvidence, renderSitemaps, renderSitemapSubmission, renderSitemapUrls, renderSites, renderSiteStatus, renderTimeline, renderUrlInspection, renderUrlInspectionRefresh, renderUsage, } from './render.js';
|
|
13
13
|
import { writeCliResponse, writeDiagnostic, writeOutput, writeProtocolResponse } from './runtime.js';
|
|
14
14
|
import { resolveSite } from './site.js';
|
|
15
15
|
import { installSkill, SKILL_AGENTS } from './skill.js';
|
|
@@ -1010,6 +1010,54 @@ async function sitemapsList(runtime, globals) {
|
|
|
1010
1010
|
}, { signal: runtime.requestSignal }));
|
|
1011
1011
|
return present(runtime, globals, response, value => renderSitemaps(value.data));
|
|
1012
1012
|
}
|
|
1013
|
+
async function sitemapsSubmission(runtime, globals) {
|
|
1014
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1015
|
+
if (resolved._tag === 'Err')
|
|
1016
|
+
return resolved;
|
|
1017
|
+
const response = await withSpinner(runtime, 'Loading Sitemap submission', () => resolved.value.api.client.sitemaps.readSubmission({
|
|
1018
|
+
params: { siteId: resolved.value.siteId },
|
|
1019
|
+
}, { signal: runtime.requestSignal }));
|
|
1020
|
+
return present(runtime, globals, response, value => renderSitemapSubmission(value.data));
|
|
1021
|
+
}
|
|
1022
|
+
async function searchWatches(runtime, globals) {
|
|
1023
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1024
|
+
if (resolved._tag === 'Err')
|
|
1025
|
+
return resolved;
|
|
1026
|
+
const response = await withSpinner(runtime, 'Loading Watched URLs', () => resolved.value.api.client.search.listWatches({
|
|
1027
|
+
params: { siteId: resolved.value.siteId },
|
|
1028
|
+
}, { signal: runtime.requestSignal }));
|
|
1029
|
+
return present(runtime, globals, response, value => renderIndexingWatches(value.data));
|
|
1030
|
+
}
|
|
1031
|
+
async function sitemapsInspect(runtime, globals, rawUrl, engine) {
|
|
1032
|
+
const url = parseAbsolutePageUrl(rawUrl);
|
|
1033
|
+
if (url._tag === 'Err')
|
|
1034
|
+
return url;
|
|
1035
|
+
const resolved = await apiAndSite(runtime, globals);
|
|
1036
|
+
if (resolved._tag === 'Err')
|
|
1037
|
+
return resolved;
|
|
1038
|
+
const response = await withSpinner(runtime, 'Reading Sitemap evidence', () => resolved.value.api.client.sitemaps.inspect({
|
|
1039
|
+
params: { siteId: resolved.value.siteId },
|
|
1040
|
+
query: { url: url.value, searchEngine: engine },
|
|
1041
|
+
}, { signal: runtime.requestSignal }));
|
|
1042
|
+
if (response._tag === 'Err')
|
|
1043
|
+
return fromSdkFailure(response.error);
|
|
1044
|
+
if (response.value.data.state._tag === 'unavailable') {
|
|
1045
|
+
const { reason } = response.value.data.state;
|
|
1046
|
+
const exitCode = reason === 'throttled' || reason === 'provider-unavailable'
|
|
1047
|
+
? EXIT_CODE.retryable
|
|
1048
|
+
: reason === 'grant-missing' || reason === 'reauthorization-required'
|
|
1049
|
+
? EXIT_CODE.authentication
|
|
1050
|
+
: reason === 'permission-lost' || reason === 'not-enabled'
|
|
1051
|
+
? EXIT_CODE.authorization
|
|
1052
|
+
: EXIT_CODE.conflict;
|
|
1053
|
+
const incomplete = fail(exitCode, `Sitemap evidence is unavailable: ${reason}.`);
|
|
1054
|
+
if (globals.json)
|
|
1055
|
+
return { _tag: 'Err', error: { ...incomplete.error, protocolResponse: response.value } };
|
|
1056
|
+
writeOutput(runtime, renderSitemapEvidence(response.value.data));
|
|
1057
|
+
return incomplete;
|
|
1058
|
+
}
|
|
1059
|
+
return present(runtime, globals, response, value => renderSitemapEvidence(value.data));
|
|
1060
|
+
}
|
|
1013
1061
|
async function sitemapsUrls(runtime, globals, args) {
|
|
1014
1062
|
const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 1000, defaultValue: 500 });
|
|
1015
1063
|
if (limit._tag === 'Err')
|
|
@@ -1326,11 +1374,10 @@ async function actionDismiss(runtime, globals, args) {
|
|
|
1326
1374
|
return fail(EXIT_CODE.invalidInput, 'Pass the action id to dismiss.', undefined, 'invalid_cli_input');
|
|
1327
1375
|
if (!args.artifactVersion)
|
|
1328
1376
|
return fail(EXIT_CODE.invalidInput, 'Pass --artifact-version, the artifactVersion returned with this action.', undefined, 'invalid_cli_input');
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
: `Dismiss action ${args.actionId}? The page stays removed.`);
|
|
1377
|
+
const note = args.note?.trim();
|
|
1378
|
+
if (!note || note.length > 2000)
|
|
1379
|
+
return fail(EXIT_CODE.invalidInput, 'Pass --note with 1 to 2000 characters that say why this action is intentional or incorrect.', undefined, 'invalid_cli_input');
|
|
1380
|
+
const confirmed = await confirmMutation(runtime, globals, `Dismiss action ${args.actionId}? Your note records this decision. No fix is verified.`);
|
|
1334
1381
|
if (confirmed._tag === 'Err')
|
|
1335
1382
|
return confirmed;
|
|
1336
1383
|
if (!confirmed.value) {
|
|
@@ -1342,7 +1389,7 @@ async function actionDismiss(runtime, globals, args) {
|
|
|
1342
1389
|
return resolved;
|
|
1343
1390
|
const response = await withSpinner(runtime, 'Dismissing action', () => resolved.value.api.client.actions.dismiss({
|
|
1344
1391
|
params: { siteId: resolved.value.siteId, actionId: args.actionId },
|
|
1345
|
-
body: { artifactVersion: args.artifactVersion,
|
|
1392
|
+
body: { artifactVersion: args.artifactVersion, note },
|
|
1346
1393
|
}, { signal: runtime.requestSignal }));
|
|
1347
1394
|
return present(runtime, globals, response, value => renderActionDismissal(value.data));
|
|
1348
1395
|
}
|
|
@@ -1666,11 +1713,11 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
1666
1713
|
run: ({ args }) => capture(execution, () => actionsResolve(runtime, globals, args.actionId), args._, 1)(),
|
|
1667
1714
|
}),
|
|
1668
1715
|
dismiss: defineCommand({
|
|
1669
|
-
meta: { name: 'dismiss', description: 'Dismiss reviewed work with a note
|
|
1716
|
+
meta: { name: 'dismiss', description: 'Dismiss reviewed work with a note that says why' },
|
|
1670
1717
|
args: {
|
|
1671
1718
|
'actionId': positional('action-id', 'Action ID'),
|
|
1672
1719
|
'artifact-version': { type: 'string', description: 'The artifactVersion returned with this action' },
|
|
1673
|
-
'note': { type: 'string', description: 'Why this action is intentional or incorrect, 1 to 2000 characters' },
|
|
1720
|
+
'note': { type: 'string', description: 'Required. Why this action is intentional or incorrect, 1 to 2000 characters' },
|
|
1674
1721
|
},
|
|
1675
1722
|
run: ({ args }) => capture(execution, () => actionDismiss(runtime, globals, {
|
|
1676
1723
|
actionId: args.actionId,
|
|
@@ -2042,6 +2089,10 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
2042
2089
|
const search = defineCommand({
|
|
2043
2090
|
meta: { name: 'search', description: 'Read Search Console state' },
|
|
2044
2091
|
subCommands: {
|
|
2092
|
+
'watches': defineCommand({
|
|
2093
|
+
meta: { name: 'watches', description: 'Read Watched URLs and retained Checkpoints' },
|
|
2094
|
+
run: ({ args }) => capture(execution, () => searchWatches(runtime, globals), args._, 0)(),
|
|
2095
|
+
}),
|
|
2045
2096
|
'status': defineCommand({
|
|
2046
2097
|
meta: { name: 'status', description: 'Read the stored Search Console connection' },
|
|
2047
2098
|
run: ({ args }) => capture(execution, () => searchStatus(runtime, globals), args._, 0)(),
|
|
@@ -2149,6 +2200,18 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
2149
2200
|
const sitemaps = defineCommand({
|
|
2150
2201
|
meta: { name: 'sitemaps', description: 'Read and manage Search Console sitemaps' },
|
|
2151
2202
|
subCommands: {
|
|
2203
|
+
submission: defineCommand({
|
|
2204
|
+
meta: { name: 'submission', description: 'Read site-level Google Sitemap submission readiness' },
|
|
2205
|
+
run: ({ args }) => capture(execution, () => sitemapsSubmission(runtime, globals), args._, 0)(),
|
|
2206
|
+
}),
|
|
2207
|
+
inspect: defineCommand({
|
|
2208
|
+
meta: { name: 'inspect', description: 'Read exact Sitemap listing evidence' },
|
|
2209
|
+
args: {
|
|
2210
|
+
url: positional('sitemap-url', 'Absolute Sitemap URL'),
|
|
2211
|
+
engine: { type: 'enum', options: ['google', 'bing'], default: 'google', description: 'Google live or Bing stored evidence' },
|
|
2212
|
+
},
|
|
2213
|
+
run: ({ args }) => capture(execution, () => sitemapsInspect(runtime, globals, args.url, args.engine), args._, 1)(),
|
|
2214
|
+
}),
|
|
2152
2215
|
list: defineCommand({
|
|
2153
2216
|
meta: { name: 'list', description: 'List the sitemaps Search Console knows' },
|
|
2154
2217
|
run: ({ args }) => capture(execution, () => sitemapsList(runtime, globals), args._, 0)(),
|
|
@@ -2305,7 +2368,7 @@ export function createRootCommand(runtime, globals, execution) {
|
|
|
2305
2368
|
},
|
|
2306
2369
|
}),
|
|
2307
2370
|
usage: defineCommand({
|
|
2308
|
-
meta: { name: 'usage', description: 'Read
|
|
2371
|
+
meta: { name: 'usage', description: 'Read Team usage' },
|
|
2309
2372
|
args: {
|
|
2310
2373
|
group: {
|
|
2311
2374
|
type: 'enum',
|
package/dist/render.d.ts
CHANGED
|
@@ -4,11 +4,11 @@ import type { ActionDismiss, ActionList, ActionResolve, ActionShow } from '@nuxt
|
|
|
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
6
|
import type { BacklinkAnchorsData, BacklinksHistory, BacklinksSummaryData, MentionsData, RecoverableBacklinksData, ReferringDomainsData } from '@nuxtseo/protocol/v1/backlinks';
|
|
7
|
-
import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, SearchAnalyticsData, SearchStatusData, UrlInspectionData, UrlInspectionRefreshData } from '@nuxtseo/protocol/v1/gsc';
|
|
7
|
+
import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, IndexingWatchesData, SearchAnalyticsData, SearchStatusData, UrlInspectionData, UrlInspectionRefreshData } 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';
|
|
10
10
|
import type { DomainAvailability, DomainTraffic, KeywordResearchData, RankingsResearchData, SerpResearchData, StoredResearchOverview } from '@nuxtseo/protocol/v1/research';
|
|
11
|
-
import type { SiteSitemapActionResponse, SiteSitemapsResponse, SiteSitemapUrlsResponse } from '@nuxtseo/protocol/v1/sitemaps';
|
|
11
|
+
import type { SiteSitemapActionResponse, SiteSitemapEvidenceData, SiteSitemapsResponse, SiteSitemapSubmissionData, SiteSitemapUrlsResponse } from '@nuxtseo/protocol/v1/sitemaps';
|
|
12
12
|
import type { Annotation, AnnotationDeleted, Annotations, TimelineEntries } from '@nuxtseo/protocol/v1/timeline';
|
|
13
13
|
export declare function renderSites(response: SitesListResponse, selectedSiteId?: string): string;
|
|
14
14
|
export declare function renderUsage(response: AccountUsageResponse): string;
|
|
@@ -25,6 +25,9 @@ export declare function renderUrlInspectionRefresh(data: UrlInspectionRefreshDat
|
|
|
25
25
|
export declare function renderUrlInspection(data: UrlInspectionData): string;
|
|
26
26
|
export declare function renderAuditChanges(data: AuditChanges): string;
|
|
27
27
|
export declare function renderLinkStructure(data: AuditLinkStructure): string;
|
|
28
|
+
export declare function renderSitemapSubmission(data: SiteSitemapSubmissionData): string;
|
|
29
|
+
export declare function renderSitemapEvidence(data: SiteSitemapEvidenceData): string;
|
|
30
|
+
export declare function renderIndexingWatches(data: IndexingWatchesData): string;
|
|
28
31
|
export declare function renderSitemaps(data: SiteSitemapsResponse['data']): string;
|
|
29
32
|
export declare function renderSitemapUrls(data: SiteSitemapUrlsResponse['data']): string;
|
|
30
33
|
export declare function renderSitemapAction(data: SiteSitemapActionResponse['data']): string;
|
package/dist/render.js
CHANGED
|
@@ -106,20 +106,56 @@ export function renderPageScan(data) {
|
|
|
106
106
|
...data.scans.map(scan => `${scan.strategy}: ${scan.scanId}`),
|
|
107
107
|
].join('\n');
|
|
108
108
|
}
|
|
109
|
+
/** A lab duration in milliseconds. Null is unmeasured, never zero. */
|
|
110
|
+
function labMs(value) {
|
|
111
|
+
return value === null ? 'not measured' : `${value}ms`;
|
|
112
|
+
}
|
|
113
|
+
function labScanLine(label, scan) {
|
|
114
|
+
if (!scan?.completedAt)
|
|
115
|
+
return `${label} no complete Scan`;
|
|
116
|
+
return `${label} perf ${scan.perf ?? 'none'} LCP ${labMs(scan.lcp)} TBT ${labMs(scan.tbt)} CLS ${scan.cls ?? 'not measured'} scanned ${scan.completedAt}`;
|
|
117
|
+
}
|
|
109
118
|
export function renderPerformance(data) {
|
|
110
|
-
|
|
119
|
+
const blocks = [];
|
|
120
|
+
if (data.health) {
|
|
121
|
+
blocks.push([
|
|
122
|
+
'Mobile medians across monitored Pages',
|
|
123
|
+
fields([
|
|
124
|
+
['Scanned pages', data.health.scannedPageCount],
|
|
125
|
+
['Last scan', data.health.lastScanAt],
|
|
126
|
+
['Performance median', data.health.perf.median],
|
|
127
|
+
['Performance worst', data.health.perf.worst],
|
|
128
|
+
['Accessibility median', data.health.a11y.median],
|
|
129
|
+
['SEO median', data.health.seo.median],
|
|
130
|
+
['LCP median', data.health.lcp.median],
|
|
131
|
+
['TBT median', data.health.tbt.median],
|
|
132
|
+
['CLS median', data.health.cls.median],
|
|
133
|
+
]),
|
|
134
|
+
].join('\n'));
|
|
135
|
+
}
|
|
136
|
+
if (data.homepage || data.homepageDesktop) {
|
|
137
|
+
blocks.push([
|
|
138
|
+
'Homepage',
|
|
139
|
+
labScanLine(' mobile ', data.homepage),
|
|
140
|
+
labScanLine(' desktop', data.homepageDesktop),
|
|
141
|
+
].join('\n'));
|
|
142
|
+
}
|
|
143
|
+
if (data.routes.length || data.routesDesktop.length) {
|
|
144
|
+
const desktopByPage = new Map(data.routesDesktop.map(route => [route.pageId, route]));
|
|
145
|
+
const mobileByPage = new Map(data.routes.map(route => [route.pageId, route]));
|
|
146
|
+
const pages = [...data.routes, ...data.routesDesktop.filter(route => !mobileByPage.has(route.pageId))];
|
|
147
|
+
blocks.push([
|
|
148
|
+
`Monitored Pages (${pages.length})`,
|
|
149
|
+
...pages.flatMap(page => [
|
|
150
|
+
` ${page.path}`,
|
|
151
|
+
labScanLine(' mobile ', mobileByPage.get(page.pageId)),
|
|
152
|
+
labScanLine(' desktop', desktopByPage.get(page.pageId)),
|
|
153
|
+
]),
|
|
154
|
+
].join('\n'));
|
|
155
|
+
}
|
|
156
|
+
if (!blocks.length)
|
|
111
157
|
return `No stored performance overview. Monitored routes: ${data.routes.length}.`;
|
|
112
|
-
return
|
|
113
|
-
['Scanned pages', data.health.scannedPageCount],
|
|
114
|
-
['Last scan', data.health.lastScanAt],
|
|
115
|
-
['Performance median', data.health.perf.median],
|
|
116
|
-
['Performance worst', data.health.perf.worst],
|
|
117
|
-
['Accessibility median', data.health.a11y.median],
|
|
118
|
-
['SEO median', data.health.seo.median],
|
|
119
|
-
['LCP median', data.health.lcp.median],
|
|
120
|
-
['TBT median', data.health.tbt.median],
|
|
121
|
-
['CLS median', data.health.cls.median],
|
|
122
|
-
]);
|
|
158
|
+
return blocks.join('\n\n');
|
|
123
159
|
}
|
|
124
160
|
export function renderSearchStatus(data) {
|
|
125
161
|
if (data._tag === 'Connected')
|
|
@@ -348,6 +384,23 @@ export function renderLinkStructure(data) {
|
|
|
348
384
|
];
|
|
349
385
|
return [header, ...slots, ...deadEnds, ...anchors].join('\n');
|
|
350
386
|
}
|
|
387
|
+
export function renderSitemapSubmission(data) {
|
|
388
|
+
return `Google Sitemap submission: ${data.state._tag}\n${JSON.stringify(data.state, null, 2)}\nCan act: ${data.callerCanAct}`;
|
|
389
|
+
}
|
|
390
|
+
export function renderSitemapEvidence(data) {
|
|
391
|
+
return [
|
|
392
|
+
`${data.searchEngine} Sitemap: ${data.sitemapUrl}`,
|
|
393
|
+
`State: ${data.state._tag}`,
|
|
394
|
+
data.capture._tag === 'captured' ? `Capture: ${data.capture.source} at ${data.capture.capturedAt}` : 'Capture: unavailable',
|
|
395
|
+
JSON.stringify(data.state, null, 2),
|
|
396
|
+
].join('\n');
|
|
397
|
+
}
|
|
398
|
+
export function renderIndexingWatches(data) {
|
|
399
|
+
return [
|
|
400
|
+
`Watched URLs: ${data.watched.length}/${data.limit}. Cadence: ${data.cadenceDays} days.`,
|
|
401
|
+
...data.watched.map(watch => `${watch.url}\n Due: ${watch.dueAt}\n Checkpoints: ${JSON.stringify(watch.checkpoints)}`),
|
|
402
|
+
].join('\n');
|
|
403
|
+
}
|
|
351
404
|
export function renderSitemaps(data) {
|
|
352
405
|
if (data.sitemaps.length === 0)
|
|
353
406
|
return `No sitemaps are known for ${data.meta.siteUrl}.`;
|
|
@@ -652,8 +705,9 @@ export function renderFieldVitalFindings(data) {
|
|
|
652
705
|
if (data.findings.length === 0)
|
|
653
706
|
return data.message ?? 'No failing field vitals attributed to an element.';
|
|
654
707
|
const shown = `${data.offset + 1}-${data.offset + data.findings.length} of ${data.total}`;
|
|
708
|
+
const window = data.window ? `, ${data.window.from} to ${data.window.to}` : '';
|
|
655
709
|
return [
|
|
656
|
-
`Field vital findings (${shown}, source ${data.source ?? 'unknown'})`,
|
|
710
|
+
`Field vital findings (${shown}, source ${data.source ?? 'unknown'}${window})`,
|
|
657
711
|
...data.findings.map(finding => [
|
|
658
712
|
`${finding.metric.toUpperCase()} ${finding.severity} ${vitalValue(finding.metric, finding.p75)} ${finding.route}`,
|
|
659
713
|
` selector ${finding.selector}`,
|
|
@@ -881,6 +935,12 @@ export function renderPageIssues(data) {
|
|
|
881
935
|
].filter(Boolean).join('\n')),
|
|
882
936
|
].join('\n') + truncated;
|
|
883
937
|
}
|
|
938
|
+
/** A failed Scan names its kind and message. Other states carry no failure row. */
|
|
939
|
+
function scanFailureRows(scan) {
|
|
940
|
+
if (scan.status !== 'failed')
|
|
941
|
+
return [];
|
|
942
|
+
return [['failure', `${scan.failureKind ?? 'unknown kind'}: ${scan.error ?? 'no message recorded'}`]];
|
|
943
|
+
}
|
|
884
944
|
export function renderScans(data) {
|
|
885
945
|
if (data.scans.length === 0)
|
|
886
946
|
return 'No Lighthouse Scans retained for this Site.';
|
|
@@ -890,6 +950,9 @@ export function renderScans(data) {
|
|
|
890
950
|
`${scan.completedAt ?? 'pending'} ${scan.strategy} ${scan.status} ${scan.url}`,
|
|
891
951
|
` ${fields([
|
|
892
952
|
['id', scan.id],
|
|
953
|
+
['trigger', scan.trigger],
|
|
954
|
+
['lighthouse', scan.lighthouseVersion],
|
|
955
|
+
...scanFailureRows(scan),
|
|
893
956
|
['performance', scan.scores.perf],
|
|
894
957
|
['accessibility', scan.scores.a11y],
|
|
895
958
|
['best practices', scan.scores.bestPractices],
|
|
@@ -897,11 +960,20 @@ export function renderScans(data) {
|
|
|
897
960
|
['lcp', scan.lcp],
|
|
898
961
|
['tbt', scan.tbt],
|
|
899
962
|
['cls', scan.cls],
|
|
900
|
-
['error', scan.error],
|
|
901
963
|
]).split('\n').join('\n ')}`,
|
|
902
964
|
].join('\n')),
|
|
903
965
|
].join('\n');
|
|
904
966
|
}
|
|
967
|
+
function renderScanComparison(comparison) {
|
|
968
|
+
if (!comparison || comparison._tag === 'none')
|
|
969
|
+
return 'Previous Scan none compared';
|
|
970
|
+
if (comparison._tag === 'incompatible')
|
|
971
|
+
return `Previous Scan ${comparison.previousScanId ?? 'unknown'} not comparable: ${comparison.reasons.join(', ')}`;
|
|
972
|
+
return [
|
|
973
|
+
`Since previous Scan ${comparison.previousScanId ?? 'unknown'} ${comparison.severity}`,
|
|
974
|
+
...comparison.changes.map(change => ` ${change.metric} ${change.previous} to ${change.current} ${change.delta > 0 ? '+' : ''}${change.delta} ${change.severity}`),
|
|
975
|
+
].join('\n');
|
|
976
|
+
}
|
|
905
977
|
export function renderMonitoredPages(data) {
|
|
906
978
|
if (data.pages.length === 0)
|
|
907
979
|
return 'No monitored Pages. Start one with `nuxtseo page scan <url>`.';
|
|
@@ -917,6 +989,9 @@ export function renderScanDetail(data) {
|
|
|
917
989
|
['URL', data.url],
|
|
918
990
|
['Strategy', data.strategy],
|
|
919
991
|
['Status', data.status],
|
|
992
|
+
...scanFailureRows(data).map(([, failure]) => ['Failure', failure]),
|
|
993
|
+
['Trigger', data.trigger],
|
|
994
|
+
['Lighthouse', data.lighthouseVersion],
|
|
920
995
|
['Completed', data.completedAt],
|
|
921
996
|
['Performance', data.scores.perf],
|
|
922
997
|
['Accessibility', data.scores.a11y],
|
|
@@ -930,6 +1005,8 @@ export function renderScanDetail(data) {
|
|
|
930
1005
|
['TTFB', data.ttfb],
|
|
931
1006
|
]),
|
|
932
1007
|
];
|
|
1008
|
+
if (data.status === 'complete')
|
|
1009
|
+
blocks.push(renderScanComparison(data.comparison));
|
|
933
1010
|
if (data.lcpElement)
|
|
934
1011
|
blocks.push(`LCP element ${data.lcpElement.selector ?? data.lcpElement.nodeLabel ?? 'unknown'}`);
|
|
935
1012
|
if (data.observedTrace?._tag === 'available') {
|
|
@@ -951,13 +1028,15 @@ export function renderScanDetail(data) {
|
|
|
951
1028
|
if (waste.length) {
|
|
952
1029
|
blocks.push([
|
|
953
1030
|
`Wasteful assets (${waste.length})`,
|
|
954
|
-
...waste.map(issue => ` ${issue.wastedMs
|
|
1031
|
+
...waste.map(issue => ` ${issue.wastedMs === null ? 'time not measured' : `${issue.wastedMs}ms`} ${issue.wastedBytes === null ? 'bytes not measured' : `${issue.wastedBytes}b`} ${issue.subtype ?? issue.type} ${issue.url ?? ''}`.trimEnd()),
|
|
955
1032
|
].join('\n'));
|
|
956
1033
|
}
|
|
957
1034
|
if (data.thirdParties.length) {
|
|
1035
|
+
// A null blocking time is unmeasured. Printing it as 0ms read as "this
|
|
1036
|
+
// script never blocks".
|
|
958
1037
|
blocks.push([
|
|
959
1038
|
`Third parties (${data.thirdParties.length})`,
|
|
960
|
-
...data.thirdParties.map(script => ` ${script.blockingTime
|
|
1039
|
+
...data.thirdParties.map(script => ` ${script.blockingTime === null ? 'blocking not measured' : `${script.blockingTime}ms blocking`} ${script.entity ?? 'unknown'}`),
|
|
961
1040
|
].join('\n'));
|
|
962
1041
|
}
|
|
963
1042
|
for (const [label, checks] of [['Accessibility failures', data.accessibility], ['Best-practices failures', data.bestPractices]]) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtseo/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.9",
|
|
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.5.
|
|
37
|
-
"@nuxtseo/sdk": "^0.5.
|
|
36
|
+
"@nuxtseo/protocol": "^0.5.9",
|
|
37
|
+
"@nuxtseo/sdk": "^0.5.9",
|
|
38
38
|
"citty": "^0.2.2",
|
|
39
39
|
"pathe": "^2.0.3"
|
|
40
40
|
},
|
|
@@ -17,7 +17,7 @@ Read the matching file before you act on its subject:
|
|
|
17
17
|
| --- | --- |
|
|
18
18
|
| [references/commands.md](references/commands.md) | use a flag, range, or default. Never guess a flag |
|
|
19
19
|
| [references/protocol.md](references/protocol.md) | parse output, page through a list, or handle an exit code |
|
|
20
|
-
| [references/indexing.md](references/indexing.md) |
|
|
20
|
+
| [references/indexing.md](references/indexing.md) | interpret indexing, Sitemap evidence, or Watched URLs |
|
|
21
21
|
|
|
22
22
|
## Setup
|
|
23
23
|
|
|
@@ -211,21 +211,21 @@ The server verifies the claim; the CLI never marks anything fixed. Resolve
|
|
|
211
211
|
sends the current `artifactVersion` for you. If the evidence moved, it exits
|
|
212
212
|
`5` with `stale_evidence`: re-run step 3 and decide again.
|
|
213
213
|
|
|
214
|
-
If the
|
|
215
|
-
from `actions show`, then:
|
|
214
|
+
If the owner confirms an action is intentional or incorrect, dismiss it. Read
|
|
215
|
+
`artifactVersion` from `actions show`, then record the reason:
|
|
216
216
|
|
|
217
217
|
```sh
|
|
218
|
-
nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --yes --json
|
|
218
|
+
nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --note "<reason>" --yes --json
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
The note is required. Review the exact scope first. A note dismisses the whole
|
|
222
|
+
action, so never use it to hide a mix of intentional and broken URLs. Dismissal
|
|
223
|
+
keeps the observations and does not verify a fix. The dashboard can restore the
|
|
224
|
+
action later.
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
the
|
|
227
|
-
Dismissal keeps the observations and does not verify a fix. The dashboard can
|
|
228
|
-
restore the action later.
|
|
226
|
+
A 404 or 410 is a gone URL. An action names one only when a link, a sitemap
|
|
227
|
+
entry, or search demand still points at it, or when the crawl cannot confirm
|
|
228
|
+
the removal.
|
|
229
229
|
|
|
230
230
|
**7. Mark the day.**
|
|
231
231
|
|
|
@@ -264,6 +264,10 @@ and do not route around it with `pull` or another read.
|
|
|
264
264
|
`coverage.bestPracticesChecksTotal` from `scans show`. `possiblyTruncated:
|
|
265
265
|
true` marks the list as a floor.
|
|
266
266
|
|
|
267
|
+
**A failed Scan has no scores.** `status: failed` with `failureKind` says why.
|
|
268
|
+
Its null scores and vitals are not zeros. A null `blockingTime` or `wastedMs`
|
|
269
|
+
is unmeasured, never zero.
|
|
270
|
+
|
|
267
271
|
For `search indexing urls`, read `totalSemantics` before quoting `total`.
|
|
268
272
|
`lower-bound` means the provider used lookahead instead of counting every URL.
|
|
269
273
|
The total can rise between pages without a filter change. Follow `hasMore` or
|
|
@@ -124,7 +124,7 @@ lines.
|
|
|
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. Needs `--artifact-version`, read from `actions show
|
|
127
|
+
| `actions dismiss <action-id>` | Mutation. Needs `--artifact-version`, read from `actions show`, and `--note`, which records why the owner considers the action intentional or incorrect. Dismissal preserves observations and never verifies a fix |
|
|
128
128
|
|
|
129
129
|
## Pages, Scans, and lab performance
|
|
130
130
|
|
|
@@ -133,11 +133,24 @@ lines.
|
|
|
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
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. Adds or re-enables the URL as a monitored Page, then starts mobile and desktop Scans |
|
|
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
|
-
| `scans list` | Retained Lighthouse Scans, newest first, with scores, lab vitals, and
|
|
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 |
|
|
136
|
+
| `performance` | Stored Lighthouse lab overview for the Site: mobile medians for perf, a11y, SEO, LCP, TBT, CLS, then the homepage and each monitored Page on mobile and desktop. Lab Scans, never real-user field data |
|
|
137
|
+
| `scans list` | Retained Lighthouse Scans, newest first, with scores, lab vitals, `trigger`, and `lighthouseVersion`. A failed Scan carries `failureKind` and `error`, and its scores are null. `--limit 1..100` (default 25) |
|
|
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. Also `trigger`, `lighthouseVersion`, and `comparison` with the previous Scan of the same Page. Read `coverage` before quoting a total |
|
|
139
139
|
| `scans pages` | The Pages this Site scans on a schedule. Add one with `page scan <url>` |
|
|
140
140
|
|
|
141
|
+
`scans list` and `scans show` return the same Scan shape as the `lighthouse_audits` MCP tool.
|
|
142
|
+
`failureKind` says why a Scan failed, for example `timeout`, `rejected`, or `quota`.
|
|
143
|
+
It is null on a Scan that did not fail, and on a Scan that failed before kinds were recorded.
|
|
144
|
+
|
|
145
|
+
`comparison` has one of three tags.
|
|
146
|
+
`compared` lists each metric both Scans measured, with `previous`, `current`, `delta`, and `severity`.
|
|
147
|
+
`incompatible` names why the two Scans differ, for example `lighthouse-version`. Do not compare their numbers.
|
|
148
|
+
`none` means no previous Scan was compared. The homepage slot always reads `none`.
|
|
149
|
+
A lab `regression` compares one Lighthouse result per Scan. Lab scores vary by a few points between Scans.
|
|
150
|
+
|
|
151
|
+
A null `blockingTime` or `wastedMs` is unmeasured. It is never zero.
|
|
152
|
+
If a Scan could not measure blocking time for a third party, text output prints "blocking not measured".
|
|
153
|
+
|
|
141
154
|
`scans show` can include `observedTrace` from the retained Lighthouse projection.
|
|
142
155
|
If its tag is `available`, its phases describe `observedLcpMs` from the browser trace.
|
|
143
156
|
`throttlingMethod` names how Lighthouse computed the reported metrics.
|
|
@@ -185,7 +198,7 @@ The monitored Page then follows its own recurring schedule.
|
|
|
185
198
|
| --- | --- |
|
|
186
199
|
| `vitals summary` | Field Core Web Vitals from CrUX: the latest p75, plus its change over the window. `--url` for one Page, `--form-factor phone\|desktop\|all`. `available: false` means the target is under the CrUX traffic floor |
|
|
187
200
|
| `vitals trend` | A 25-week field p75 series. Same inputs as `vitals summary` |
|
|
188
|
-
| `vitals findings` | Failing field vitals attributed to a DOM element. `--metric lcp\|inp\|cls`, `--limit 1..50` (default 20), `--offset`, `--no-only-failing`. Each finding carries the CSS selector and a ready-to-apply `fixPrompt`. Needs a connected Web Analytics provider |
|
|
201
|
+
| `vitals findings` | Failing field vitals attributed to a DOM element. `--metric lcp\|inp\|cls`, `--limit 1..50` (default 20), `--offset`, `--no-only-failing`. Each finding carries the CSS selector, `poorFraction`, `labHint`, and a ready-to-apply `fixPrompt`. An LCP finding adds `resource`, `fetchPriority`, `initiatorType`, and `phases`. An INP finding adds `interaction`. `window` gives the dates the provider aggregated. Needs a connected Web Analytics provider |
|
|
189
202
|
|
|
190
203
|
## Search Console and sitemaps
|
|
191
204
|
|
|
@@ -198,6 +211,9 @@ The monitored Page then follows its own recurring schedule.
|
|
|
198
211
|
| `search inspect <url>` | Latest retained Google URL Inspection for one URL. Takes an absolute URL or a site-relative path. `--fresh --yes` requests Google URL Inspection through the Site connection and uses inspection quota. Errors or skipped URLs produce a nonzero exit code |
|
|
199
212
|
| `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` |
|
|
200
213
|
| `sitemaps list` | Search Console sitemap snapshot: errors, warnings, URL counts, sync status |
|
|
214
|
+
| `sitemaps submission` | Site-level Google Sitemap submission state. Redacts account identity. Does not prove listing for an arbitrary Sitemap URL |
|
|
215
|
+
| `sitemaps inspect <sitemap-url>` | Exact Sitemap listing evidence. `--engine google\|bing` defaults to `google`. Google reads live. Bing reads dated stored data. Missing exits `0`; unavailable exits `3`, `4`, `5`, or `6` |
|
|
216
|
+
| `search watches` | Retained Watched URLs, `dueAt`, and Checkpoints. An empty Checkpoint list means no retained Checkpoint for that watch |
|
|
201
217
|
| `sitemaps urls` | Sitemap URL membership. `--generation-id`, `--feedpath`, `--cursor`, `--limit 1..1000` |
|
|
202
218
|
| `sitemaps submit <sitemap-url>` | Mutation. Submits one sitemap through the Site's stored Search Console credential. Success means Search Console accepted it, not that Google downloaded it. Read `lastDownloaded` and `isPending` in `sitemaps list` |
|
|
203
219
|
| `sitemaps delete <sitemap-url>` | Mutation. Removes one sitemap through the Site's stored Search Console credential |
|
|
@@ -65,4 +65,19 @@ Keep these reads separate:
|
|
|
65
65
|
| Which URLs appeared in the current period? | `search analytics pages` | Rows with `impressions > 0`, then each `url` |
|
|
66
66
|
| What did Google say at the last inspection? | `search inspect <url>` | The retained verdict and inspection date |
|
|
67
67
|
| What does Google say about one URL now? | `search inspect <url>` | Add `--fresh --yes`. Report fresh results, quota, and explicit errors or skipped URLs |
|
|
68
|
-
| Which URLs
|
|
68
|
+
| Which URLs belong to the Sitemap generation? | `sitemaps list`, then `sitemaps urls` | Sitemap totals and retained membership |
|
|
69
|
+
| Does a provider list this exact Sitemap URL? | `sitemaps inspect <sitemap-url>` | `state`, `capture.source`, and `capture.capturedAt` |
|
|
70
|
+
| Can the Site submit its selected Google Sitemap? | `sitemaps submission` | Site-level `state` and `callerCanAct` |
|
|
71
|
+
| What checks exist for Watched URLs? | `search watches` | Each URL's `dueAt` and retained Checkpoints |
|
|
72
|
+
|
|
73
|
+
`sitemaps inspect` uses `--engine google` by default. Use `--engine bing` for Bing's stored evidence.
|
|
74
|
+
Google reads live. Bing reports its stored capture time; it does not make a live provider request.
|
|
75
|
+
`state._tag: missing` proves absence only within that successful dated read.
|
|
76
|
+
`state._tag: unavailable` provides no listing conclusion and produces a nonzero exit.
|
|
77
|
+
Grant or reauthorization failures exit `3`. Permission loss or disabled access exits `4`.
|
|
78
|
+
Unverified or unchecked evidence exits `5`. Throttling or provider failure exits `6`.
|
|
79
|
+
The JSON response preserves the evidence with these failures.
|
|
80
|
+
|
|
81
|
+
Watched URL due times describe scheduling. They do not prove that checks can run.
|
|
82
|
+
An empty Checkpoint list means no retained Checkpoint for that watch.
|
|
83
|
+
Neither Sitemap listing nor Watched URL enrollment proves that Google indexed a Page.
|
|
@@ -135,6 +135,9 @@ command returns; never guess a chain. Verified against the response schemas.
|
|
|
135
135
|
| `search index-history` | `data.changes` | none |
|
|
136
136
|
| `search cohorts` | `data.established` and `data.ranked` | `data.rankedTotal` |
|
|
137
137
|
| `sitemaps list` | `data.sitemaps` | none |
|
|
138
|
+
| `sitemaps submission` | no list; `data.state` | none |
|
|
139
|
+
| `sitemaps inspect` | no list; `data.capture`, `data.state` | none |
|
|
140
|
+
| `search watches` | `data.watched`; each item's `checkpoints` | none |
|
|
138
141
|
| `sitemaps urls` | `data.items` | `data.page.nextCursor`, `data.page.limit` |
|
|
139
142
|
| `audit content-decay` | `data.rows` | `data.page`; `data.total`, `data.truncated` |
|
|
140
143
|
| `audit link-opportunities` | `data.rows` | `data.page`; `data.total` |
|