@nuxtseo/cli 0.5.1 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -256,7 +256,9 @@ reads retained rows and spends nothing.
256
256
  `search status` reads stored connection state. It never waits for Google.
257
257
  `search indexing summary` reads retained URL Inspection coverage. `asOf` and `oldestCheckAt` bound the age of its verdicts.
258
258
  `search index-history` dates an indexing change against known releases.
259
- `search inspect <url>` reads Google's own verdict for one URL.
259
+ `search inspect <url>` reads the latest retained Google URL Inspection for one URL.
260
+ `search inspect <url> --fresh --yes` requests Google URL Inspection through the Site connection and uses inspection quota.
261
+ Fresh inspection reports errors and skipped URLs with a nonzero exit code. If the request needs more time, add `--timeout-ms 60000`.
260
262
  `page inspect <url>` reads the Nuxt SEO observation store for the same URL.
261
263
  `research keywords`, `research serp`, and `research rankings` can use the Team
262
264
  research limit. Keyword responses report cache use in `evidence`. SERP and
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, 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, 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';
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';
@@ -717,13 +717,36 @@ async function searchIndexHistory(runtime, globals, args) {
717
717
  }, { signal: runtime.requestSignal }));
718
718
  return present(runtime, globals, response, value => renderIndexingHistory(value.data));
719
719
  }
