Skip to content

Commit 918ba8a

Browse files
authored
feat(ahrefs): validate integration, fix cents/column bugs, add 13 v3 endpoints (#5447)
* feat(ahrefs): validate integration, fix cents/column bugs, add 13 v3 endpoints Audited the existing Ahrefs integration against live API v3 docs and fixed real bugs: broken_backlinks selected the wrong column (http_code_target instead of http_code), and keyword_overview/metrics/metrics_history returned CPC/cost fields in USD cents without converting to USD. Also fixed the top-pages mode dropdown missing the "exact" option. Added 13 new tools covering previously-unsupported Ahrefs v3 endpoints: Rank Tracker (overview, SERP overview, competitors overview, competitors stats), Batch Analysis, Site Audit page explorer, four history/trend endpoints (domain rating, metrics, referring domains, keywords), Related Terms, Anchors, and Paid Pages. Note: the CPC/cost unit fix silently shifts existing organicCost/paidCost/cpc values by 100x for any workflow already consuming them (the old values were wrong, in cents instead of dollars). * fix(ahrefs): convert remaining cents-to-USD fields, drop unneeded fallback key Rank Tracker SERP Overview's value field, Competitors Stats' trafficValue, and Competitors Overview's nested competitor value were all left in USD cents while every other monetary field in the integration converts to USD - fixed for consistency with the rest of the integration. Also drops the unverified `data.results` fallback in Batch Analysis; the Ahrefs v3 docs confirm the response key is always `targets`. * docs(ahrefs): regenerate docs to reflect cents-to-USD conversion fix The generated docs page was stale after the previous commit converted rank_tracker_serp_overview.value, rank_tracker_competitors_stats.trafficValue, and rank_tracker_competitors_overview's nested value from cents to USD - regenerating picks up the corrected field descriptions. * fix(ahrefs): revert incorrect broken_backlinks column, fix missed top_pages conversion, revert unverified competitor value conversions Independent re-verification against live Ahrefs v3 docs surfaced two real regressions from the earlier fix rounds and one missed conversion: - broken_backlinks: the earlier fix changed the selected column from http_code_target to http_code, but the docs say http_code is the *referring page's* status and http_code_target is the *broken target page's* status - the tool needs the latter (matches its own output description). Reverted to http_code_target. - top_pages: value field was never divided by 100 despite the docs stating it's in USD cents and the output already claiming USD - fixed. - rank_tracker_competitors_stats.trafficValue and the nested competitor.value in rank_tracker_competitors_overview were converted from cents to USD last round on a "match every other monetary field" assumption, but the live docs do not document these two fields as cents (unlike every field that was correctly converted). Reverted to passthrough and dropped the "(USD)" claim from their descriptions until Ahrefs documents the unit. Also added the missing "exact" mode option to top_pages' mode param description, matching every sibling tool. * fix(ahrefs): convert rank tracker competitor value/trafficValue to USD Both bots independently flagged these two fields as inconsistent with every other monetary field in the integration, all 7 of which are explicitly documented as USD cents. Neither field has explicit unit documentation (one is undocumented as cents, the other's schema isn't statically retrievable at all), but given the unanimous pattern across every other verified field and no contrary evidence, converting for consistency is the better bet than leaving them as an outlier. * style(ahrefs): drop non-TSDoc inline comments introduced in this PR Repo convention disallows non-TSDoc comments; removed the three explanatory // comments this PR added next to cents-to-USD conversions (keyword_overview, paid_pages, related_terms) - pre-existing comments elsewhere in the file are untouched, out of scope for this PR. * fix(ahrefs): default optional country to us consistently across all tools paid_pages, metrics_history, keywords_history, and batch_analysis were the only 4 of the 11 tools with an optional country param that didn't fall back to "us" when omitted, unlike domain_rating, metrics, keyword_overview, organic_keywords, organic_competitors, top_pages, and related_terms - all of which default client-side. Aligned all four to the same convention so direct tool/agent calls without an explicit country get the same behavior regardless of which operation is used. * fix(ahrefs): split shared date subBlock id for datetime-format operations site_audit_page_explorer and rank_tracker_serp_overview both reused the generic 'date' subBlock id with YYYY-MM-DDThh:mm:ss semantics, while every other operation using that same id expects YYYY-MM-DD. Switching operations without clearing the field could carry a stale wrong-format value into the new operation's request. Split into distinct ids (crawlDate, asOfDate) mapped back to each tool's date param in tools.config.params.
1 parent be6f693 commit 918ba8a

22 files changed

Lines changed: 3475 additions & 14 deletions

apps/docs/content/docs/en/integrations/ahrefs.mdx

Lines changed: 373 additions & 3 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/ahrefs.ts

Lines changed: 850 additions & 5 deletions
Large diffs are not rendered by default.

apps/sim/lib/integrations/integrations.json

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,64 @@
387387
"name": "Top Pages",
388388
"description": "Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value."
389389
},
390+
{
391+
"name": "Paid Pages",
392+
"description": "Get a target domain's pages that receive paid search traffic, sorted by estimated paid traffic. Returns page URLs with their paid traffic, keyword counts, and estimated spend."
393+
},
394+
{
395+
"name": "Anchors",
396+
"description": "Get the anchor text distribution for a target domain or URL's backlinks, showing how many links and referring domains use each anchor text."
397+
},
390398
{
391399
"name": "Keyword Overview",
392400
"description": "Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential."
401+
},
402+
{
403+
"name": "Related Terms",
404+
"description": "Get keyword ideas related to a seed keyword: terms the same top-ranking pages also rank for (\"also rank for\") or also discuss (\"also talk about\"), with volume, difficulty, and CPC."
405+
},
406+
{
407+
"name": "Domain Rating History",
408+
"description": "Get the historical Domain Rating (DR) trend for a target domain or URL over a date range, grouped daily, weekly, or monthly."
409+
},
410+
{
411+
"name": "Metrics History",
412+
"description": "Get the historical organic and paid traffic trend for a target domain or URL over a date range: organic traffic/cost and paid traffic/cost at each point in time."
413+
},
414+
{
415+
"name": "Referring Domains History",
416+
"description": "Get the historical referring domains trend for a target domain or URL over a date range, grouped daily, weekly, or monthly."
417+
},
418+
{
419+
"name": "Keywords History",
420+
"description": "Get the historical organic keyword ranking distribution for a target domain or URL over a date range: how many keywords rank in each position bucket at each point in time."
421+
},
422+
{
423+
"name": "Batch Analysis",
424+
"description": "Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once."
425+
},
426+
{
427+
"name": "Site Audit Page Explorer",
428+
"description": "Get crawled pages from an Ahrefs Site Audit project with health and SEO metrics: HTTP status, title, link counts, backlinks, indexability, and traffic. Optionally filter to pages affected by a specific issue."
429+
},
430+
{
431+
"name": "Rank Tracker Overview",
432+
"description": "Get ranking overview metrics for the keywords tracked in an Ahrefs Rank Tracker project: position, search volume, keyword difficulty, and estimated traffic. This endpoint is free and does not consume API units."
433+
},
434+
{
435+
"name": "Rank Tracker SERP Overview",
436+
"description": "Get the full SERP (search engine results page) for a keyword tracked in an Ahrefs Rank Tracker project, including every ranking URL with its position, title, and authority metrics. This endpoint is free and does not consume API units."
437+
},
438+
{
439+
"name": "Rank Tracker Competitors Overview",
440+
"description": "Get competitor rankings for the keywords tracked in an Ahrefs Rank Tracker project: each tracked keyword's volume and difficulty alongside every competitor's position, traffic, and traffic value. This endpoint is free and does not consume API units."
441+
},
442+
{
443+
"name": "Rank Tracker Competitors Stats",
444+
"description": "Get aggregate competitor stats for an Ahrefs Rank Tracker project: each competitor's traffic, traffic value, average position, and share of voice across all tracked keywords. This endpoint is free and does not consume API units."
393445
}
394446
],
395-
"operationCount": 10,
447+
"operationCount": 23,
396448
"triggers": [],
397449
"triggerCount": 0,
398450
"authType": "api-key",

