@nuxtseo/cli 0.5.0 → 0.5.3

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/commands.js CHANGED
@@ -1223,7 +1223,11 @@ async function actionDismiss(runtime, globals, args) {
1223
1223
  return fail(EXIT_CODE.invalidInput, 'Pass the action id to dismiss.', undefined, 'invalid_cli_input');
1224
1224
  if (!args.artifactVersion)
1225
1225
  return fail(EXIT_CODE.invalidInput, 'Pass --artifact-version, the artifactVersion returned with this action.', undefined, 'invalid_cli_input');
1226
- const confirmed = await confirmMutation(runtime, globals, `Dismiss action ${args.actionId}? The page stays removed.`);
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.`);
1227
1231
  if (confirmed._tag === 'Err')
1228
1232
  return confirmed;
1229
1233
  if (!confirmed.value) {
@@ -1235,7 +1239,7 @@ async function actionDismiss(runtime, globals, args) {
1235
1239
  return resolved;
1236
1240
  const response = await withSpinner(runtime, 'Dismissing action', () => resolved.value.api.client.actions.dismiss({
1237
1241
  params: { siteId: resolved.value.siteId, actionId: args.actionId },
1238
- body: { artifactVersion: args.artifactVersion },
1242
+ body: { artifactVersion: args.artifactVersion, ...(args.note !== undefined ? { note: args.note.trim() } : {}) },
1239
1243
  }, { signal: runtime.requestSignal }));
1240
1244
  return present(runtime, globals, response, value => renderActionDismissal(value.data));
1241
1245
  }
@@ -1559,14 +1563,16 @@ export function createRootCommand(runtime, globals, execution) {
1559
1563
  run: ({ args }) => capture(execution, () => actionsResolve(runtime, globals, args.actionId), args._, 1)(),
1560
1564
  }),
1561
1565
  dismiss: defineCommand({
1562
- meta: { name: 'dismiss', description: 'Dismiss a broken-page action so the page stays removed' },
1566
+ meta: { name: 'dismiss', description: 'Dismiss reviewed work with a note, or confirm an unlinked page removal' },
1563
1567
  args: {
1564
1568
  'actionId': positional('action-id', 'Action ID'),
1565
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' },
1566
1571
  },
1567
1572
  run: ({ args }) => capture(execution, () => actionDismiss(runtime, globals, {
1568
1573
  actionId: args.actionId,
1569
1574
  artifactVersion: args['artifact-version'],
1575
+ note: args.note,
1570
1576
  }), args._, 1)(),
1571
1577
  }),
1572
1578
  },
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,6 +319,15 @@ 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);
322
+ if (!explainedAnalyticsConnection
323
+ && candidate.name.startsWith('analytics ')
324
+ && result._tag === 'Err'
325
+ && result.error._tag === 'ApiFailure'
326
+ && result.error.code === 'conflict'
327
+ && result.error.details?.action === 'connect_web_analytics') {
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
+ }
321
331
  exitCodes.push(outcome.exitCode);
322
332
  completed++;
323
333
  // An expired or rejected credential fails every later read the same way.
@@ -325,6 +335,11 @@ export async function runPull(runtime, globals, args, resolve, catalogue = PULL_
325
335
  if (outcome.exitCode === EXIT_CODE.authentication || outcome.exitCode === EXIT_CODE.interrupted)
326
336
  stopped = true;
327
337
  writeDiagnostic(runtime, `[${completed}/${operations.length}] ${candidate.name} ${outcome.failure ? `failed: ${outcome.failure.code}` : 'ok'}`);
338
+ if (outcome.failure?.code === 'request_timeout' && timeoutMs < 300_000) {
339
+ const retryTimeout = Math.min(timeoutMs * 2, 300_000);
340
+ // Retry only this read. Repeating a full paid pull can spend twice.
341
+ 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`);
342
+ }
328
343
  });
