@nuxtseo/protocol 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.
@@ -355,26 +355,76 @@ export const fieldVitalFindingsQuerySchema = schema.strictObject({
355
355
  limit: schema.coerce.number().int().min(1).max(50).default(20),
356
356
  offset: schema.coerce.number().int().min(0).default(0),
357
357
  });
358
- export const fieldVitalFindingSchema = defineResponseObject({
359
- metric: schema.enum(['lcp', 'inp', 'cls']),
358
+ const fieldVitalFindingBaseShape = {
360
359
  severity: fieldVitalRatingSchema,
361
360
  p75: schema.number().nullable(),
362
361
  route: schema.string(),
363
362
  selector: schema.string(),
364
363
  estimatedViews: schema.number(),
364
+ /** Share of sampled views rated poor, 0 to 1. Null when the provider sent no distribution. */
365
+ poorFraction: schema.number().min(0).max(1).nullable(),
366
+ /** The blocking script a lab Scan of the same route found. Null when no Scan matches. */
367
+ labHint: schema.string().nullable(),
365
368
  fixPrompt: schema.string().min(1),
366
- }, {
367
- metric: schema.enum(['lcp', 'inp', 'cls']),
368
- severity: fieldVitalRatingSchema,
369
- p75: schema.number().nullable(),
370
- route: schema.string(),
371
- selector: schema.string(),
372
- estimatedViews: schema.number(),
373
- fixPrompt: schema.string().min(1),
374
- });
369
+ };
370
+ // A newer client reading an older server sees no structured fields, so the
371
+ // client marks each one optional.
372
+ const fieldVitalFindingBaseClientShape = {
373
+ ...fieldVitalFindingBaseShape,
374
+ poorFraction: fieldVitalFindingBaseShape.poorFraction.optional(),
375
+ labHint: fieldVitalFindingBaseShape.labHint.optional(),
376
+ };
377
+ /** Mean LCP sub-phase durations in milliseconds across the sampled views. */
378
+ const fieldVitalLcpPhasesShape = {
379
+ timeToFirstByte: schema.number().nullable(),
380
+ resourceLoadDelay: schema.number().nullable(),
381
+ resourceLoadTime: schema.number().nullable(),
382
+ elementRenderDelay: schema.number().nullable(),
383
+ };
384
+ const fieldVitalLcpShape = {
385
+ /** Host and path of the LCP resource. Null for a text LCP element. */
386
+ resource: schema.string().nullable(),
387
+ /** The `fetchpriority` the browser saw on the LCP resource. */
388
+ fetchPriority: schema.string().nullable(),
389
+ /** How the resource was requested, for example `img` or `css`. */
390
+ initiatorType: schema.string().nullable(),
391
+ phases: schema.strictObject(fieldVitalLcpPhasesShape),
392
+ };
393
+ /**
394
+ * One failing real-user vital attributed to a DOM element. The fields after
395
+ * `selector` depend on `metric`: an LCP finding names its resource and phases,
396
+ * an INP finding names the interaction. `fixPrompt` restates the same facts as
397
+ * prose for an agent to apply.
398
+ */
399
+ export const fieldVitalFindingSchema = {
400
+ producer: schema.discriminatedUnion('metric', [
401
+ schema.strictObject({ metric: schema.literal('lcp'), ...fieldVitalFindingBaseShape, ...fieldVitalLcpShape }),
402
+ schema.strictObject({ metric: schema.literal('inp'), ...fieldVitalFindingBaseShape, interaction: schema.string().nullable() }),
403
+ schema.strictObject({ metric: schema.literal('cls'), ...fieldVitalFindingBaseShape }),
404
+ ]),
405
+ client: schema.discriminatedUnion('metric', [
406
+ schema.looseObject({
407
+ metric: schema.literal('lcp'),
408
+ ...fieldVitalFindingBaseClientShape,
409
+ resource: fieldVitalLcpShape.resource.optional(),
410
+ fetchPriority: fieldVitalLcpShape.fetchPriority.optional(),
411
+ initiatorType: fieldVitalLcpShape.initiatorType.optional(),
412
+ phases: schema.looseObject(fieldVitalLcpPhasesShape).optional(),
413
+ }),
414
+ schema.looseObject({ metric: schema.literal('inp'), ...fieldVitalFindingBaseClientShape, interaction: schema.string().nullable().optional() }),
415
+ schema.looseObject({ metric: schema.literal('cls'), ...fieldVitalFindingBaseClientShape }),
416
+ ]),
417
+ };
418
+ /** The dates the Web Analytics provider aggregated, inclusive, as `YYYY-MM-DD`. */
419
+ const fieldVitalWindowShape = {
420
+ from: schema.iso.date(),
421
+ to: schema.iso.date(),
422
+ };
375
423
  export const fieldVitalFindingsDataSchema = defineResponseObject({
376
424
  source: schema.string().nullable(),
377
425
  message: schema.string().nullable(),
426
+ /** Null when no Web Analytics provider answered. */
427
+ window: schema.strictObject(fieldVitalWindowShape).nullable(),
378
428
  findings: schema.array(fieldVitalFindingSchema.producer),
379
429
  total: schema.number().int().nonnegative(),
380
430
  limit: schema.number().int().positive(),
@@ -382,6 +432,7 @@ export const fieldVitalFindingsDataSchema = defineResponseObject({
382
432
  }, {
383
433
  source: schema.string().nullable(),
384
434
  message: schema.string().nullable(),
435
+ window: schema.looseObject(fieldVitalWindowShape).nullable().optional(),
385
436
  findings: schema.array(fieldVitalFindingSchema.client),
386
437
  total: schema.number().int().nonnegative(),
387
438
  limit: schema.number().int().positive(),
@@ -427,7 +478,7 @@ export const fieldVitalFindingsOperation = defineOperation({
427
478
  lifecycle: { introduced: '1.2.0' },
428
479
  docs: {
429
480
  summary: 'List field Core Web Vitals findings',
430
- description: 'Returns each failing real-user vital attributed to the DOM element behind it, with the p75, the CSS selector, and a ready-to-apply fix prompt. It requires a connected Web Analytics provider. If no provider is connected, findings is empty and message says so.',
481
+ description: 'Returns each failing real-user vital attributed to the DOM element behind it, with the p75, the CSS selector, and a ready-to-apply fix prompt. An LCP finding also names its resource, fetchPriority and phases. An INP finding names the interaction. window gives the dates the provider aggregated. It requires a connected Web Analytics provider. If no provider is connected, findings is empty and message says so.',
431
482
  tags: ['Pages'],
432
483
  examples: [{
433
484
  request: { params: { siteId: 'site_01JXYZ' }, query: { onlyFailing: '1', limit: 20, offset: 0 } },
@@ -437,6 +488,7 @@ export const fieldVitalFindingsOperation = defineOperation({
437
488
  data: {
438
489
  source: 'cloudflare',
439
490
  message: null,
491
+ window: { from: '2026-07-13', to: '2026-08-09' },
440
492
  findings: [{
441
493
  metric: 'lcp',
442
494
  severity: 'poor',
@@ -444,6 +496,12 @@ export const fieldVitalFindingsOperation = defineOperation({
444
496
  route: '/blog',
445
497
  selector: 'main > img.hero',
446
498
  estimatedViews: 1840,
499
+ poorFraction: 0.31,
500
+ labHint: null,
501
+ resource: 'example.com/images/hero.jpg',
502
+ fetchPriority: 'auto',
503
+ initiatorType: 'img',
504
+ phases: { timeToFirstByte: 600, resourceLoadDelay: 1900, resourceLoadTime: 1200, elementRenderDelay: 600 },
447
505
  fixPrompt: 'The LCP element on /blog is main > img.hero at 4300ms.',
448
506
  }],
449
507
  total: 1,
@@ -475,31 +533,130 @@ const scanScoresShape = {
475
533
  bestPractices: nullableMetricSchema,
476
534
  seo: nullableMetricSchema,
477
535
  };
478
- export const performanceScanSummarySchema = defineResponseObject({
536
+ /**
537
+ * Why a Scan started. `claim-recheck` follows a claimed Action. `deploy`
538
+ * follows a new Nuxt build. `confirmation` repeats a Scan that looked like a
539
+ * regression.
540
+ */
541
+ export const PERFORMANCE_SCAN_TRIGGERS = ['scheduled', 'manual', 'claim-recheck', 'deploy', 'confirmation'];
542
+ /**
543
+ * Why a Scan failed. `timeout`, `unavailable`, `quota`, `rejected`, `network`,
544
+ * `empty`, `malformed` and `oversize` describe the PageSpeed Insights request.
545
+ * The rest describe the pipeline around it. `rejected` means PSI refused the
546
+ * URL, so a retry of the same URL fails the same way.
547
+ */
548
+ export const PERFORMANCE_SCAN_FAILURE_KINDS = [
549
+ 'timeout',
550
+ 'unavailable',
551
+ 'quota',
552
+ 'rejected',
553
+ 'network',
554
+ 'empty',
555
+ 'malformed',
556
+ 'oversize',
557
+ 'artifact-missing',
558
+ 'processing',
559
+ 'abandoned',
560
+ 'unqueued',
561
+ 'monitoring-changed',
562
+ 'internal',
563
+ ];
564
+ /**
565
+ * Fields every Scan carries, in the Scan list and the Scan read alike.
566
+ * `failureKind` and `error` are null unless `status` is `failed`. A Scan that
567
+ * failed before failure kinds were recorded has a null `failureKind`.
568
+ */
569
+ const scanBaseShape = {
479
570
  id: schema.string().min(1),
480
571
  url: schema.string().min(1),
481
- strategy: schema.string().min(1),
482
- status: schema.string().min(1),
483
- trigger: schema.string().nullable(),
572
+ strategy: schema.enum(['mobile', 'desktop']),
573
+ status: schema.enum(['running', 'complete', 'failed']),
574
+ trigger: schema.enum(PERFORMANCE_SCAN_TRIGGERS),
575
+ failureKind: schema.enum(PERFORMANCE_SCAN_FAILURE_KINDS).nullable(),
576
+ error: schema.string().nullable(),
577
+ lighthouseVersion: schema.string().nullable(),
484
578
  scores: schema.strictObject(scanScoresShape),
485
579
  lcp: nullableMetricSchema,
486
580
  tbt: nullableMetricSchema,
487
581
  cls: nullableMetricSchema,
488
582
  completedAt: schema.string().datetime().nullable(),
489
- error: schema.string().nullable(),
490
- }, {
583
+ };
584
+ // Strings, not enums, so an older client accepts a status, trigger or failure
585
+ // kind that a newer server adds. Fields added after 1.2.0 are optional.
586
+ const scanBaseClientShape = {
491
587
  id: schema.string().min(1),
492
588
  url: schema.string().min(1),
493
589
  strategy: schema.string().min(1),
494
590
  status: schema.string().min(1),
495
- trigger: schema.string().nullable(),
591
+ trigger: schema.string().nullable().optional(),
592
+ failureKind: schema.string().nullable().optional(),
593
+ error: schema.string().nullable().optional(),
594
+ lighthouseVersion: schema.string().nullable().optional(),
496
595
  scores: schema.looseObject(scanScoresShape),
497
596
  lcp: nullableMetricSchema,
498
597
  tbt: nullableMetricSchema,
499
598
  cls: nullableMetricSchema,
500
599
  completedAt: schema.string().datetime().nullable(),
501
- error: schema.string().nullable(),
502
- });
600
+ };
601
+ export const performanceScanSummarySchema = defineResponseObject(scanBaseShape, scanBaseClientShape);
602
+ /** Metrics a Scan comparison can name. Same names and units as the Scan fields. */
603
+ export const PERFORMANCE_SCAN_COMPARISON_METRICS = ['perf', 'a11y', 'bestPractices', 'seo', 'lcp', 'tbt', 'cls', 'inp', 'fcp', 'speedIndex', 'ttfb'];
604
+ /** Why two Scans of one Page cannot be compared. */
605
+ export const PERFORMANCE_SCAN_INCOMPATIBILITY_REASONS = [
606
+ 'source',
607
+ 'target-url',
608
+ 'strategy',
609
+ 'lighthouse-version',
610
+ 'form-factor',
611
+ 'cpu-throttling',
612
+ 'network-throttling',
613
+ 'host-browser',
614
+ 'runtime-environment',
615
+ ];
616
+ const scanChangeSeveritySchema = schema.enum(['improvement', 'regression', 'neutral']);
617
+ const scanMetricChangeShape = {
618
+ metric: schema.enum(PERFORMANCE_SCAN_COMPARISON_METRICS),
619
+ previous: schema.number(),
620
+ current: schema.number(),
621
+ delta: schema.number(),
622
+ severity: scanChangeSeveritySchema,
623
+ };
624
+ /**
625
+ * This Scan against the previous comparable Scan of the same Page and strategy.
626
+ * `compared` lists each metric both Scans measured. `incompatible` names why
627
+ * the two Scans differ, for example a Lighthouse version change. `none` means
628
+ * no previous Scan was compared, which is always true for the homepage slot.
629
+ */
630
+ export const performanceScanComparisonSchema = {
631
+ producer: schema.discriminatedUnion('_tag', [
632
+ schema.strictObject({
633
+ _tag: schema.literal('compared'),
634
+ previousScanId: schema.string().min(1).nullable(),
635
+ severity: scanChangeSeveritySchema,
636
+ changes: schema.array(schema.strictObject(scanMetricChangeShape)),
637
+ }),
638
+ schema.strictObject({
639
+ _tag: schema.literal('incompatible'),
640
+ previousScanId: schema.string().min(1).nullable(),
641
+ reasons: schema.array(schema.enum(PERFORMANCE_SCAN_INCOMPATIBILITY_REASONS)),
642
+ }),
643
+ schema.strictObject({ _tag: schema.literal('none') }),
644
+ ]),
645
+ client: schema.discriminatedUnion('_tag', [
646
+ schema.looseObject({
647
+ _tag: schema.literal('compared'),
648
+ previousScanId: schema.string().min(1).nullable(),
649
+ severity: schema.string().min(1),
650
+ changes: schema.array(schema.looseObject({ ...scanMetricChangeShape, metric: schema.string().min(1), severity: schema.string().min(1) })),
651
+ }),
652
+ schema.looseObject({
653
+ _tag: schema.literal('incompatible'),
654
+ previousScanId: schema.string().min(1).nullable(),
655
+ reasons: schema.array(schema.string().min(1)),
656
+ }),
657
+ schema.looseObject({ _tag: schema.literal('none') }),
658
+ ]),
659
+ };
503
660
  export const performanceScansDataSchema = defineResponseObject({
504
661
  scans: schema.array(performanceScanSummarySchema.producer),
505
662
  total: schema.number().int().nonnegative(),
@@ -546,7 +703,7 @@ export const performanceScansOperation = defineOperation({
546
703
  lifecycle: { introduced: '1.2.0' },
547
704
  docs: {
548
705
  summary: 'List Lighthouse Scans',
549
- description: 'Returns retained Lighthouse Scans for the Site, newest first, with the scores and lab vitals each one recorded. A failed Scan carries its error. Use one Scan id with the Scan read for the failing checks behind a score.',
706
+ description: 'Returns retained Lighthouse Scans for the Site, newest first, with the scores and lab vitals each one recorded. A failed Scan carries its failureKind and error, and its scores are null. Use one Scan id with the Scan read for the failing checks behind a score.',
550
707
  tags: ['Pages'],
551
708
  examples: [{
552
709
  request: { params: { siteId: 'site_01JXYZ' }, query: { limit: 25 } },
@@ -560,14 +717,30 @@ export const performanceScansOperation = defineOperation({
560
717
  strategy: 'mobile',
561
718
  status: 'complete',
562
719
  trigger: 'scheduled',
720
+ failureKind: null,
721
+ error: null,
722
+ lighthouseVersion: '13.5.0',
563
723
  scores: { perf: 82, a11y: 96, bestPractices: 92, seo: 98 },
564
724
  lcp: 2200,
565
725
  tbt: 180,
566
726
  cls: 0.08,
567
727
  completedAt: '2026-08-10T01:00:00.000Z',
568
- error: null,
728
+ }, {
729
+ id: 'scan_01JXZA',
730
+ url: 'https://example.com/blog',
731
+ strategy: 'mobile',
732
+ status: 'failed',
733
+ trigger: 'scheduled',
734
+ failureKind: 'timeout',
735
+ error: 'PSI timed out loading the page',
736
+ lighthouseVersion: null,
737
+ scores: { perf: null, a11y: null, bestPractices: null, seo: null },
738
+ lcp: null,
739
+ tbt: null,
740
+ cls: null,
741
+ completedAt: '2026-08-10T01:05:00.000Z',
569
742
  }],
570
- total: 1,
743
+ total: 2,
571
744
  limit: 25,
572
745
  },
573
746
  meta: { requestId: 'req_01JXYZ', version: '1.0' },
@@ -765,18 +938,11 @@ export const lcpObservedTraceSchema = schema.discriminatedUnion('_tag', [
765
938
  }
766
939
  });
767
940
  export const performanceScanDetailDataSchema = defineResponseObject({
768
- id: schema.string().min(1),
769
- url: schema.string().min(1),
770
- strategy: schema.string().min(1),
771
- status: schema.string().min(1),
772
- scores: schema.strictObject(scanScoresShape),
773
- lcp: nullableMetricSchema,
774
- tbt: nullableMetricSchema,
775
- cls: nullableMetricSchema,
941
+ ...scanBaseShape,
776
942
  fcp: nullableMetricSchema,
777
943
  speedIndex: nullableMetricSchema,
778
944
  ttfb: nullableMetricSchema,
779
- completedAt: schema.string().datetime().nullable(),
945
+ comparison: performanceScanComparisonSchema.producer,
780
946
  javascript: schema.strictObject(scanJavascriptShape).nullable(),
781
947
  observedTrace: lcpObservedTraceSchema.optional(),
782
948
  performanceIssues: schema.array(scanPerformanceIssueSchema.producer),
@@ -791,18 +957,11 @@ export const performanceScanDetailDataSchema = defineResponseObject({
791
957
  bestPractices: schema.array(scanFailingCheckSchema.producer),
792
958
  coverage: schema.strictObject(scanCoverageShape),
793
959
  }, {
794
- id: schema.string().min(1),
795
- url: schema.string().min(1),
796
- strategy: schema.string().min(1),
797
- status: schema.string().min(1),
798
- scores: schema.looseObject(scanScoresShape),
799
- lcp: nullableMetricSchema,
800
- tbt: nullableMetricSchema,
801
- cls: nullableMetricSchema,
960
+ ...scanBaseClientShape,
802
961
  fcp: nullableMetricSchema,
803
962
  speedIndex: nullableMetricSchema,
804
963
  ttfb: nullableMetricSchema,
805
- completedAt: schema.string().datetime().nullable(),
964
+ comparison: performanceScanComparisonSchema.client.optional(),
806
965
  javascript: schema.looseObject(scanJavascriptShape).nullable().optional(),
807
966
  observedTrace: lcpObservedTraceSchema.optional(),
808
967
  performanceIssues: schema.array(scanPerformanceIssueSchema.client),
@@ -854,7 +1013,7 @@ export const performanceScanDetailOperation = defineOperation({
854
1013
  lifecycle: { introduced: '1.2.0' },
855
1014
  docs: {
856
1015
  summary: 'Get one Lighthouse Scan',
857
- description: 'Returns one Scan with the evidence behind its scores: the wasteful assets, the repeat-visit cache policy, the third-party scripts, the LCP element, and every failing accessibility and best-practices check with the CSS selectors and DOM snippets that failed. Read coverage before quoting a total: elements and checks are both capped per Scan.',
1016
+ description: 'Returns one Scan with the evidence behind its scores: the wasteful assets, the repeat-visit cache policy, the third-party scripts, the LCP element, and every failing accessibility and best-practices check with the CSS selectors and DOM snippets that failed. It also returns why the Scan started, the Lighthouse version, and the comparison with the previous Scan of the same Page. A failed Scan carries its failureKind and error. Read coverage before quoting a total: elements and checks are both capped per Scan. A null blockingTime or wastedMs is unmeasured, never zero.',
858
1017
  tags: ['Pages'],
859
1018
  examples: [{
860
1019
  request: { params: { siteId: 'site_01JXYZ', scanId: 'scan_01JXYZ' } },
@@ -863,9 +1022,13 @@ export const performanceScanDetailOperation = defineOperation({
863
1022
  body: {
864
1023
  data: {
865
1024
  id: 'scan_01JXYZ',
866
- url: 'https://example.com/',
1025
+ url: 'https://example.com/pricing',
867
1026
  strategy: 'mobile',
868
1027
  status: 'complete',
1028
+ trigger: 'scheduled',
1029
+ failureKind: null,
1030
+ error: null,
1031
+ lighthouseVersion: '13.5.0',
869
1032
  scores: { perf: 82, a11y: 96, bestPractices: 92, seo: 98 },
870
1033
  lcp: 2200,
871
1034
  tbt: 180,
@@ -874,6 +1037,15 @@ export const performanceScanDetailOperation = defineOperation({
874
1037
  speedIndex: 2400,
875
1038
  ttfb: 210,
876
1039
  completedAt: '2026-08-10T01:00:00.000Z',
1040
+ comparison: {
1041
+ _tag: 'compared',
1042
+ previousScanId: 'scan_01JXXX',
1043
+ severity: 'regression',
1044
+ changes: [
1045
+ { metric: 'lcp', previous: 1500, current: 2200, delta: 700, severity: 'regression' },
1046
+ { metric: 'perf', previous: 85, current: 82, delta: -3, severity: 'neutral' },
1047
+ ],
1048
+ },
877
1049
  javascript: null,
878
1050
  performanceIssues: [],
879
1051
  cachePolicy: [],