@nuxtseo/protocol 0.1.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.
Files changed (44) hide show
  1. package/dist/analytics-codecs/analytics-row.d.ts +265 -0
  2. package/dist/analytics-codecs/analytics-row.js +175 -0
  3. package/dist/analytics-codecs/analytics-web-vitals.d.ts +57 -0
  4. package/dist/analytics-codecs/analytics-web-vitals.js +76 -0
  5. package/dist/analytics-codecs/capabilities.d.ts +35 -0
  6. package/dist/analytics-codecs/capabilities.js +76 -0
  7. package/dist/analytics-codecs/index.d.ts +3 -0
  8. package/dist/analytics-codecs/index.js +3 -0
  9. package/dist/cli-pairing.d.ts +67 -0
  10. package/dist/cli-pairing.js +52 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/v1/account.d.ts +715 -0
  14. package/dist/v1/account.js +123 -0
  15. package/dist/v1/actions.d.ts +2591 -0
  16. package/dist/v1/actions.js +460 -0
  17. package/dist/v1/analytics.d.ts +6145 -0
  18. package/dist/v1/analytics.js +296 -0
  19. package/dist/v1/audit.d.ts +4053 -0
  20. package/dist/v1/audit.js +609 -0
  21. package/dist/v1/backlinks.d.ts +1002 -0
  22. package/dist/v1/backlinks.js +239 -0
  23. package/dist/v1/content.d.ts +2745 -0
  24. package/dist/v1/content.js +326 -0
  25. package/dist/v1/core.d.ts +301 -0
  26. package/dist/v1/core.js +333 -0
  27. package/dist/v1/documents.d.ts +4 -0
  28. package/dist/v1/documents.js +216 -0
  29. package/dist/v1/gsc.d.ts +5172 -0
  30. package/dist/v1/gsc.js +583 -0
  31. package/dist/v1/index.d.ts +49 -0
  32. package/dist/v1/index.js +52 -0
  33. package/dist/v1/pages.d.ts +1404 -0
  34. package/dist/v1/pages.js +287 -0
  35. package/dist/v1/performance.d.ts +1379 -0
  36. package/dist/v1/performance.js +198 -0
  37. package/dist/v1/research.d.ts +1173 -0
  38. package/dist/v1/research.js +174 -0
  39. package/dist/v1/sitemaps.d.ts +1506 -0
  40. package/dist/v1/sitemaps.js +199 -0
  41. package/dist/v1/sites.d.ts +591 -0
  42. package/dist/v1/sites.js +101 -0
  43. package/generated/openapi.v1.json +1 -0
  44. package/package.json +131 -0