329
344
  const failed = exitCodes.filter(code => code !== EXIT_CODE.success).length;
330
345
  writeDiagnostic(runtime, [
package/dist/render.js CHANGED
@@ -445,7 +445,7 @@ export function renderIndexingDiagnostics(data) {
445
445
  if (data.urls.length === 0)
446
446
  return 'No Search Console indexing URLs match these filters.';
447
447
  return [
448
- `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})`,
449
449
  ...data.urls.map(row => `${row.verdict ?? 'unknown'} ${row.coverageState ?? 'unknown'}\n ${row.url}`),
450
450
  data.hasMore ? `More URLs available after offset ${data.offset + data.limit}.` : '',
451
451
  ].filter(Boolean).join('\n');
@@ -893,10 +893,26 @@ export function renderScanDetail(data) {
893
893
  ];
894
894
  if (data.lcpElement)
895
895
  blocks.push(`LCP element ${data.lcpElement.selector ?? data.lcpElement.nodeLabel ?? 'unknown'}`);
896
- if (data.performanceIssues.length) {
896
+ if (data.observedTrace?._tag === 'available') {
897
+ const trace = data.observedTrace;
897
898
  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()),
899
+ `Observed trace LCP ${trace.observedLcpMs}ms throttling ${trace.throttlingMethod}`,
900
+ ...trace.phases.map(phase => ` ${phase.id} ${phase.durationMs}ms`),
901
+ 'Trace phases describe observed LCP. Reported LCP can be simulated.',
902
+ ].join('\n'));
903
+ }
904
+ else {
905
+ blocks.push(`Observed trace unavailable ${data.observedTrace?.reason ?? 'legacy-response'}`);
906
+ }
907
+ if (data.javascript) {
908
+ const bytes = (amount) => amount === null ? 'unknown' : `${amount}b`;
909
+ blocks.push(`JavaScript ${bytes(data.javascript.transferBytes)} transferred ${bytes(data.javascript.decodedBytes)} decoded ${data.javascript.scriptCount ?? 'unknown'} resources`);
910
+ }
911
+ const waste = data.performanceIssues.filter(issue => issue.subtype !== 'js-transfer-weight');
912
+ if (waste.length) {
913
+ blocks.push([
914
+ `Wasteful assets (${waste.length})`,
915
+ ...waste.map(issue => ` ${issue.wastedMs ?? 0}ms ${issue.wastedBytes ?? 0}b ${issue.subtype ?? issue.type} ${issue.url ?? ''}`.trimEnd()),
900
916
  ].join('\n'));
901
917
  }
902
918
  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.0",
4
+ "version": "0.5.3",
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.0",
37
- "@nuxtseo/sdk": "^0.5.0",
36
+ "@nuxtseo/protocol": "^0.5.3",
37
+ "@nuxtseo/sdk": "^0.5.3",
38
38
  "citty": "^0.2.2",
39
39
  "pathe": "^2.0.3"
40
40
  },
@@ -28,8 +28,18 @@ 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, run `node packages/cli/dist/cli-entry.js`
32
- instead.
31
+ Inside the Nuxt SEO monorepo, build the workspace dependencies in order
32
+ before using `node packages/cli/dist/cli-entry.js`:
33
+
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.
33
43
 
34
44
  2. **Skill version.** Run `nuxtseo --version --json`. Compare the CLI version
35
45
  with this file's frontmatter version. If the CLI is older, update the CLI
@@ -112,10 +122,20 @@ nuxtseo status --site <site-id> --json
112
122
  Route family: a lead to check, never the Site verdict. `assessment.aggregate`
113
123
  is always the whole-Site verdict. If `healthStage` is `quality_rejection`,
114
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.
115
130
  - Read `dataQuality.status` before you quote the verdict. `degraded` means
116
131
  proofs were missing, so the verdict is Provisional. `unavailable` means it
117
132
  has not run.
118
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.
119
139
 