720
- async function searchInspect(runtime, globals, rawUrl) {
720
+ async function searchInspect(runtime, globals, rawUrl, fresh = false) {
721
721
  const url = parsePageUrlOrPath(rawUrl);
722
722
  if (url._tag === 'Err')
723
723
  return url;
724
+ if (fresh) {
725
+ const confirmed = await confirmMutation(runtime, globals, `Request a fresh URL Inspection for ${url.value}? This uses inspection quota.`);
726
+ if (confirmed._tag === 'Err')
727
+ return confirmed;
728
+ if (!confirmed.value)
729
+ return ok(undefined);
730
+ }
724
731
  const resolved = await apiAndSite(runtime, globals);
725
732
  if (resolved._tag === 'Err')
726
733
  return resolved;
734
+ if (fresh) {
735
+ const response = await withSpinner(runtime, 'Requesting URL Inspection', () => resolved.value.api.client.search.refreshUrlInspection({
736
+ params: { siteId: resolved.value.siteId },
737
+ body: { url: url.value },
738
+ }, { signal: runtime.requestSignal }));
739
+ if (response._tag === 'Err')
740
+ return fromSdkFailure(response.error);
741
+ if (response.value.data.errors.length || response.value.data.skipped.length || response.value.data.results.length !== 1) {
742
+ const incomplete = fail(EXIT_CODE.conflict, 'Fresh URL Inspection did not complete. Read the errors or skipped URLs in the response.');
743
+ if (globals.json)
744
+ return { _tag: 'Err', error: { ...incomplete.error, protocolResponse: response.value } };
745
+ writeOutput(runtime, renderUrlInspectionRefresh(response.value.data));
746
+ return incomplete;
747
+ }
748
+ return present(runtime, globals, response, value => renderUrlInspectionRefresh(value.data));
749
+ }
727
750
  const response = await withSpinner(runtime, 'Loading URL Inspection', () => resolved.value.api.client.search.readUrlInspection({
728
751
  params: { siteId: resolved.value.siteId },
729
752
  query: { url: url.value },
@@ -2016,8 +2039,11 @@ export function createRootCommand(runtime, globals, execution) {
2016
2039
  }),
2017
2040
  'inspect': defineCommand({
2018
2041
  meta: { name: 'inspect', description: 'Read the latest URL Inspection result for one URL' },
2019
- args: { url: positional('url', 'Page URL or path') },
2020
- run: ({ args }) => capture(execution, () => searchInspect(runtime, globals, args.url), args._, 1)(),
2042
+ args: {
2043
+ url: positional('url', 'Page URL or path'),
2044
+ fresh: { type: 'boolean', description: 'Request Google URL Inspection using inspection quota', default: false },
2045
+ },
2046
+ run: ({ args }) => capture(execution, () => searchInspect(runtime, globals, args.url, args.fresh), args._, 1)(),
2021
2047
  }),
2022
2048
  },
2023
2049
  });
package/dist/pull.js CHANGED
@@ -304,6 +304,7 @@ export async function runPull(runtime, globals, args, resolve, catalogue = PULL_
304
304
  const skipped = [];
305
305
  let stopped = false;
306
306
  let completed = 0;
307
+ let explainedAnalyticsConnection = false;
307
308
  await forEachBounded(operations, concurrency.value, async (candidate) => {
308
309
  if (stopped) {
309
310
  skipped.push(candidate.name);
@@ -318,13 +319,25 @@ export async function runPull(runtime, globals, args, resolve, catalogue = PULL_
318
319
  });
319
320
  const outcome = pullLine(candidate.name, result);
320
321
  writeProtocolResponse(runtime, outcome.line);
321
- exitCodes.push(outcome.exitCode);
322
+ const analyticsUnconnected = candidate.name.startsWith('analytics ')
323
+ && result._tag === 'Err'
324
+ && result.error._tag === 'ApiFailure'
325
+ && result.error.code === 'conflict'
326
+ && result.error.details?.action === 'connect_web_analytics';
327
+ if (!explainedAnalyticsConnection && analyticsUnconnected) {
328
+ explainedAnalyticsConnection = true;
329
+ writeDiagnostic(runtime, 'Web Analytics is not connected. Connect a property to read these views. For a full SEO pull, use --exclude analytics.');
330
+ }
331
+ // Default pulls include optional integrations. An explicit include asks
332
+ // for those reads, so their missing connection still fails that request.
333
+ const optionalUnavailable = analyticsUnconnected && !args.include;
334
+ exitCodes.push(optionalUnavailable ? EXIT_CODE.success : outcome.exitCode);
322
335
  completed++;
323
336
  // An expired or rejected credential fails every later read the same way.
324
337
  // Stopping there saves the requests and keeps the reason on one line.
325
338
  if (outcome.exitCode === EXIT_CODE.authentication || outcome.exitCode === EXIT_CODE.interrupted)
326
339
  stopped = true;
327
- writeDiagnostic(runtime, `[${completed}/${operations.length}] ${candidate.name} ${outcome.failure ? `failed: ${outcome.failure.code}` : 'ok'}`);
340
+ writeDiagnostic(runtime, `[${completed}/${operations.length}] ${candidate.name} ${optionalUnavailable ? 'unavailable' : outcome.failure ? `failed: ${outcome.failure.code}` : 'ok'}`);
328
341
  if (outcome.failure?.code === 'request_timeout' && timeoutMs < 300_000) {
329
342
  const retryTimeout = Math.min(timeoutMs * 2, 300_000);
330
343
  // Retry only this read. Repeating a full paid pull can spend twice.
package/dist/render.d.ts CHANGED
@@ -4,7 +4,7 @@ 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 } from '@nuxtseo/protocol/v1/gsc';
7
+ import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, 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';
@@ -21,6 +21,7 @@ export declare function renderPerformance(data: SitePerformanceOverview): string
21
21
  export declare function renderSearchStatus(data: SearchStatusData): string;
22
22
  export declare function renderSearchAnalytics(data: SearchAnalyticsData): string;
23
23
  export declare function renderIndexingHistory(data: IndexingHistoryData): string;
24
+ export declare function renderUrlInspectionRefresh(data: UrlInspectionRefreshData): string;
24
25
  export declare function renderUrlInspection(data: UrlInspectionData): string;
25
26
  export declare function renderAuditChanges(data: AuditChanges): string;
26
27
  export declare function renderLinkStructure(data: AuditLinkStructure): string;
package/dist/render.js CHANGED
@@ -232,6 +232,25 @@ export function renderIndexingHistory(data) {
232
232
  }),
233
233
  ].join('\n');
234
234
  }
235
+ export function renderUrlInspectionRefresh(data) {
236
+ return [
237
+ `URL Inspection request completed at ${data.checkedAt}.`,
238
+ ...data.results.map(result => fields([
239
+ ['URL', result.url],
240
+ ['Verdict', result.verdict],
241
+ ['Coverage', result.coverageState],
242
+ ['Indexing', result.indexingState],
243
+ ['robots.txt', result.robotsTxtState],
244
+ ['Page fetch', result.pageFetchState],
245
+ ['User canonical', result.userCanonical],
246
+ ['Google canonical', result.googleCanonical],
247
+ ['Last crawl', result.lastCrawlTime],
248
+ ])),
249
+ ...data.errors.map(result => `Inspection failed for ${result.url}: ${result.error}`),
250
+ ...data.skipped.map(result => `Inspection skipped for ${result.url}: ${result.reason}`),
251
+ `Inspection quota remaining: ${data.rateLimit.remaining} of ${data.rateLimit.limit}.`,
252
+ ].join('\n');
253
+ }
235
254
  export function renderUrlInspection(data) {
236
255
  if (data._tag === 'no-inspection-data')
237
256
  return `No retained URL Inspection for ${data.requestedUrl}.`;
@@ -604,7 +623,7 @@ export function renderFieldVitals(data) {
604
623
  ['As of', summary.asOf],
605
624
  ...['lcp', 'inp', 'cls'].map(metric => [
606
625
  metric.toUpperCase(),
607
- `${vitalValue(metric, summary[metric].p75)} ${summary[metric].status ?? 'unrated'} delta ${summary[metric].deltaOverPeriod ?? 'none'}`,
626
+ `${vitalValue(metric, summary[metric].p75)} ${summary[metric].status ?? 'unrated'} delta ${summary[metric].deltaOverPeriod ?? 'none'} As of ${summary[metric].asOf ?? 'unknown'}`,
608
627
  ]),
609
628
  ]),
610
629
  ].join('\n');