@@ -0,0 +1,609 @@
1
+ import { z as schema } from 'zod';
2
+ import { defineOperation, defineRegistry, defineResponseObject, defineSuccessEnvelope, publicV1ErrorEnvelope, publicV1StandardRatePolicy, } from './core.js';
3
+ export const auditSiteParamsSchema = schema.strictObject({
4
+ siteId: schema.string().min(1),
5
+ });
6
+ export const auditChangesQuerySchema = schema.strictObject({
7
+ from: schema.coerce.number().int().positive().optional(),
8
+ to: schema.coerce.number().int().positive().optional(),
9
+ });
10
+ export const auditChangeExampleSchema = defineResponseObject({
11
+ url: schema.string(),
12
+ path: schema.string(),
13
+ }, {
14
+ url: schema.string(),
15
+ path: schema.string(),
16
+ });
17
+ export const auditChangeCheckSchema = defineResponseObject({
18
+ key: schema.string(),
19
+ label: schema.string(),
20
+ severity: schema.enum(['error', 'warning', 'notice']),
21
+ subject: schema.enum(['page', 'link-target']),
22
+ pages: schema.number(),
23
+ }, {
24
+ key: schema.string(),
25
+ label: schema.string(),
26
+ severity: schema.enum(['error', 'warning', 'notice']),
27
+ subject: schema.enum(['page', 'link-target']),
28
+ pages: schema.number(),
29
+ });
30
+ export const auditChangeGroupSchema = defineResponseObject({
31
+ key: schema.string(),
32
+ label: schema.string(),
33
+ severity: schema.enum(['error', 'warning', 'notice']),
34
+ subject: schema.enum(['page', 'link-target']),
35
+ pages: schema.number(),
36
+ examples: schema.array(auditChangeExampleSchema.producer),
37
+ checks: schema.array(auditChangeCheckSchema.producer),
38
+ spike: schema.boolean(),
39
+ }, {
40
+ key: schema.string(),
41
+ label: schema.string(),
42
+ severity: schema.enum(['error', 'warning', 'notice']),
43
+ subject: schema.enum(['page', 'link-target']),
44
+ pages: schema.number(),
45
+ examples: schema.array(auditChangeExampleSchema.client),
46
+ checks: schema.array(auditChangeCheckSchema.client),
47
+ spike: schema.boolean(),
48
+ });
49
+ export const auditChangeFacetSchema = defineResponseObject({
50
+ groups: schema.array(auditChangeGroupSchema.producer),
51
+ pages: schema.number(),
52
+ groupTotal: schema.number(),
53
+ checkTotal: schema.number(),
54
+ }, {
55
+ groups: schema.array(auditChangeGroupSchema.client),
56
+ pages: schema.number(),
57
+ groupTotal: schema.number(),
58
+ checkTotal: schema.number(),
59
+ });
60
+ export const auditChangeGenerationSchema = defineResponseObject({
61
+ generation: schema.number(),
62
+ buildId: schema.string().nullable(),
63
+ completedAt: schema.string().nullable(),
64
+ }, {
65
+ generation: schema.number(),
66
+ buildId: schema.string().nullable(),
67
+ completedAt: schema.string().nullable(),
68
+ });
69
+ export const auditRegressionHistoryPointSchema = defineResponseObject({
70
+ ...auditChangeGenerationSchema.producer.shape,
71
+ regressedUrls: schema.number(),
72
+ }, {
73
+ ...auditChangeGenerationSchema.client.shape,
74
+ regressedUrls: schema.number(),
75
+ });
76
+ export const auditChangesDataSchema = defineResponseObject({
77
+ state: schema.enum(['changes', 'no-changes', 'no-baseline', 'no-crawl']),
78
+ verdict: schema.string(),
79
+ window: schema.strictObject({
80
+ observedAt: schema.string().nullable(),
81
+ comparedTo: schema.string().nullable(),
82
+ }),
83
+ regressions: auditChangeFacetSchema.producer,
84
+ resolved: auditChangeFacetSchema.producer,
85
+ pages: auditChangeFacetSchema.producer,
86
+ sampled: schema.boolean(),
87
+ generations: schema.array(auditChangeGenerationSchema.producer),
88
+ regressionHistory: schema.array(auditRegressionHistoryPointSchema.producer),
89
+ comparison: schema.strictObject({
90
+ from: auditChangeGenerationSchema.producer,
91
+ to: auditChangeGenerationSchema.producer,
92
+ }).nullable(),
93
+ clicksAtRisk: schema.number().nullable(),
94
+ }, {
95
+ state: schema.enum(['changes', 'no-changes', 'no-baseline', 'no-crawl']),
96
+ verdict: schema.string(),
97
+ window: schema.looseObject({
98
+ observedAt: schema.string().nullable(),
99
+ comparedTo: schema.string().nullable(),
100
+ }),
101
+ regressions: auditChangeFacetSchema.client,
102
+ resolved: auditChangeFacetSchema.client,
103
+ pages: auditChangeFacetSchema.client,
104
+ sampled: schema.boolean(),
105
+ generations: schema.array(auditChangeGenerationSchema.client),
106
+ regressionHistory: schema.array(auditRegressionHistoryPointSchema.client),
107
+ comparison: schema.looseObject({
108
+ from: auditChangeGenerationSchema.client,
109
+ to: auditChangeGenerationSchema.client,
110
+ }).nullable(),
111
+ clicksAtRisk: schema.number().nullable(),
112
+ });
113
+ export const auditChangesResponseSchema = defineSuccessEnvelope(auditChangesDataSchema);
114
+ export const contentDecayRowSchema = defineResponseObject({
115
+ url: schema.string(),
116
+ path: schema.string(),
117
+ clicks: schema.number(),
118
+ prevClicks: schema.number(),
119
+ clicksDelta: schema.number(),
120
+ clicksChangePct: schema.number(),
121
+ impressions: schema.number(),
122
+ prevImpressions: schema.number(),
123
+ position: schema.number().nullable(),
124
+ prevPosition: schema.number().nullable(),
125
+ }, {
126
+ url: schema.string(),
127
+ path: schema.string(),
128
+ clicks: schema.number(),
129
+ prevClicks: schema.number(),
130
+ clicksDelta: schema.number(),
131
+ clicksChangePct: schema.number(),
132
+ impressions: schema.number(),
133
+ prevImpressions: schema.number(),
134
+ position: schema.number().nullable(),
135
+ prevPosition: schema.number().nullable(),
136
+ });
137
+ export const auditContentDecayDataSchema = defineResponseObject({
138
+ connected: schema.boolean(),
139
+ rows: schema.array(contentDecayRowSchema.producer),
140
+ total: schema.number(),
141
+ clicksLost: schema.number(),
142
+ window: schema.strictObject({
143
+ startDate: schema.string(),
144
+ endDate: schema.string(),
145
+ prevStartDate: schema.string(),
146
+ prevEndDate: schema.string(),
147
+ }).nullable(),
148
+ truncated: schema.boolean(),
149
+ }, {
150
+ connected: schema.boolean(),
151
+ rows: schema.array(contentDecayRowSchema.client),
152
+ total: schema.number(),
153
+ clicksLost: schema.number(),
154
+ window: schema.looseObject({
155
+ startDate: schema.string(),
156
+ endDate: schema.string(),
157
+ prevStartDate: schema.string(),
158
+ prevEndDate: schema.string(),
159
+ }).nullable(),
160
+ truncated: schema.boolean(),
161
+ });
162
+ export const auditContentDecayResponseSchema = defineSuccessEnvelope(auditContentDecayDataSchema);
163
+ export const duplicateMemberSchema = defineResponseObject({
164
+ url: schema.string(),
165
+ path: schema.string(),
166
+ clicks: schema.number().nullable(),
167
+ impressions: schema.number().nullable(),
168
+ }, {
169
+ url: schema.string(),
170
+ path: schema.string(),
171
+ clicks: schema.number().nullable(),
172
+ impressions: schema.number().nullable(),
173
+ });
174
+ export const duplicateClusterSchema = defineResponseObject({
175
+ pathPattern: schema.string(),
176
+ size: schema.number(),
177
+ exactDuplicates: schema.number(),
178
+ members: schema.array(duplicateMemberSchema.producer),
179
+ keepPath: schema.string().nullable(),
180
+ titles: schema.strictObject({
181
+ distinct: schema.number(),
182
+ sampled: schema.number(),
183
+ verdict: schema.enum(['distinct', 'shared', 'mixed', 'unknown']),
184
+ }),
185
+ }, {
186
+ pathPattern: schema.string(),
187
+ size: schema.number(),
188
+ exactDuplicates: schema.number(),
189
+ members: schema.array(duplicateMemberSchema.client),
190
+ keepPath: schema.string().nullable(),
191
+ titles: schema.looseObject({
192
+ distinct: schema.number(),
193
+ sampled: schema.number(),
194
+ verdict: schema.enum(['distinct', 'shared', 'mixed', 'unknown']),
195
+ }),
196
+ });
197
+ export const auditDuplicateClustersDataSchema = defineResponseObject({
198
+ connected: schema.boolean(),
199
+ clusters: schema.array(duplicateClusterSchema.producer),
200
+ total: schema.number(),
201
+ pagesAffected: schema.number(),
202
+ }, {
203
+ connected: schema.boolean(),
204
+ clusters: schema.array(duplicateClusterSchema.client),
205
+ total: schema.number(),
206
+ pagesAffected: schema.number(),
207
+ });
208
+ export const auditDuplicateClustersResponseSchema = defineSuccessEnvelope(auditDuplicateClustersDataSchema);
209
+ export const linkOpportunityRowSchema = defineResponseObject({
210
+ sourceUrl: schema.string(),
211
+ sourcePath: schema.string(),
212
+ sourceRank: schema.number().nullable(),
213
+ targetUrl: schema.string(),
214
+ targetPath: schema.string(),
215
+ phrase: schema.string(),
216
+ snippet: schema.string(),
217
+ impressions: schema.number(),
218
+ clicks: schema.number(),
219
+ position: schema.number(),
220
+ }, {
221
+ sourceUrl: schema.string(),
222
+ sourcePath: schema.string(),
223
+ sourceRank: schema.number().nullable(),
224
+ targetUrl: schema.string(),
225
+ targetPath: schema.string(),
226
+ phrase: schema.string(),
227
+ snippet: schema.string(),
228
+ impressions: schema.number(),
229
+ clicks: schema.number(),
230
+ position: schema.number(),
231
+ });
232
+ export const auditLinkOpportunitiesDataSchema = defineResponseObject({
233
+ connected: schema.boolean(),
234
+ textUnavailable: schema.boolean(),
235
+ rows: schema.array(linkOpportunityRowSchema.producer),
236
+ total: schema.number(),
237
+ pagesScanned: schema.number(),
238
+ pagesTotal: schema.number(),
239
+ }, {
240
+ connected: schema.boolean(),
241
+ textUnavailable: schema.boolean(),
242
+ rows: schema.array(linkOpportunityRowSchema.client),
243
+ total: schema.number(),
244
+ pagesScanned: schema.number(),
245
+ pagesTotal: schema.number(),
246
+ });
247
+ export const auditLinkOpportunitiesResponseSchema = defineSuccessEnvelope(auditLinkOpportunitiesDataSchema);
248
+ export const templateSlotSchema = defineResponseObject({
249
+ url: schema.string(),
250
+ path: schema.string(),
251
+ templateLinks: schema.number(),
252
+ sourcePages: schema.number(),
253
+ locations: schema.array(schema.string()),
254
+ share: schema.number(),
255
+ clicks: schema.number().nullable(),
256
+ }, {
257
+ url: schema.string(),
258
+ path: schema.string(),
259
+ templateLinks: schema.number(),
260
+ sourcePages: schema.number(),
261
+ locations: schema.array(schema.string()),
262
+ share: schema.number(),
263
+ clicks: schema.number().nullable(),
264
+ });
265
+ export const deadEndGroupSchema = defineResponseObject({
266
+ label: schema.string(),
267
+ pages: schema.number(),
268
+ examples: schema.array(schema.string()),
269
+ topRank: schema.number().nullable(),
270
+ singleUrl: schema.string().nullable(),
271
+ }, {
272
+ label: schema.string(),
273
+ pages: schema.number(),
274
+ examples: schema.array(schema.string()),
275
+ topRank: schema.number().nullable(),
276
+ singleUrl: schema.string().nullable(),
277
+ });
278
+ export const genericAnchorSchema = defineResponseObject({
279
+ anchor: schema.string(),
280
+ links: schema.number(),
281
+ targets: schema.number(),
282
+ exampleTargets: schema.array(schema.string()),
283
+ }, {
284
+ anchor: schema.string(),
285
+ links: schema.number(),
286
+ targets: schema.number(),
287
+ exampleTargets: schema.array(schema.string()),
288
+ });
289
+ export const auditLinkStructureDataSchema = defineResponseObject({
290
+ gscConnected: schema.boolean(),
291
+ internalLinks: schema.number(),
292
+ templateLinks: schema.number(),
293
+ templateTargets: schema.number(),
294
+ editorialLinks: schema.number(),
295
+ editorialTargets: schema.number(),
296
+ templateSlots: schema.array(templateSlotSchema.producer),
297
+ templateSlotTotal: schema.number(),
298
+ deadEnds: schema.array(deadEndGroupSchema.producer),
299
+ deadEndGroupTotal: schema.number(),
300
+ deadEndPageTotal: schema.number(),
301
+ deadEndScanTruncated: schema.boolean(),
302
+ genericAnchors: schema.array(genericAnchorSchema.producer),
303
+ genericAnchorTotal: schema.number(),
304
+ genericAnchorLinks: schema.number(),
305
+ }, {
306
+ gscConnected: schema.boolean(),
307
+ internalLinks: schema.number(),
308
+ templateLinks: schema.number(),
309
+ templateTargets: schema.number(),
310
+ editorialLinks: schema.number(),
311
+ editorialTargets: schema.number(),
312
+ templateSlots: schema.array(templateSlotSchema.client),
313
+ templateSlotTotal: schema.number(),
314
+ deadEnds: schema.array(deadEndGroupSchema.client),
315
+ deadEndGroupTotal: schema.number(),
316
+ deadEndPageTotal: schema.number(),
317
+ deadEndScanTruncated: schema.boolean(),
318
+ genericAnchors: schema.array(genericAnchorSchema.client),
319
+ genericAnchorTotal: schema.number(),
320
+ genericAnchorLinks: schema.number(),
321
+ });
322
+ export const auditLinkStructureResponseSchema = defineSuccessEnvelope(auditLinkStructureDataSchema);
323
+ const auditReadSemantics = {
324
+ kind: 'query',
325
+ sideEffects: 'none',
326
+ retry: 'idempotent',
327
+ readConsistency: 'primary',
328
+ };
329
+ const auditReadAuth = {
330
+ credentials: ['session', 'api_token'],
331
+ scopes: ['audit:read'],
332
+ minimumRole: 'viewer',
333
+ ownership: 'site',
334
+ };
335
+ const auditReadErrors = [
336
+ 'invalid_request',
337
+ 'unauthorized',
338
+ 'auth_expired',
339
+ 'forbidden',
340
+ 'not_found',
341
+ 'rate_limited',
342
+ 'provider_unavailable',
343
+ 'contract_violation',
344
+ 'internal_error',
345
+ ];
346
+ const auditReadResources = {
347
+ reads: [{ type: 'site.audit', idFrom: 'params.siteId' }],
348
+ changes: [],
349
+ };
350
+ const auditReadPolicy = {
351
+ rate: publicV1StandardRatePolicy,
352
+ idempotency: { _tag: 'none' },
353
+ entitlement: null,
354
+ meter: null,
355
+ };
356
+ export const auditChangesOperation = defineOperation({
357
+ id: 'audit.changes',
358
+ method: 'GET',
359
+ path: '/api/v1/sites/{siteId}/audit/changes',
360
+ visibility: 'public',
361
+ semantics: auditReadSemantics,
362
+ auth: auditReadAuth,
363
+ request: {
364
+ params: auditSiteParamsSchema,
365
+ query: auditChangesQuerySchema,
366
+ headers: null,
367
+ body: null,
368
+ },
369
+ responses: { 200: auditChangesResponseSchema },
370
+ errors: auditReadErrors,
371
+ errorResponse: publicV1ErrorEnvelope,
372
+ resources: auditReadResources,
373
+ policy: auditReadPolicy,
374
+ lifecycle: { introduced: '1.0.0' },
375
+ docs: {
376
+ summary: 'Read audit changes',
377
+ description: 'Compares published crawl generations and keeps no crawl, no baseline, no changes and observed changes as distinct states.',
378
+ tags: ['Audit'],
379
+ examples: [{
380
+ request: { params: { siteId: 'site_01JXYZ' }, query: {} },
381
+ response: {
382
+ status: 200,
383
+ body: {
384
+ data: {
385
+ state: 'no-baseline',
386
+ verdict: '',
387
+ window: { observedAt: '2026-08-10T01:30:00.000Z', comparedTo: null },
388
+ regressions: { groups: [], pages: 0, groupTotal: 0, checkTotal: 0 },
389
+ resolved: { groups: [], pages: 0, groupTotal: 0, checkTotal: 0 },
390
+ pages: { groups: [], pages: 0, groupTotal: 0, checkTotal: 0 },
391
+ sampled: false,
392
+ generations: [{ generation: 4, buildId: 'build_01JXYZ', completedAt: '2026-08-10T01:30:00.000Z' }],
393
+ regressionHistory: [],
394
+ comparison: null,
395
+ clicksAtRisk: null,
396
+ },
397
+ meta: { requestId: 'req_01JXYZ', version: '1.0' },
398
+ },
399
+ },
400
+ }],
401
+ },
402
+ });
403
+ export const auditContentDecayOperation = defineOperation({
404
+ id: 'audit.content.decay',
405
+ method: 'GET',
406
+ path: '/api/v1/sites/{siteId}/audit/content-decay',
407
+ visibility: 'public',
408
+ semantics: auditReadSemantics,
409
+ auth: auditReadAuth,
410
+ request: { params: auditSiteParamsSchema, query: null, headers: null, body: null },
411
+ responses: { 200: auditContentDecayResponseSchema },
412
+ errors: auditReadErrors,
413
+ errorResponse: publicV1ErrorEnvelope,
414
+ resources: auditReadResources,
415
+ policy: auditReadPolicy,
416
+ lifecycle: { introduced: '1.0.0' },
417
+ docs: {
418
+ summary: 'Read content decay',
419
+ description: 'Lists crawled pages losing Search Console clicks against the prior equal-length window, including coverage state and aggregate loss.',
420
+ tags: ['Audit'],
421
+ examples: [{
422
+ request: { params: { siteId: 'site_01JXYZ' } },
423
+ response: {
424
+ status: 200,
425
+ body: {
426
+ data: {
427
+ connected: true,
428
+ rows: [{
429
+ url: 'https://example.com/guide',
430
+ path: '/guide',
431
+ clicks: 12,
432
+ prevClicks: 30,
433
+ clicksDelta: -18,
434
+ clicksChangePct: -60,
435
+ impressions: 400,
436
+ prevImpressions: 620,
437
+ position: 8.4,
438
+ prevPosition: 5.1,
439
+ }],
440
+ total: 1,
441
+ clicksLost: 18,
442
+ window: {
443
+ startDate: '2026-07-13',
444
+ endDate: '2026-08-09',
445
+ prevStartDate: '2026-06-15',
446
+ prevEndDate: '2026-07-12',
447
+ },
448
+ truncated: false,
449
+ },
450
+ meta: { requestId: 'req_01JXYZ', version: '1.0' },
451
+ },
452
+ },
453
+ }],
454
+ },
455
+ });
456
+ export const auditDuplicateClustersOperation = defineOperation({
457
+ id: 'audit.duplicate.clusters',
458
+ method: 'GET',
459
+ path: '/api/v1/sites/{siteId}/audit/duplicates',
460
+ visibility: 'public',
461
+ semantics: auditReadSemantics,
462
+ auth: auditReadAuth,
463
+ request: { params: auditSiteParamsSchema, query: null, headers: null, body: null },
464
+ responses: { 200: auditDuplicateClustersResponseSchema },
465
+ errors: auditReadErrors,
466
+ errorResponse: publicV1ErrorEnvelope,
467
+ resources: auditReadResources,
468
+ policy: auditReadPolicy,
469
+ lifecycle: { introduced: '1.0.0' },
470
+ docs: {
471
+ summary: 'Read duplicate clusters',
472
+ description: 'Returns unresolved near-duplicate route families, member evidence, title distinctness and an optional traffic-backed keep candidate.',
473
+ tags: ['Audit'],
474
+ examples: [{
475
+ request: { params: { siteId: 'site_01JXYZ' } },
476
+ response: {
477
+ status: 200,
478
+ body: {
479
+ data: {
480
+ connected: true,
481
+ clusters: [{
482
+ pathPattern: '/guides/*',
483
+ size: 3,
484
+ exactDuplicates: 2,
485
+ members: [{ url: 'https://example.com/guides/alpha', path: '/guides/alpha', clicks: 14, impressions: 200 }],
486
+ keepPath: '/guides/alpha',
487
+ titles: { distinct: 1, sampled: 3, verdict: 'shared' },
488
+ }],
489
+ total: 1,
490
+ pagesAffected: 3,
491
+ },
492
+ meta: { requestId: 'req_01JXYZ', version: '1.0' },
493
+ },
494
+ },
495
+ }],
496
+ },
497
+ });
498
+ export const auditLinkOpportunitiesOperation = defineOperation({
499
+ id: 'audit.link.opportunities',
500
+ method: 'GET',
501
+ path: '/api/v1/sites/{siteId}/audit/link-opportunities',
502
+ visibility: 'public',
503
+ semantics: auditReadSemantics,
504
+ auth: auditReadAuth,
505
+ request: { params: auditSiteParamsSchema, query: null, headers: null, body: null },
506
+ responses: { 200: auditLinkOpportunitiesResponseSchema },
507
+ errors: auditReadErrors,
508
+ errorResponse: publicV1ErrorEnvelope,
509
+ resources: auditReadResources,
510
+ policy: auditReadPolicy,
511
+ lifecycle: { introduced: '1.0.0' },
512
+ docs: {
513
+ summary: 'Read link opportunities',
514
+ description: 'Returns source pages that mention a target query without linking to its ranking page, with crawl coverage disclosed.',
515
+ tags: ['Audit'],
516
+ examples: [{
517
+ request: { params: { siteId: 'site_01JXYZ' } },
518
+ response: {
519
+ status: 200,
520
+ body: {
521
+ data: {
522
+ connected: true,
523
+ textUnavailable: false,
524
+ rows: [{
525
+ sourceUrl: 'https://example.com/handbook',
526
+ sourcePath: '/handbook',
527
+ sourceRank: 72,
528
+ targetUrl: 'https://example.com/guide',
529
+ targetPath: '/guide',
530
+ phrase: 'technical SEO guide',
531
+ snippet: 'Read our technical SEO guide before launch.',
532
+ impressions: 900,
533
+ clicks: 18,
534
+ position: 8.2,
535
+ }],
536
+ total: 1,
537
+ pagesScanned: 152,
538
+ pagesTotal: 154,
539
+ },
540
+ meta: { requestId: 'req_01JXYZ', version: '1.0' },
541
+ },
542
+ },
543
+ }],
544
+ },
545
+ });
546
+ export const auditLinkStructureOperation = defineOperation({
547
+ id: 'audit.link.structure',
548
+ method: 'GET',
549
+ path: '/api/v1/sites/{siteId}/audit/link-structure',
550
+ visibility: 'public',
551
+ semantics: auditReadSemantics,
552
+ auth: auditReadAuth,
553
+ request: { params: auditSiteParamsSchema, query: null, headers: null, body: null },
554
+ responses: { 200: auditLinkStructureResponseSchema },
555
+ errors: auditReadErrors,
556
+ errorResponse: publicV1ErrorEnvelope,
557
+ resources: auditReadResources,
558
+ policy: auditReadPolicy,
559
+ lifecycle: { introduced: '1.0.0' },
560
+ docs: {
561
+ summary: 'Read link structure',
562
+ description: 'Summarizes internal link allocation across template slots, dead-end route families and generic editorial anchors.',
563
+ tags: ['Audit'],
564
+ examples: [{
565
+ request: { params: { siteId: 'site_01JXYZ' } },
566
+ response: {
567
+ status: 200,
568
+ body: {
569
+ data: {
570
+ gscConnected: true,
571
+ internalLinks: 1400,
572
+ templateLinks: 900,
573
+ templateTargets: 12,
574
+ editorialLinks: 500,
575
+ editorialTargets: 120,
576
+ templateSlots: [{
577
+ url: 'https://example.com/pricing',
578
+ path: '/pricing',
579
+ templateLinks: 120,
580
+ sourcePages: 118,
581
+ locations: ['nav', 'footer'],
582
+ share: 0.13,
583
+ clicks: 40,
584
+ }],
585
+ templateSlotTotal: 12,
586
+ deadEnds: [{ label: '/archive/*', pages: 5, examples: ['/archive/2024'], topRank: 18, singleUrl: null }],
587
+ deadEndGroupTotal: 1,
588
+ deadEndPageTotal: 5,
589
+ deadEndScanTruncated: false,
590
+ genericAnchors: [{ anchor: 'click here', links: 9, targets: 4, exampleTargets: ['/guide'] }],
591
+ genericAnchorTotal: 1,
592
+ genericAnchorLinks: 9,
593
+ },
594
+ meta: { requestId: 'req_01JXYZ', version: '1.0' },
595
+ },
596
+ },
597
+ }],
598
+ },
599
+ });
600
+ export const auditV1Registry = defineRegistry({
601
+ feature: 'audit',
602
+ operations: {
603
+ changes: auditChangesOperation,
604
+ contentDecay: auditContentDecayOperation,
605
+ duplicateClusters: auditDuplicateClustersOperation,
606
+ linkOpportunities: auditLinkOpportunitiesOperation,
607
+ linkStructure: auditLinkStructureOperation,
608
+ },
609
+ });