120
140
  For the whole Site instead of the next action, run one `pull`. It performs
121
141
  every spend-free Site read and writes one envelope per line:
@@ -124,6 +144,18 @@ every spend-free Site read and writes one envelope per line:
124
144
  nuxtseo pull --site <site-id> --json > site.ndjson
125
145
  ```
126
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
+
127
159
  **2. Pick a ranked action.**
128
160
 
129
161
  ```sh
@@ -133,6 +165,10 @@ nuxtseo actions list --site <site-id> --json
133
165
  Keep server order. Check `evidence.freshness` on each row. If `verdict` is
134
166
  `aged` with a large `ageHours`, live-check a cheap sample before you fix. The
135
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.
136
172
 
137
173
  **3. Read the evidence.**
138
174
 
@@ -142,7 +178,11 @@ nuxtseo actions show <action-id> --site <site-id> --json
142
178
 
143
179
  It names the pages, the finding type, and when it was observed. If
144
180
  `data.workPrompt` is not null, it is the Work Prompt: the task for this action,
145
- grounded in its evidence. Start the fix from it. Two reads go deeper:
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:
146
186
 
147
187
  - `page issues --action-id <action-id>`: the raw observations, with URLs,
148
188
  status codes, redirect targets, and Lighthouse selectors. This is the
@@ -175,8 +215,14 @@ from `actions show`, then:
175
215
  nuxtseo actions dismiss <action-id> --site <site-id> --artifact-version <v> --yes --json
176
216
  ```
177
217
 
178
- The server admits a dismiss only for a broken-page action with complete 404 or
179
- 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.
180
226
 
181
227
  **7. Mark the day.**
182
228
 
@@ -215,6 +261,13 @@ and do not route around it with `pull` or another read.
215
261
  `coverage.bestPracticesChecksTotal` from `scans show`. `possiblyTruncated:
216
262
  true` marks the list as a floor.
217
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
+
218
271
  **Keep these datasets apart:**
219
272
 
220
273
  - `performance` and `scans *` read Lighthouse **lab** Scans. `vitals summary`
@@ -251,6 +304,19 @@ its two lists:
251
304
  with `statisticallyEstablished: false` is a lead to verify, for example by
252
305
  inspecting its URLs. Presenting it as a cause is a hard failure.
253
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
+
254
320
  ## Guardrails
255
321
 
256
322
  - **Get consent before a mutation.** `page scan`, `actions resolve`, `actions
@@ -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. Says the page stays removed. Needs `--artifact-version`, read from `actions show`. Admitted only for a broken-page action with complete 404 or 410 evidence and no internal referrers |
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
 
@@ -132,15 +132,52 @@ lines.
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
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. Starts a mobile Scan and a desktop Scan |
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 |
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
+ With `simulate`, reported `lcp` is modeled. Observed trace phases do not partition that modeled value.
145
+ With `devtools` or `provided`, Lighthouse uses observed metrics.
146
+ Neither value is real-user field data.
147
+ An `unavailable` tag names missing, invalid, or legacy evidence.
148
+ Older servers can omit the field. Omission also means unavailable.
149
+ This command never reads raw reports or starts a projection backfill.
150
+
141
151
  `page inspect` and `page scan` take an absolute URL, for example
142
152
  `https://example.com/about`.
143
153
 