apps/sim/tools/ahrefs/anchors.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { AhrefsAnchorsParams, AhrefsAnchorsResponse } from '@/tools/ahrefs/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
const SELECT_FIELDS = 'anchor,links_to_target,dofollow_links,refdomains,first_seen,last_seen'
5+
6+
export const anchorsTool: ToolConfig<AhrefsAnchorsParams, AhrefsAnchorsResponse> = {
7+
id: 'ahrefs_anchors',
8+
name: 'Ahrefs Anchors',
9+
description:
10+
"Get the anchor text distribution for a target domain or URL's backlinks, showing how many links and referring domains use each anchor text.",
11+
version: '1.0.0',
12+
13+
params: {
14+
target: {
15+
type: 'string',
16+
required: true,
17+
visibility: 'user-or-llm',
18+
description:
19+
'The target domain or URL to analyze. Example: "example.com" or "https://example.com/page"',
20+
},
21+
mode: {
22+
type: 'string',
23+
required: false,
24+
visibility: 'user-or-llm',
25+
description:
26+
'Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
27+
},
28+
history: {
29+
type: 'string',
30+
required: false,
31+
visibility: 'user-or-llm',
32+
description:
33+
'Historical scope: "live" (currently live), "all_time" (default, includes lost backlinks), or "since:YYYY-MM-DD" (backlinks found since a date)',
34+
},
35+
limit: {
36+
type: 'number',
37+
required: false,
38+
visibility: 'user-or-llm',
39+
description: 'Maximum number of results to return. Example: 50 (default: 1000)',
40+
},
41+
apiKey: {
42+
type: 'string',
43+
required: true,
44+
visibility: 'user-only',
45+
description: 'Ahrefs API Key',
46+
},
47+
},
48+
49+
request: {
50+
url: (params) => {
51+
const url = new URL('https://api.ahrefs.com/v3/site-explorer/anchors')
52+
url.searchParams.set('target', params.target)
53+
url.searchParams.set('select', SELECT_FIELDS)
54+
if (params.mode) url.searchParams.set('mode', params.mode)
55+
url.searchParams.set('history', params.history || 'all_time')
56+
if (params.limit) url.searchParams.set('limit', String(params.limit))
57+
return url.toString()
58+
},
59+
method: 'GET',
60+
headers: (params) => ({
61+
Accept: 'application/json',
62+
Authorization: `Bearer ${params.apiKey}`,
63+
}),
64+
},
65+
66+
transformResponse: async (response: Response) => {
67+
const data = await response.json()
68+
69+
if (!response.ok) {
70+
throw new Error(data.error?.message || data.error || 'Failed to get anchors')
71+
}
72+
73+
const anchors = (data.anchors || []).map((item: any) => ({
74+
anchor: item.anchor || '',
75+
backlinks: item.links_to_target ?? 0,
76+
dofollowBacklinks: item.dofollow_links ?? 0,
77+
referringDomains: item.refdomains ?? 0,
78+
firstSeen: item.first_seen || '',
79+
lastSeen: item.last_seen ?? null,
80+
}))
81+
82+
return {
83+
success: true,
84+
output: {
85+
anchors,
86+
},
87+
}
88+
},
89+
90+
outputs: {
91+
anchors: {
92+
type: 'array',
93+
description: 'Anchor text distribution for the backlink profile',
94+
items: {
95+
type: 'object',
96+
properties: {
97+
anchor: { type: 'string', description: 'The anchor text' },
98+
backlinks: { type: 'number', description: 'Total backlinks using this anchor text' },
99+
dofollowBacklinks: {
100+
type: 'number',
101+
description: 'Number of dofollow backlinks using this anchor text',
102+
},
103+
referringDomains: {
104+
type: 'number',
105+
description: 'Number of unique referring domains using this anchor text',
106+
},
107+
firstSeen: {
108+
type: 'string',
109+
description: 'When a link with this anchor was first found',
110+
},
111+
lastSeen: {
112+
type: 'string',
113+
description: 'When a backlink with this anchor was last seen (null if still live)',
114+
optional: true,
115+
},
116+
},
117+
},
118+
},
119+
},
120+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type { AhrefsBatchAnalysisParams, AhrefsBatchAnalysisResponse } from '@/tools/ahrefs/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
const SELECT_FIELDS =
5+
'url,domain_rating,ahrefs_rank,backlinks,refdomains,org_traffic,org_keywords,paid_traffic'
6+
7+
export const batchAnalysisTool: ToolConfig<AhrefsBatchAnalysisParams, AhrefsBatchAnalysisResponse> =
8+
{
9+
id: 'ahrefs_batch_analysis',
10+
name: 'Ahrefs Batch Analysis',
11+
description:
12+
'Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once.',
13+
version: '1.0.0',
14+
15+
params: {
16+
targets: {
17+
type: 'string',
18+
required: true,
19+
visibility: 'user-or-llm',
20+
description:
21+
'Comma-separated list of domains or URLs to analyze. Example: "example.com,competitor.com"',
22+
},
23+
mode: {
24+
type: 'string',
25+
required: false,
26+
visibility: 'user-or-llm',
27+
description:
28+
'Analysis mode applied to every target: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)',
29+
},
30+
protocol: {
31+
type: 'string',
32+
required: false,
33+
visibility: 'user-or-llm',
34+
description: 'Protocol applied to every target: "both" (default), "http", or "https"',
35+
},
36+
country: {
37+
type: 'string',
38+
required: false,
39+
visibility: 'user-or-llm',
40+
description: 'Country code for traffic data. Example: "us", "gb", "de" (default: "us")',
41+
},
42+
volumeMode: {
43+
type: 'string',
44+
required: false,
45+
visibility: 'user-or-llm',
46+
description: 'Search volume calculation: "monthly" or "average" (default: "monthly")',
47+
},
48+
apiKey: {
49+
type: 'string',
50+
required: true,
51+
visibility: 'user-only',
52+
description: 'Ahrefs API Key',
53+
},
54+
},
55+
56+
request: {
57+
url: () => 'https://api.ahrefs.com/v3/batch-analysis/batch-analysis',
58+
method: 'POST',
59+
headers: (params) => ({
60+
Accept: 'application/json',
61+
'Content-Type': 'application/json',
62+
Authorization: `Bearer ${params.apiKey}`,
63+
}),
64+
body: (params) => {
65+
const targets = params.targets
66+
.split(',')
67+
.map((target) => target.trim())
68+
.filter((target) => target.length > 0)
69+
.map((url) => ({
70+
url,
71+
mode: params.mode || 'subdomains',
72+
protocol: params.protocol || 'both',
73+
}))
74+
75+
return {
76+
select: SELECT_FIELDS.split(','),
77+
targets,
78+
country: params.country || 'us',
79+
volume_mode: params.volumeMode || 'monthly',
80+
}
81+
},
82+
},
83+
84+
transformResponse: async (response: Response) => {
85+
const data = await response.json()
86+
87+
if (!response.ok) {
88+
throw new Error(data.error?.message || data.error || 'Failed to run batch analysis')
89+
}
90+
91+
const results = (data.targets || []).map((item: any) => ({
92+
url: item.url || '',
93+
index: item.index ?? 0,
94+
domainRating: item.domain_rating ?? null,
95+
ahrefsRank: item.ahrefs_rank ?? null,
96+
backlinks: item.backlinks ?? null,
97+
referringDomains: item.refdomains ?? null,
98+
organicTraffic: item.org_traffic ?? null,
99+
organicKeywords: item.org_keywords ?? null,
100+
paidTraffic: item.paid_traffic ?? null,
101+
error: item.error ?? null,
102+
}))
103+
104+
return {
105+
success: true,
106+
output: {
107+
results,
108+
},
109+
}
110+
},
111+
112+
outputs: {
113+
results: {
114+
type: 'array',
115+
description: 'Bulk metrics for each analyzed target, in submission order',
116+
items: {
117+
type: 'object',
118+
properties: {
119+
url: { type: 'string', description: 'The analyzed target URL or domain' },
120+
index: { type: 'number', description: 'Index of the target in the submitted list' },
121+
domainRating: {
122+
type: 'number',
123+
description: 'Domain Rating score (0-100)',
124+
optional: true,
125+
},
126+
ahrefsRank: {
127+
type: 'number',
128+
description: 'Ahrefs Rank (global ranking)',
129+
optional: true,
130+
},
131+
backlinks: {
132+
type: 'number',
133+
description: 'Total backlinks to the target',
134+
optional: true,
135+
},
136+
referringDomains: {
137+
type: 'number',
138+
description: 'Unique domains linking to the target',
139+
optional: true,
140+
},
141+
organicTraffic: {
142+
type: 'number',
143+
description: 'Estimated monthly organic traffic',
144+
optional: true,
145+
},
146+
organicKeywords: {
147+
type: 'number',
148+
description: 'Number of organic keywords ranked (top 100)',
149+
optional: true,
150+
},
151+
paidTraffic: {
152+
type: 'number',
153+
description: 'Estimated monthly paid search traffic',
154+
optional: true,
155+
},
156+
error: {
157+
type: 'string',
158+
description: 'Error message if this target could not be analyzed',
159+
optional: true,
160+
},
161+
},
162+
},
163+
},
164+
},
165+
}

0 commit comments

Comments
 (0)