@@ -893,10 +912,26 @@ export function renderScanDetail(data) {
893
912
  ];
894
913
  if (data.lcpElement)
895
914
  blocks.push(`LCP element ${data.lcpElement.selector ?? data.lcpElement.nodeLabel ?? 'unknown'}`);
896
- if (data.performanceIssues.length) {
915
+ if (data.observedTrace?._tag === 'available') {
916
+ const trace = data.observedTrace;
917
+ blocks.push([
918
+ `Observed trace LCP ${trace.observedLcpMs}ms throttling ${trace.throttlingMethod ?? 'not reported'}`,
919
+ ...trace.phases.map(phase => ` ${phase.id} ${phase.durationMs}ms`),
920
+ 'Trace phases describe observed LCP. Reported LCP can be simulated.',
921
+ ].join('\n'));
922
+ }
923
+ else {
924
+ blocks.push(`Observed trace unavailable ${data.observedTrace?.reason ?? 'legacy-response'}`);
925
+ }
926
+ if (data.javascript) {
927
+ const bytes = (amount) => amount === null ? 'unknown' : `${amount}b`;
928
+ blocks.push(`JavaScript ${bytes(data.javascript.transferBytes)} transferred ${bytes(data.javascript.decodedBytes)} decoded ${data.javascript.scriptCount ?? 'unknown'} resources`);
929
+ }
930
+ const waste = data.performanceIssues.filter(issue => issue.subtype !== 'js-transfer-weight');
931
+ if (waste.length) {
897
932
  blocks.push([
898
- `Wasteful assets (${data.performanceIssues.length})`,
899
- ...data.performanceIssues.map(issue => ` ${issue.wastedMs ?? 0}ms ${issue.wastedBytes ?? 0}b ${issue.subtype ?? issue.type} ${issue.url ?? ''}`.trimEnd()),
933
+ `Wasteful assets (${waste.length})`,
934
+ ...waste.map(issue => ` ${issue.wastedMs ?? 0}ms ${issue.wastedBytes ?? 0}b ${issue.subtype ?? issue.type} ${issue.url ?? ''}`.trimEnd()),
900
935
  ].join('\n'));
901
936
  }
902
937
  if (data.thirdParties.length) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nuxtseo/cli",
3
3
  "type": "module",
4
- "version": "0.5.1",
4
+ "version": "0.5.4",
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.1",
37
- "@nuxtseo/sdk": "^0.5.1",
36
+ "@nuxtseo/protocol": "^0.5.4",
37
+ "@nuxtseo/sdk": "^0.5.4",
38
38
  "citty": "^0.2.2",
39
39
  "pathe": "^2.0.3"
40
40
  },
@@ -277,7 +277,10 @@ Do not compare this population with a summary or cohort from another capture.
277
277
  - `status` is the verdict, `performance` the lab score, `vitals` what real
278
278
  users measured. Never substitute one for another.
279
279
  - `page inspect` reads the Nuxt SEO observation store. `search inspect` reads
280
- Google's index verdict for the same URL.
280
+ the latest retained Google URL Inspection for the same URL.
281
+ `search inspect <url> --fresh --yes` requests Google URL Inspection through
282
+ the Site connection and uses inspection quota. Errors or skipped URLs mean
283
+ the fresh inspection did not complete. Never present them as fresh verdicts.
281
284
  - `search status` reads the stored connection and never waits for Google.
282
285
  `search analytics`, `search indexing`, `search index-history`, and
283
286
  `search inspect` read retained Search Console evidence.
@@ -138,9 +138,36 @@ lines.
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 |
139
139
  | `scans pages` | The Pages this Site scans on a schedule. Add one with `page scan <url>` |
140
140
 