154
+ ### JavaScript byte measurements
155
+
156
+ `scans show` returns `data.javascript` with stored `transferBytes`, `decodedBytes`, and `scriptCount`.
157
+ Each measurement is nullable. `javascript: null` means no stored JavaScript measurement.
158
+ An older server may omit the field.
159
+
160
+ The extractor counts JavaScript by resource type, MIME type, or `.js` / `.mjs` URL.
161
+ It includes downloaded JavaScript labelled `Other`, which can include prefetch requests.
162
+ The stored totals do not separate prefetch from executed JavaScript.
163
+ They do not measure parse, compile, or execution work.
164
+
165
+ For a `js-transfer-weight` diagnostic, `wastedBytes` holds total decoded JavaScript bytes.
166
+ It is not removable code or Lighthouse unused-byte savings.
167
+ Use `data.javascript` for byte comparisons, and `unused` findings for unused-byte evidence.
168
+ Do not infer a reduction in executed JavaScript from download totals alone.
169
+
170
+ `page scan` enables recurring monitoring.
171
+ Read `scans pages` before scanning many new URLs. The server refuses a new
172
+ Page when five monitored Pages are already enabled. That limit comes from
173
+ the Page scan service, not the Team plan.
174
+
175
+ `pageCreated: true` means a monitored Page was created. The URL may already
176
+ have retained Scans. Automatic homepage assessment Scans use a separate
177
+ slot without a monitored Page. A manual homepage Scan can therefore create
178
+ a monitored Page while older homepage assessment Scans exist.
179
+ The monitored Page then follows its own recurring schedule.
180
+
144
181
  ## Field Core Web Vitals
145
182
 
146
183
  | Command | What it returns, and its flags |
@@ -177,23 +214,33 @@ different numbers. Read the one that answers your question.
177
214
  | Field | What it counts |
178
215
  | --- | --- |
179
216
  | `total` | The number of rows the query matched, not clicks or impressions |
180
- | `totals` | Clicks, impressions, CTR and position for the full upstream query population. `totalsScope` says so in words. It may include rows outside this page. `null` when the server has no total |
217
+ | `totals` | Upstream period totals from a separate read without grouping dimensions. These can include anonymized queries. Read `totalsScope`. `null` means unavailable |
181
218
  | `representedTotals` | Clicks and impressions for the rows in this response only, after local filtering. `representedTotalsScope` says so in words |
182
219
 
183
- So `representedTotals` is at or below `totals`, and raising `--limit` moves
184
- `representedTotals` while leaving `totals` still. Never sum `rows` by hand when
185
- `totals` is present.
220
+ Raising `--limit` changes `representedTotals` while `totals` covers the full window.
221
+ Grouped Page impressions can exceed whole-Site impressions when several Pages appear in one search.
222
+ Do not assume `representedTotals` is at or below `totals`.
223
+ Use `totals` for traffic totals, and `representedTotals` for the returned rows.
224
+ Neither row filters nor pagination make `totals` a sum of the returned rows.
225
+
226
+ Keyword lists omit `site:` and `inurl:` search operators after upstream paging.
227
+ `total` and `page` still count upstream rows.
228
+ Even `--all` can return fewer keyword rows than `total`.
229
+
230
+ ### Read keyword coverage separately
186
231
 
187
- ### Never compare a keyword total with a page total
232
+ Google suppresses query rows below a privacy threshold.
233
+ Keyword rows therefore omit some traffic, even after `--all`.
234
+ Their `representedTotals` measure only the returned keywords.
188
235
 
189
- Google suppresses query rows below a privacy threshold. So the `keywords` view
190
- sees a fraction of the traffic the `pages` view sees. On a typical property
191
- that is roughly half the impressions. Query-dimension totals are therefore
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.
236
+ The engine can read dimensionless totals that include anonymized traffic.
237
+ Then `keywords.totals` can equal `pages.totals` for the same Site and period.
238
+ Do not assume `keywords.totals` is the sum of visible keyword rows.
194
239
 
195
- Read the `pages` view for "how much traffic". Read the `keywords` view for
196
- "which terms are visible". A missing query row is never evidence of no demand.
240
+ Read `timeseries.totals` for whole-Site traffic, `pages` rows for landing pages,
241
+ and `keywords` rows for visible terms. Use `prevTotals` to compare Site traffic
242
+ across periods.
243
+ A missing query row is never evidence of no demand.
197
244
 
198
245
  ## Web Analytics
199
246