@nuxtseo/protocol 0.1.0 → 0.1.1
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/actions.d.ts +77 -0
- package/dist/v1/actions.js +12 -0
- package/dist/v1/gsc.d.ts +120 -268
- package/dist/v1/gsc.js +23 -50
- package/dist/v1/pages.d.ts +10 -0
- package/dist/v1/pages.js +2 -2
- package/dist/v1/research.d.ts +2221 -180
- package/dist/v1/research.js +328 -0
- package/generated/openapi.v1.json +1 -1
- package/package.json +2 -2
package/dist/v1/research.js
CHANGED
|
@@ -3,6 +3,10 @@ import { defineOperation, defineRegistry, defineResponseObject, defineSuccessEnv
|
|
|
3
3
|
export const researchSiteParamsSchema = schema.strictObject({
|
|
4
4
|
siteId: schema.string().min(1),
|
|
5
5
|
});
|
|
6
|
+
const queryBooleanSchema = schema.union([
|
|
7
|
+
schema.boolean(),
|
|
8
|
+
schema.enum(['true', 'false', '1', '0']),
|
|
9
|
+
]).transform(value => value === true || value === 'true' || value === '1');
|
|
6
10
|
export const researchMetricsSchema = defineResponseObject({
|
|
7
11
|
organicTraffic: schema.number().nullable(),
|
|
8
12
|
totalKeywords: schema.number().nullable(),
|
|
@@ -109,6 +113,223 @@ export const storedResearchOverviewDataSchema = defineResponseObject({
|
|
|
109
113
|
competitors: schema.array(researchCompetitorSchema.client),
|
|
110
114
|
});
|
|
111
115
|
export const storedResearchOverviewResponseSchema = defineSuccessEnvelope(storedResearchOverviewDataSchema);
|
|
116
|
+
export const keywordResearchQuerySchema = schema.strictObject({
|
|
117
|
+
topic: schema.string().trim().min(2).max(200),
|
|
118
|
+
minVolume: schema.coerce.number().int().nonnegative().default(10),
|
|
119
|
+
maxVolume: schema.coerce.number().int().nonnegative().default(10_000),
|
|
120
|
+
minDifficulty: schema.coerce.number().min(0).max(100).default(0),
|
|
121
|
+
maxDifficulty: schema.coerce.number().min(0).max(100).default(60),
|
|
122
|
+
intent: schema.string().trim().min(1).optional(),
|
|
123
|
+
includeRelated: queryBooleanSchema.default(true),
|
|
124
|
+
verbose: queryBooleanSchema.default(false),
|
|
125
|
+
limit: schema.coerce.number().int().min(1).max(100).default(20),
|
|
126
|
+
locationCode: schema.coerce.number().int().positive().default(2840),
|
|
127
|
+
}).superRefine((value, context) => {
|
|
128
|
+
if (value.maxVolume < value.minVolume)
|
|
129
|
+
context.addIssue({ code: 'custom', path: ['maxVolume'], message: 'maxVolume must be at least minVolume.' });
|
|
130
|
+
if (value.maxDifficulty < value.minDifficulty)
|
|
131
|
+
context.addIssue({ code: 'custom', path: ['maxDifficulty'], message: 'maxDifficulty must be at least minDifficulty.' });
|
|
132
|
+
});
|
|
133
|
+
const keywordTrendPointSchema = defineResponseObject({
|
|
134
|
+
year: schema.number().int(),
|
|
135
|
+
month: schema.number().int().min(1).max(12),
|
|
136
|
+
volume: schema.number().int().nonnegative(),
|
|
137
|
+
});
|
|
138
|
+
const liveKeywordSchema = defineResponseObject({
|
|
139
|
+
keyword: schema.string().min(1),
|
|
140
|
+
volume: schema.number().nullable(),
|
|
141
|
+
difficulty: schema.number().nullable(),
|
|
142
|
+
intent: schema.string().nullable(),
|
|
143
|
+
secondaryIntents: schema.array(schema.string()).nullable(),
|
|
144
|
+
cpc: schema.number().nullable(),
|
|
145
|
+
competition: schema.number().nullable(),
|
|
146
|
+
competitionLevel: schema.enum(['LOW', 'MEDIUM', 'HIGH']).nullable(),
|
|
147
|
+
lowBid: schema.number().nullable(),
|
|
148
|
+
highBid: schema.number().nullable(),
|
|
149
|
+
trend: schema.array(keywordTrendPointSchema.producer).nullable(),
|
|
150
|
+
serpFeatures: schema.array(schema.string()).nullable(),
|
|
151
|
+
backlinks: schema.number().nullable(),
|
|
152
|
+
referringDomains: schema.number().nullable(),
|
|
153
|
+
});
|
|
154
|
+
const serpFallbackResultSchema = defineResponseObject({
|
|
155
|
+
position: schema.number(),
|
|
156
|
+
title: schema.string(),
|
|
157
|
+
domain: schema.string(),
|
|
158
|
+
url: schema.string(),
|
|
159
|
+
});
|
|
160
|
+
const keywordResearchEvidenceProducerSchema = schema.discriminatedUnion('_tag', [
|
|
161
|
+
schema.strictObject({
|
|
162
|
+
_tag: schema.literal('no-provider'),
|
|
163
|
+
provider: schema.null(),
|
|
164
|
+
spend: schema.strictObject({ key: schema.literal('dataforseo'), units: schema.literal(0) }),
|
|
165
|
+
}),
|
|
166
|
+
schema.strictObject({
|
|
167
|
+
_tag: schema.literal('cache'),
|
|
168
|
+
provider: schema.literal('dataforseo'),
|
|
169
|
+
spend: schema.strictObject({ key: schema.literal('dataforseo'), units: schema.literal(0) }),
|
|
170
|
+
}),
|
|
171
|
+
schema.strictObject({
|
|
172
|
+
_tag: schema.literal('live'),
|
|
173
|
+
provider: schema.literal('dataforseo'),
|
|
174
|
+
spend: schema.strictObject({ key: schema.literal('dataforseo'), units: schema.literal(1) }),
|
|
175
|
+
}),
|
|
176
|
+
]);
|
|
177
|
+
const keywordResearchEvidenceClientSchema = schema.discriminatedUnion('_tag', [
|
|
178
|
+
schema.looseObject({
|
|
179
|
+
_tag: schema.literal('no-provider'),
|
|
180
|
+
provider: schema.null(),
|
|
181
|
+
spend: schema.looseObject({ key: schema.literal('dataforseo'), units: schema.literal(0) }),
|
|
182
|
+
}),
|
|
183
|
+
schema.looseObject({
|
|
184
|
+
_tag: schema.literal('cache'),
|
|
185
|
+
provider: schema.literal('dataforseo'),
|
|
186
|
+
spend: schema.looseObject({ key: schema.literal('dataforseo'), units: schema.literal(0) }),
|
|
187
|
+
}),
|
|
188
|
+
schema.looseObject({
|
|
189
|
+
_tag: schema.literal('live'),
|
|
190
|
+
provider: schema.literal('dataforseo'),
|
|
191
|
+
spend: schema.looseObject({ key: schema.literal('dataforseo'), units: schema.literal(1) }),
|
|
192
|
+
}),
|
|
193
|
+
]);
|
|
194
|
+
const keywordResearchDataSchema = defineResponseObject({
|
|
195
|
+
topic: schema.string().min(1),
|
|
196
|
+
keywords: schema.array(liveKeywordSchema.producer),
|
|
197
|
+
totalFound: schema.number().int().nonnegative(),
|
|
198
|
+
filters: schema.strictObject({
|
|
199
|
+
minVolume: schema.number(),
|
|
200
|
+
maxVolume: schema.number(),
|
|
201
|
+
minDifficulty: schema.number(),
|
|
202
|
+
maxDifficulty: schema.number(),
|
|
203
|
+
intent: schema.string().optional(),
|
|
204
|
+
}),
|
|
205
|
+
message: schema.string().optional(),
|
|
206
|
+
tip: schema.string().optional(),
|
|
207
|
+
serpFallback: schema.strictObject({
|
|
208
|
+
message: schema.string(),
|
|
209
|
+
topResults: schema.array(serpFallbackResultSchema.producer),
|
|
210
|
+
tip: schema.string(),
|
|
211
|
+
}).nullable().optional(),
|
|
212
|
+
evidence: keywordResearchEvidenceProducerSchema,
|
|
213
|
+
}, {
|
|
214
|
+
topic: schema.string().min(1),
|
|
215
|
+
keywords: schema.array(liveKeywordSchema.client),
|
|
216
|
+
totalFound: schema.number().int().nonnegative(),
|
|
217
|
+
filters: schema.looseObject({
|
|
218
|
+
minVolume: schema.number(),
|
|
219
|
+
maxVolume: schema.number(),
|
|
220
|
+
minDifficulty: schema.number(),
|
|
221
|
+
maxDifficulty: schema.number(),
|
|
222
|
+
intent: schema.string().optional(),
|
|
223
|
+
}),
|
|
224
|
+
message: schema.string().optional(),
|
|
225
|
+
tip: schema.string().optional(),
|
|
226
|
+
serpFallback: schema.looseObject({
|
|
227
|
+
message: schema.string(),
|
|
228
|
+
topResults: schema.array(serpFallbackResultSchema.client),
|
|
229
|
+
tip: schema.string(),
|
|
230
|
+
}).nullable().optional(),
|
|
231
|
+
evidence: keywordResearchEvidenceClientSchema,
|
|
232
|
+
});
|
|
233
|
+
export const keywordResearchResponseSchema = defineSuccessEnvelope(keywordResearchDataSchema);
|
|
234
|
+
export const serpResearchQuerySchema = schema.strictObject({
|
|
235
|
+
keyword: schema.string().trim().min(2).max(200),
|
|
236
|
+
depth: schema.coerce.number().int().min(1).max(20).default(10),
|
|
237
|
+
locationCode: schema.coerce.number().int().positive().default(2840),
|
|
238
|
+
});
|
|
239
|
+
const serpOrganicResultSchema = defineResponseObject({
|
|
240
|
+
position: schema.number(),
|
|
241
|
+
url: schema.string(),
|
|
242
|
+
title: schema.string(),
|
|
243
|
+
domain: schema.string(),
|
|
244
|
+
description: schema.string().optional(),
|
|
245
|
+
});
|
|
246
|
+
const serpResearchDataSchema = defineResponseObject({
|
|
247
|
+
keyword: schema.string().min(1),
|
|
248
|
+
depth: schema.number().int().min(1).max(20),
|
|
249
|
+
results: schema.array(serpOrganicResultSchema.producer),
|
|
250
|
+
serpFeatures: schema.array(schema.string()),
|
|
251
|
+
hasAiOverview: schema.boolean(),
|
|
252
|
+
hasFeaturedSnippet: schema.boolean(),
|
|
253
|
+
hasLocalPack: schema.boolean(),
|
|
254
|
+
hasPeopleAlsoAsk: schema.boolean(),
|
|
255
|
+
fetchedAt: schema.string().datetime(),
|
|
256
|
+
cached: schema.boolean(),
|
|
257
|
+
}, {
|
|
258
|
+
keyword: schema.string().min(1),
|
|
259
|
+
depth: schema.number().int().min(1).max(20),
|
|
260
|
+
results: schema.array(serpOrganicResultSchema.client),
|
|
261
|
+
serpFeatures: schema.array(schema.string()),
|
|
262
|
+
hasAiOverview: schema.boolean(),
|
|
263
|
+
hasFeaturedSnippet: schema.boolean(),
|
|
264
|
+
hasLocalPack: schema.boolean(),
|
|
265
|
+
hasPeopleAlsoAsk: schema.boolean(),
|
|
266
|
+
fetchedAt: schema.string().datetime(),
|
|
267
|
+
cached: schema.boolean(),
|
|
268
|
+
});
|
|
269
|
+
export const serpResearchResponseSchema = defineSuccessEnvelope(serpResearchDataSchema);
|
|
270
|
+
export const rankingsResearchQuerySchema = schema.strictObject({
|
|
271
|
+
domain: schema.string().trim().min(1).max(253),
|
|
272
|
+
limit: schema.coerce.number().int().min(1).max(100).default(50),
|
|
273
|
+
minPosition: schema.coerce.number().int().min(1).max(100).default(1),
|
|
274
|
+
maxPosition: schema.coerce.number().int().min(1).max(100).default(20),
|
|
275
|
+
locationCode: schema.coerce.number().int().positive().default(2840),
|
|
276
|
+
order: schema.enum(['position', 'traffic']).default('position'),
|
|
277
|
+
}).superRefine((value, context) => {
|
|
278
|
+
if (value.maxPosition < value.minPosition)
|
|
279
|
+
context.addIssue({ code: 'custom', path: ['maxPosition'], message: 'maxPosition must be at least minPosition.' });
|
|
280
|
+
});
|
|
281
|
+
const rankingKeywordSchema = defineResponseObject({
|
|
282
|
+
keyword: schema.string().min(1),
|
|
283
|
+
position: schema.number(),
|
|
284
|
+
volume: schema.number(),
|
|
285
|
+
traffic: schema.number(),
|
|
286
|
+
url: schema.string(),
|
|
287
|
+
intent: schema.string().nullable().optional(),
|
|
288
|
+
});
|
|
289
|
+
const rankingPageSchema = defineResponseObject({
|
|
290
|
+
path: schema.string(),
|
|
291
|
+
traffic: schema.number(),
|
|
292
|
+
keywords: schema.number(),
|
|
293
|
+
});
|
|
294
|
+
const rankingMetricsSchema = defineResponseObject({
|
|
295
|
+
organicTraffic: schema.number(),
|
|
296
|
+
totalKeywords: schema.number(),
|
|
297
|
+
keywordFootprint: schema.strictObject({
|
|
298
|
+
score: schema.number().nullable(),
|
|
299
|
+
scale: schema.strictObject({ min: schema.literal(0), max: schema.literal(100) }),
|
|
300
|
+
provenance: schema.literal('local-keyword-count'),
|
|
301
|
+
}),
|
|
302
|
+
trafficValue: schema.number().nullable(),
|
|
303
|
+
topPages: schema.array(rankingPageSchema.producer),
|
|
304
|
+
}, {
|
|
305
|
+
organicTraffic: schema.number(),
|
|
306
|
+
totalKeywords: schema.number(),
|
|
307
|
+
keywordFootprint: schema.looseObject({
|
|
308
|
+
score: schema.number().nullable(),
|
|
309
|
+
scale: schema.looseObject({ min: schema.literal(0), max: schema.literal(100) }),
|
|
310
|
+
provenance: schema.literal('local-keyword-count'),
|
|
311
|
+
}),
|
|
312
|
+
trafficValue: schema.number().nullable(),
|
|
313
|
+
topPages: schema.array(rankingPageSchema.client),
|
|
314
|
+
});
|
|
315
|
+
const rankingsResearchDataSchema = defineResponseObject({
|
|
316
|
+
domain: schema.string().min(1),
|
|
317
|
+
keywords: schema.array(rankingKeywordSchema.producer),
|
|
318
|
+
totalFound: schema.number().int().nonnegative(),
|
|
319
|
+
metrics: rankingMetricsSchema.producer.nullable(),
|
|
320
|
+
positionRange: schema.strictObject({ min: schema.number(), max: schema.number() }).optional(),
|
|
321
|
+
message: schema.string().optional(),
|
|
322
|
+
cached: schema.boolean(),
|
|
323
|
+
}, {
|
|
324
|
+
domain: schema.string().min(1),
|
|
325
|
+
keywords: schema.array(rankingKeywordSchema.client),
|
|
326
|
+
totalFound: schema.number().int().nonnegative(),
|
|
327
|
+
metrics: rankingMetricsSchema.client.nullable(),
|
|
328
|
+
positionRange: schema.looseObject({ min: schema.number(), max: schema.number() }).optional(),
|
|
329
|
+
message: schema.string().optional(),
|
|
330
|
+
cached: schema.boolean(),
|
|
331
|
+
});
|
|
332
|
+
export const rankingsResearchResponseSchema = defineSuccessEnvelope(rankingsResearchDataSchema);
|
|
112
333
|
export const storedResearchOverviewOperation = defineOperation({
|
|
113
334
|
id: 'research.overview',
|
|
114
335
|
method: 'GET',
|
|
@@ -166,9 +387,116 @@ export const storedResearchOverviewOperation = defineOperation({
|
|
|
166
387
|
}],
|
|
167
388
|
},
|
|
168
389
|
});
|
|
390
|
+
const liveResearchErrors = [
|
|
391
|
+
'invalid_request',
|
|
392
|
+
'unauthorized',
|
|
393
|
+
'auth_expired',
|
|
394
|
+
'forbidden',
|
|
395
|
+
'not_found',
|
|
396
|
+
'entitlement_required',
|
|
397
|
+
'quota_exhausted',
|
|
398
|
+
'provider_unavailable',
|
|
399
|
+
'conflict',
|
|
400
|
+
'rate_limited',
|
|
401
|
+
'contract_violation',
|
|
402
|
+
'internal_error',
|
|
403
|
+
];
|
|
404
|
+
const liveResearchSemantics = {
|
|
405
|
+
kind: 'query',
|
|
406
|
+
sideEffects: 'none',
|
|
407
|
+
retry: 'idempotent',
|
|
408
|
+
readConsistency: 'primary',
|
|
409
|
+
};
|
|
410
|
+
const liveResearchAuth = {
|
|
411
|
+
credentials: ['session', 'api_token'],
|
|
412
|
+
scopes: ['research:read'],
|
|
413
|
+
minimumRole: 'viewer',
|
|
414
|
+
ownership: 'site',
|
|
415
|
+
};
|
|
416
|
+
const liveResearchPolicy = {
|
|
417
|
+
rate: publicV1StandardRatePolicy,
|
|
418
|
+
idempotency: { _tag: 'none' },
|
|
419
|
+
entitlement: 'growth',
|
|
420
|
+
meter: { key: 'dataforseo', units: 1, charge: 'live-only' },
|
|
421
|
+
};
|
|
422
|
+
export const keywordResearchOperation = defineOperation({
|
|
423
|
+
id: 'research.keywords',
|
|
424
|
+
method: 'GET',
|
|
425
|
+
path: '/api/v1/sites/{siteId}/research/keywords',
|
|
426
|
+
visibility: 'public',
|
|
427
|
+
semantics: liveResearchSemantics,
|
|
428
|
+
auth: liveResearchAuth,
|
|
429
|
+
request: { params: researchSiteParamsSchema, query: keywordResearchQuerySchema, headers: null, body: null },
|
|
430
|
+
responses: { 200: keywordResearchResponseSchema },
|
|
431
|
+
errors: liveResearchErrors,
|
|
432
|
+
errorResponse: publicV1ErrorEnvelope,
|
|
433
|
+
resources: { reads: [{ type: 'site.research', idFrom: 'params.siteId' }], changes: [] },
|
|
434
|
+
policy: liveResearchPolicy,
|
|
435
|
+
lifecycle: { introduced: '1.0.0' },
|
|
436
|
+
docs: {
|
|
437
|
+
summary: 'Research keyword ideas',
|
|
438
|
+
description: 'Returns keyword ideas with volume, difficulty, intent, cost data, and explicit provider execution and spend evidence. Local guidance and cached reads consume no units.',
|
|
439
|
+
tags: ['Research'],
|
|
440
|
+
examples: [{
|
|
441
|
+
request: { params: { siteId: 'site_01JXYZ' }, query: { topic: 'nuxt seo', limit: 20 } },
|
|
442
|
+
response: { status: 200, body: { data: { topic: 'nuxt seo', keywords: [], totalFound: 0, filters: { minVolume: 10, maxVolume: 10000, minDifficulty: 0, maxDifficulty: 60 }, evidence: { _tag: 'live', provider: 'dataforseo', spend: { key: 'dataforseo', units: 1 } } }, meta: { requestId: 'req_01JXYZ', version: '1.0' } } },
|
|
443
|
+
}],
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
export const serpResearchOperation = defineOperation({
|
|
447
|
+
id: 'research.serp',
|
|
448
|
+
method: 'GET',
|
|
449
|
+
path: '/api/v1/sites/{siteId}/research/serp',
|
|
450
|
+
visibility: 'public',
|
|
451
|
+
semantics: liveResearchSemantics,
|
|
452
|
+
auth: liveResearchAuth,
|
|
453
|
+
request: { params: researchSiteParamsSchema, query: serpResearchQuerySchema, headers: null, body: null },
|
|
454
|
+
responses: { 200: serpResearchResponseSchema },
|
|
455
|
+
errors: liveResearchErrors,
|
|
456
|
+
errorResponse: publicV1ErrorEnvelope,
|
|
457
|
+
resources: { reads: [{ type: 'site.research', idFrom: 'params.siteId' }], changes: [] },
|
|
458
|
+
policy: liveResearchPolicy,
|
|
459
|
+
lifecycle: { introduced: '1.0.0' },
|
|
460
|
+
docs: {
|
|
461
|
+
summary: 'Analyze a Google SERP',
|
|
462
|
+
description: 'Returns one dated SERP snapshot and its result features. Cached reads consume no units.',
|
|
463
|
+
tags: ['Research'],
|
|
464
|
+
examples: [{
|
|
465
|
+
request: { params: { siteId: 'site_01JXYZ' }, query: { keyword: 'nuxt seo', depth: 10 } },
|
|
466
|
+
response: { status: 200, body: { data: { keyword: 'nuxt seo', depth: 10, results: [], serpFeatures: [], hasAiOverview: false, hasFeaturedSnippet: false, hasLocalPack: false, hasPeopleAlsoAsk: false, fetchedAt: '2026-08-20T00:00:00.000Z', cached: false }, meta: { requestId: 'req_01JXYZ', version: '1.0' } } },
|
|
467
|
+
}],
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
export const rankingsResearchOperation = defineOperation({
|
|
471
|
+
id: 'research.rankings',
|
|
472
|
+
method: 'GET',
|
|
473
|
+
path: '/api/v1/sites/{siteId}/research/rankings',
|
|
474
|
+
visibility: 'public',
|
|
475
|
+
semantics: liveResearchSemantics,
|
|
476
|
+
auth: liveResearchAuth,
|
|
477
|
+
request: { params: researchSiteParamsSchema, query: rankingsResearchQuerySchema, headers: null, body: null },
|
|
478
|
+
responses: { 200: rankingsResearchResponseSchema },
|
|
479
|
+
errors: liveResearchErrors,
|
|
480
|
+
errorResponse: publicV1ErrorEnvelope,
|
|
481
|
+
resources: { reads: [{ type: 'site.research', idFrom: 'params.siteId' }], changes: [] },
|
|
482
|
+
policy: liveResearchPolicy,
|
|
483
|
+
lifecycle: { introduced: '1.0.0' },
|
|
484
|
+
docs: {
|
|
485
|
+
summary: 'Research domain rankings',
|
|
486
|
+
description: 'Returns current keyword rankings and domain metrics. Cached reads consume no units.',
|
|
487
|
+
tags: ['Research'],
|
|
488
|
+
examples: [{
|
|
489
|
+
request: { params: { siteId: 'site_01JXYZ' }, query: { domain: 'nuxtseo.com', limit: 20 } },
|
|
490
|
+
response: { status: 200, body: { data: { domain: 'nuxtseo.com', keywords: [], totalFound: 0, metrics: null, positionRange: { min: 1, max: 20 }, cached: false }, meta: { requestId: 'req_01JXYZ', version: '1.0' } } },
|
|
491
|
+
}],
|
|
492
|
+
},
|
|
493
|
+
});
|
|
169
494
|
export const researchV1Registry = defineRegistry({
|
|
170
495
|
feature: 'research',
|
|
171
496
|
operations: {
|
|
172
497
|
overview: storedResearchOverviewOperation,
|
|
498
|
+
keywords: keywordResearchOperation,
|
|
499
|
+
serp: serpResearchOperation,
|
|
500
|
+
rankings: rankingsResearchOperation,
|
|
173
501
|
},
|
|
174
502
|
});
|