141
+ `scans show` can include `observedTrace` from the retained Lighthouse projection.
142
+ If its tag is `available`, its phases describe `observedLcpMs` from the browser trace.
143
+ `throttlingMethod` names how Lighthouse computed the reported metrics.
144
+ If it is `null`, the provider omitted the method. Do not infer a method from the timings.
145
+ With `simulate`, reported `lcp` is modeled. Observed trace phases do not partition that modeled value.
146
+ With `devtools` or `provided`, Lighthouse uses observed metrics.
147
+ Neither value is real-user field data.
148
+ An `unavailable` tag names missing, invalid, or legacy evidence.
149
+ Older servers can omit the field. Omission also means unavailable.
150
+ This command never reads raw reports or starts a projection backfill.
151
+
141
152
  `page inspect` and `page scan` take an absolute URL, for example
142
153
  `https://example.com/about`.
143
154
 
155
+ ### JavaScript byte measurements
156
+
157
+ `scans show` returns `data.javascript` with stored `transferBytes`, `decodedBytes`, and `scriptCount`.
158
+ Each measurement is nullable. `javascript: null` means no stored JavaScript measurement.
159
+ An older server may omit the field.
160
+
161
+ The extractor counts JavaScript by resource type, MIME type, or `.js` / `.mjs` URL.
162
+ It includes downloaded JavaScript labelled `Other`, which can include prefetch requests.
163
+ The stored totals do not separate prefetch from executed JavaScript.
164
+ They do not measure parse, compile, or execution work.
165
+
166
+ For a `js-transfer-weight` diagnostic, `wastedBytes` holds total decoded JavaScript bytes.
167
+ It is not removable code or Lighthouse unused-byte savings.
168
+ Use `data.javascript` for byte comparisons, and `unused` findings for unused-byte evidence.
169
+ Do not infer a reduction in executed JavaScript from download totals alone.
170
+
144
171
  `page scan` enables recurring monitoring.
145
172
  Read `scans pages` before scanning many new URLs. The server refuses a new
146
173
  Page when five monitored Pages are already enabled. That limit comes from
@@ -168,7 +195,7 @@ The monitored Page then follows its own recurring schedule.
168
195
  | `search analytics <view>` | Search Console rows, trend, detail, or analysis. Views: `pages`, `keywords`, `countries`, `devices`, `timeseries`, `page-detail`, `keyword-detail`, `analysis`. `--period 7d\|28d\|3m\|6m\|12m` (default `28d`). No other period value is accepted; `90d` exits `2`, and `3m` is the three-month window. `--limit 1..100` (default 25), `--page` (default 1), `--search`, `--sort clicks\|impressions\|ctr\|position` (default `clicks`), `--sort-dir asc\|desc` (default `desc`), `--filter top-level\|new\|lost\|improving\|declining`, `--min-clicks`, `--max-clicks`, `--min-impressions`, `--max-impressions`, `--min-position 1..100`, `--max-position 1..100`, `--max-ctr 0..1`. The `page-detail` view needs `--page-url`, `keyword-detail` needs `--keyword`, and `analysis` needs `--preset`. The brand presets also need `--brand-terms` |
169
196
  | `search indexing <summary\|urls>` | Retained URL Inspection coverage. `summary` returns indexed counts. `urls` supports `--issue`, `--status`, `--limit 1..500` (default 50), `--offset` |
170
197
  | `search index-history` | Retained indexing changes and candidate causes. `--url`, `--field`, `--days 1..730`. Dates a regression against a release |
171
- | `search inspect <url>` | Google's own verdict for one URL. Takes an absolute URL or a site-relative path |
198
+ | `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 |
172
199
  | `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` |
173
200
  | `sitemaps list` | Search Console sitemap snapshot: errors, warnings, URL counts, sync status |
174
201
  | `sitemaps urls` | Sitemap URL membership. `--generation-id`, `--feedpath`, `--cursor`, `--limit 1..1000` |
@@ -63,5 +63,6 @@ Keep these reads separate:
63
63
  | What is the indexed count for retained URLs? | `search indexing summary` | `totalUrls`, `indexed`, `oldestCheckAt`, `asOf` |
64
64
  | Which retained URLs are indexed or not indexed? | `search indexing urls` | `total`, then the complete `urls` stream |
65
65
  | Which URLs appeared in the current period? | `search analytics pages` | Rows with `impressions > 0`, then each `url` |
66
- | What does Google say about one URL now? | `search inspect <url>` | The returned URL Inspection verdict |
66
+ | What did Google say at the last inspection? | `search inspect <url>` | The retained verdict and inspection date |
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 |
67
68
  | Which URLs did the Site submit? | `sitemaps list`, then `sitemaps urls` | Sitemap totals and membership |