feat(ahrefs): validate integration, fix cents/column bugs, add 13 v3 endpoints#5447
Conversation
…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).
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Bug fixes and polish: CPC and cost-style fields ( The Ahrefs block adds operation-specific inputs (date ranges, Reviewed by Cursor Bugbot for commit b1b5611. Configure here. |
| httpCode: page.http_code ?? null, | ||
| title: page.title ?? [], | ||
| internalLinks: (page.internal_links || []).length, | ||
| externalLinks: (page.external_links || []).length, |
There was a problem hiding this comment.
Link counts use array length
Medium Severity
In ahrefs_site_audit_page_explorer, internalLinks and externalLinks are derived with (page.internal_links || []).length and the same for external links, while backlinks on the same row is taken as a numeric field. If the API returns link counts as integers (consistent with backlinks), non-zero counts become undefined because numbers have no .length, so workflows see missing or wrong link metrics.
Reviewed by Cursor Bugbot for commit 934dca7. Configure here.
There was a problem hiding this comment.
Checked this directly against the live Ahrefs v3 docs raw JSON schema for GET /v3/site-audit/page-explorer:
"internal_links":{"items":{"type":"string","format":"url"},"type":"array","title":"internal_links","description":"The list of internal outgoing links on the page"}
Same shape for external_links. Both fields are genuinely documented as array<string (url)> (lists of the actual outgoing link URLs), not integer counts — Ahrefs also exposes separate dedicated count fields for this (links_count_internal, links_count_external), which are integers, but those are not what this tool selects. Since internal_links/external_links are arrays, .length correctly derives the count. I don't think this is a bug — happy to switch to the links_count_* fields directly if we'd rather avoid deriving a count from the full URL list, but the current code isn't producing undefined for non-zero values given the actual API contract. Leaving as-is unless you see contrary evidence from a live response.
Greptile SummaryThis PR audits and extends the Ahrefs integration — fixing three data-correctness bugs and adding 13 new v3 API tools. The changes are self-contained to the Ahrefs tool layer and do not touch shared infrastructure.
Confidence Score: 5/5This PR is safe to merge — all changes are additive or targeted bug fixes within the Ahrefs tool layer, with no shared infrastructure touched. The three bug fixes (wrong column name, missing cents-to-USD conversion, missing dropdown option) are narrow and correct. All 13 new tools follow the established pattern with consistent monetary conversion and error handling. Prior review rounds caught and resolved the SERP overview cents issue and the batch analysis dual-key fallback — both confirmed fixed in the current HEAD. No logic errors or data-correctness problems remain. No files require special attention. The rank_tracker_competitors_stats.ts uses an unusual hyphenated JSON key (data['competitors-metrics']) but the author confirmed this against live API docs. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[AhrefsBlock UI] -->|selects operation| B{Operation Type}
B --> C[Site Explorer Tools]
B --> D[Keywords Explorer Tools]
B --> E[Rank Tracker Tools]
B --> F[Batch Analysis]
B --> G[Site Audit Tools]
C --> C1[Domain Rating / History]
C --> C2[Backlinks / Broken Backlinks / Stats]
C --> C3[Referring Domains / History]
C --> C4[Organic Keywords / Competitors]
C --> C5[Top Pages / Paid Pages]
C --> C6[Anchors]
C --> C7[Metrics / Metrics History]
C --> C8[Keywords History]
D --> D1[Keyword Overview]
D --> D2[Related Terms]
E --> E1[Overview]
E --> E2[SERP Overview]
E --> E3[Competitors Overview]
E --> E4[Competitors Stats]
F --> F1[Batch Analysis POST]
G --> G1[Page Explorer]
C1 & C2 & C3 & C4 & C5 & C6 & C7 & C8 --> H[transformResponse]
D1 & D2 --> H
E1 & E2 & E3 & E4 --> H
F1 --> H
G1 --> H
H -->|monetary fields| I[div 100 cents to USD]
H --> J[Structured output]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[AhrefsBlock UI] -->|selects operation| B{Operation Type}
B --> C[Site Explorer Tools]
B --> D[Keywords Explorer Tools]
B --> E[Rank Tracker Tools]
B --> F[Batch Analysis]
B --> G[Site Audit Tools]
C --> C1[Domain Rating / History]
C --> C2[Backlinks / Broken Backlinks / Stats]
C --> C3[Referring Domains / History]
C --> C4[Organic Keywords / Competitors]
C --> C5[Top Pages / Paid Pages]
C --> C6[Anchors]
C --> C7[Metrics / Metrics History]
C --> C8[Keywords History]
D --> D1[Keyword Overview]
D --> D2[Related Terms]
E --> E1[Overview]
E --> E2[SERP Overview]
E --> E3[Competitors Overview]
E --> E4[Competitors Stats]
F --> F1[Batch Analysis POST]
G --> G1[Page Explorer]
C1 & C2 & C3 & C4 & C5 & C6 & C7 & C8 --> H[transformResponse]
D1 & D2 --> H
E1 & E2 & E3 & E4 --> H
F1 --> H
G1 --> H
H -->|monetary fields| I[div 100 cents to USD]
H --> J[Structured output]
Reviews (8): Last reviewed commit: "fix(ahrefs): split shared date subBlock ..." | Re-trigger Greptile |
…lback 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`.
|
@greptile review |
|
@cursor review |
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.
|
Fixed the stale docs Greptile flagged (SERP Overview |
|
@greptile review |
|
@cursor review |
…_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.
|
Ran a completely independent, from-scratch re-verification of all 23 tools against live Ahrefs v3 docs (not trusting any prior round's conclusions) and self-caught 3 issues in e4dcffc, none of which the bots had flagged:
Docs regenerated to match. Re-requesting review. |
|
@greptile review |
|
@cursor review |
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.
|
@greptile review |
|
@cursor review |
| url.searchParams.set('country', params.country || 'us') | ||
| url.searchParams.set('keywords', params.keyword) | ||
| if (params.terms) url.searchParams.set('terms', params.terms) | ||
| if (params.viewFor) url.searchParams.set('view_for', params.viewFor) |
There was a problem hiding this comment.
Related terms omits documented defaults
Medium Severity
The new ahrefs_related_terms request only adds terms and view_for when those params are truthy, while param descriptions promise defaults of all and top_10. Omitted values (common on direct agent tool calls) rely on undocumented API defaults instead of the documented behavior, unlike country, which always falls back to us.
Reviewed by Cursor Bugbot for commit ea0e1a4. Configure here.
There was a problem hiding this comment.
Checked the live docs for GET /v3/keywords-explorer/related-terms directly — both params have documented API-side defaults that match our own param descriptions exactly: terms defaults to all, view_for defaults to top_10 (confirmed via the docs' own "Default:" field for each param, not just prose). Omitting them when not provided isn't relying on an undocumented default — it's relying on the exact documented one. Only country needed an explicit client-side fallback because its default isn't spelled out the same way in the docs. Not changing this; happy to revisit if you can point to evidence the live API's actual default diverges from what's documented.
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.
|
@greptile review |
|
@cursor review |
…ools 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.
|
@greptile review |
|
@cursor review |
…ions 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.
|
@greptile review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
2 issues from previous reviews remain unresolved.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b1b5611. Configure here.


Summary
broken_backlinksselecting the wrong column (http_code_targetinstead ofhttp_code), which broke thehttpCodeoutput on every rowkeyword_overview,metrics, andmetrics_historyreturning CPC/cost fields in USD cents without converting to USD (values were 100x too high)exactoptionahrefs_organic_keywordsandahrefs_rank_tracker_competitors_overviewboth usedkeywordswith incompatible shapes) — the latter is nowcompetitorKeywordsNote on backwards compatibility
The CPC/cost unit fix is a silent 100x value change for
organicCost/paidCost/cpcon any existing workflow already consuming those fields — the old values were wrong (in cents, not dollars). Everything else is additive; no existing tool ids, subBlock ids, or output keys were removed or renamed.Type of Change
Testing
Tested manually — validated every tool's endpoint, params, and response mapping against live Ahrefs v3 docs across three independent verification passes (fresh docs re-check of all 13 new tools, full block-wiring cross-check, backwards-compat diff review).
bun run lint,bun run type-check, andbun run check:api-validationall pass clean.Checklist