@nuxtseo/protocol 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/v1/pages.js CHANGED
@@ -289,10 +289,249 @@ export const pageScanOperation = defineOperation({
289
289
  }],
290
290
  },
291
291
  });
292
+ // ---------------------------------------------------------------------------
293
+ // Raw page observations — the read model behind the `page_issues` MCP tool.
294
+ // This is the mutable observation store, never canonical Action membership:
295
+ // read an Action with the Action operations for that. Select either one
296
+ // Action's stamped keys (actionId) or one explicit source and issue pair.
297
+ // ---------------------------------------------------------------------------
298
+ export const pageIssuesQuerySchema = schema.strictObject({
299
+ actionId: schema.string().min(1).optional(),
300
+ source: schema.string().min(1).optional(),
301
+ issue: schema.string().min(1).optional(),
302
+ url: schema.string().min(1).optional(),
303
+ pathPrefix: schema.string().startsWith('/').optional(),
304
+ limit: schema.coerce.number().int().min(1).max(200).default(100),
305
+ offset: schema.coerce.number().int().min(0).default(0),
306
+ includeResolved: publicV1BooleanFlag(false),
307
+ });
308
+ const pageIssueTargetShape = {
309
+ url: schema.string().min(1),
310
+ status: schema.number().int().nullable(),
311
+ };
312
+ export const pageIssueTargetSchema = defineResponseObject(pageIssueTargetShape, pageIssueTargetShape);
313
+ const pageIssueElementShape = {
314
+ selector: schema.string().nullable(),
315
+ snippet: schema.string().nullable(),
316
+ message: schema.string().nullable(),
317
+ explanation: schema.string().nullable(),
318
+ sourceUrl: schema.string().nullable(),
319
+ };
320
+ export const pageIssueElementSchema = defineResponseObject(pageIssueElementShape, pageIssueElementShape);
321
+ /**
322
+ * `observedContentType` and `observedAt` are evidence, never a gate.
323
+ * `observedAt` dates a Search Console verdict: a fault fixed since that date is
324
+ * still reported here until Google re-inspects, so quote it as "as of", never
325
+ * as current. A null `observedContentType` means unknown, never `text/html`.
326
+ */
327
+ export const pageIssueDetailSchema = defineResponseObject({
328
+ label: schema.string().nullable(),
329
+ resourceUrl: schema.string().nullable(),
330
+ statusCode: schema.number().int().nullable(),
331
+ targets: schema.array(pageIssueTargetSchema.producer),
332
+ isNew: schema.boolean().nullable(),
333
+ title: schema.string().nullable(),
334
+ description: schema.string().nullable(),
335
+ wcag: schema.array(schema.string()),
336
+ elements: schema.array(pageIssueElementSchema.producer),
337
+ elementsTotal: schema.number().int().nonnegative().nullable(),
338
+ observedContentType: schema.string().nullable(),
339
+ /**
340
+ * Whether the crawl recorded a content type for this row at all. It is the
341
+ * only way to read a null `observedContentType`: false means the crawl
342
+ * never captured one, true means it saw a response it could not name.
343
+ * Neither is `text/html`. The `page_issues` MCP tool carries the same
344
+ * distinction by omitting the key, so publishing the flag keeps the two
345
+ * surfaces answering the same question.
346
+ */
347
+ observedContentTypeCaptured: schema.boolean(),
348
+ redirectTarget: schema.string().nullable(),
349
+ observedAt: schema.string().nullable(),
350
+ }, {
351
+ label: schema.string().nullable(),
352
+ resourceUrl: schema.string().nullable(),
353
+ statusCode: schema.number().int().nullable(),
354
+ targets: schema.array(pageIssueTargetSchema.client),
355
+ isNew: schema.boolean().nullable(),
356
+ title: schema.string().nullable(),
357
+ description: schema.string().nullable(),
358
+ wcag: schema.array(schema.string()),
359
+ elements: schema.array(pageIssueElementSchema.client),
360
+ elementsTotal: schema.number().int().nonnegative().nullable(),
361
+ observedContentType: schema.string().nullable(),
362
+ observedContentTypeCaptured: schema.boolean(),
363
+ redirectTarget: schema.string().nullable(),
364
+ observedAt: schema.string().nullable(),
365
+ });
366
+ /**
367
+ * `resolution` says WHY a closed row closed. `unmonitored` and `expired` mean
368
+ * nobody re-checked the page, so the defect may still be live; only a fixed row
369
+ * is evidence of a fix.
370
+ */
371
+ export const pageIssueSchema = defineResponseObject({
372
+ url: schema.string().min(1),
373
+ path: schema.string(),
374
+ pathWithQuery: schema.string().nullable(),
375
+ source: schema.string().min(1),
376
+ issueType: schema.string().min(1),
377
+ severity: schema.string().min(1),
378
+ detail: pageIssueDetailSchema.producer,
379
+ firstSeenAt: schema.string().nullable(),
380
+ lastSeenAt: schema.string().nullable(),
381
+ resolution: schema.string().nullable(),
382
+ linkedFrom: schema.array(schema.string()),
383
+ linkedFromTotal: schema.number().int().nonnegative().nullable(),
384
+ }, {
385
+ url: schema.string().min(1),
386
+ path: schema.string(),
387
+ pathWithQuery: schema.string().nullable(),
388
+ source: schema.string().min(1),
389
+ issueType: schema.string().min(1),
390
+ severity: schema.string().min(1),
391
+ detail: pageIssueDetailSchema.client,
392
+ firstSeenAt: schema.string().nullable(),
393
+ lastSeenAt: schema.string().nullable(),
394
+ resolution: schema.string().nullable(),
395
+ linkedFrom: schema.array(schema.string()),
396
+ linkedFromTotal: schema.number().int().nonnegative().nullable(),
397
+ });
398
+ const pageIssuesCoverageShape = {
399
+ perSourceCap: schema.number().int().positive(),
400
+ perRowTargetCap: schema.number().int().positive(),
401
+ perRowElementCap: schema.number().int().positive(),
402
+ possiblyTruncated: schema.boolean(),
403
+ };
404
+ export const pageIssuesDataSchema = defineResponseObject({
405
+ semantics: schema.literal('raw-observations'),
406
+ issues: schema.array(pageIssueSchema.producer),
407
+ total: schema.number().int().nonnegative(),
408
+ limit: schema.number().int().positive(),
409
+ offset: schema.number().int().nonnegative(),
410
+ /** Source and issue pairs this Site has, returned when the selector matched nothing. */
411
+ availableKeys: schema.array(schema.strictObject({
412
+ source: schema.string().min(1),
413
+ issue: schema.string().min(1),
414
+ openCount: schema.number().int().nonnegative(),
415
+ })).nullable(),
416
+ message: schema.string().nullable(),
417
+ asOf: schema.string().nullable(),
418
+ coverage: schema.strictObject(pageIssuesCoverageShape),
419
+ }, {
420
+ semantics: schema.literal('raw-observations'),
421
+ issues: schema.array(pageIssueSchema.client),
422
+ total: schema.number().int().nonnegative(),
423
+ limit: schema.number().int().positive(),
424
+ offset: schema.number().int().nonnegative(),
425
+ availableKeys: schema.array(schema.looseObject({
426
+ source: schema.string().min(1),
427
+ issue: schema.string().min(1),
428
+ openCount: schema.number().int().nonnegative(),
429
+ })).nullable(),
430
+ message: schema.string().nullable(),
431
+ asOf: schema.string().nullable(),
432
+ coverage: schema.looseObject(pageIssuesCoverageShape),
433
+ });
434
+ export const pageIssuesResponseSchema = defineSuccessEnvelope(pageIssuesDataSchema);
435
+ export const pageIssuesOperation = defineOperation({
436
+ id: 'page.issues',
437
+ method: 'GET',
438
+ path: '/api/v1/sites/{siteId}/pages/issues',
439
+ visibility: 'public',
440
+ semantics: { kind: 'query', sideEffects: 'none', retry: 'idempotent', readConsistency: 'primary' },
441
+ auth: {
442
+ credentials: ['session', 'api_token'],
443
+ scopes: ['page:read'],
444
+ minimumRole: 'viewer',
445
+ ownership: 'site',
446
+ },
447
+ request: { params: pageSiteParamsSchema, query: pageIssuesQuerySchema, headers: null, body: null },
448
+ responses: { 200: pageIssuesResponseSchema },
449
+ errors: [
450
+ 'invalid_request',
451
+ 'unauthorized',
452
+ 'auth_expired',
453
+ 'forbidden',
454
+ // The Site monitoring fence this read enforces denies with these two:
455
+ // `entitlement_required` when the Site has no Pro Site access, `conflict` when
456
+ // it is archived or its collection is paused.
457
+ 'entitlement_required',
458
+ 'conflict',
459
+ 'not_found',
460
+ 'rate_limited',
461
+ 'contract_violation',
462
+ 'internal_error',
463
+ ],
464
+ errorResponse: publicV1ErrorEnvelope,
465
+ resources: { reads: [{ type: 'site.page', idFrom: 'params.siteId' }], changes: [] },
466
+ policy: {
467
+ rate: publicV1StandardRatePolicy,
468
+ idempotency: { _tag: 'none' },
469
+ entitlement: null,
470
+ meter: null,
471
+ },
472
+ lifecycle: { introduced: '1.2.0' },
473
+ docs: {
474
+ summary: 'Browse raw Page observations',
475
+ description: 'Returns raw page observations for one action id, or for one source and issue pair. Pass actionId, or pass both source and issue; passing both selectors is rejected. When the selector matches nothing, availableKeys lists the pairs this Site does have. These rows are the mutable observation store and never represent canonical action membership. Read coverage before quoting a total: storage retains at most 500 rows per source, and one request returns at most 200 rows, 8 targets, and 5 element samples per row.',
476
+ tags: ['Pages'],
477
+ examples: [{
478
+ request: {
479
+ params: { siteId: 'site_01JXYZ' },
480
+ query: { source: 'crawl', issue: 'broken-internal-links', limit: 100, offset: 0, includeResolved: '0' },
481
+ },
482
+ response: {
483
+ status: 200,
484
+ body: {
485
+ data: {
486
+ semantics: 'raw-observations',
487
+ issues: [{
488
+ url: 'https://example.com/docs/old',
489
+ path: '/docs/old',
490
+ pathWithQuery: null,
491
+ source: 'crawl',
492
+ issueType: 'broken-internal-links',
493
+ severity: 'error',
494
+ detail: {
495
+ label: 'Broken internal link',
496
+ resourceUrl: null,
497
+ statusCode: 404,
498
+ targets: [{ url: 'https://example.com/docs/new', status: 200 }],
499
+ isNew: true,
500
+ title: null,
501
+ description: null,
502
+ wcag: [],
503
+ elements: [],
504
+ elementsTotal: null,
505
+ observedContentType: 'text/html',
506
+ observedContentTypeCaptured: true,
507
+ redirectTarget: null,
508
+ observedAt: null,
509
+ },
510
+ firstSeenAt: '2026-08-01T00:00:00.000Z',
511
+ lastSeenAt: '2026-09-01T00:00:00.000Z',
512
+ resolution: null,
513
+ linkedFrom: ['https://example.com/docs/index'],
514
+ linkedFromTotal: 1,
515
+ }],
516
+ total: 1,
517
+ limit: 100,
518
+ offset: 0,
519
+ availableKeys: null,
520
+ message: null,
521
+ asOf: null,
522
+ coverage: { perSourceCap: 500, perRowTargetCap: 8, perRowElementCap: 5, possiblyTruncated: false },
523
+ },
524
+ meta: { requestId: 'req_01JXYZ', version: '1.0' },
525
+ },
526
+ },
527
+ }],
528
+ },
529
+ });
292
530
  export const pagesV1Registry = defineRegistry({
293
531
  feature: 'page',
294
532
  operations: {
295
533
  inspect: pageInspectOperation,
296
534
  scan: pageScanOperation,
535
+ issues: pageIssuesOperation,
297
536
  },
298
537
  });