docs: add OpenAPI spec for query fan-out report endpoints#2402
Merged
Conversation
|
This PR will trigger a minor release when merged. |
Defines the contract for the upcoming Query Fan-Out feature (LLMO):
- GET /org/{spaceCatId}/brands/{brandId}/fanout-report — 302 to a presigned
S3 URL when a report exists, 404 otherwise. Lambda never loads the body.
- POST /org/{spaceCatId}/brands/{brandId}/fanout-report — synchronously
generates the report, writes it to S3, returns 201 with no body.
Adds the FanoutReport / FanoutReportTopic / FanoutReportSubQuery /
FanoutReportRanking schemas describing the JSON document the GET redirect
points to. Implementation lands in a follow-up PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops topicId, description, matchedTopicId, similarityScore, promptsMention, promptsCitation — debug-only or pre-filtered redundancy. Keeps matchedTopicName for the "Semrush rewrote the input" hint case. Final per-topic shape (9 fields): topicUuid, name, matchedTopicName, volume, promptsTotal, mentionRate, citationRate, priorityScore, subQueries. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@quazar/ai-seo-ts now includes v2/fanout/. Pinning the intent values to KEYWORD_INTENT_ENUM (COMMERCIAL / INFORMATIONAL / NAVIGATIONAL / TRANSACTIONAL / UNSPECIFIED) and documenting that the curation step picks intents[0] from Semrush's multi-label array. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15cdce8 to
4daed04
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
OpenAPI-only PR. Defines the contract for the upcoming Query Fan-Out feature so the UI can build against a stable schema while the backend implementation lands in a follow-up.
GET /org/{spaceCatId}/brands/{brandId}/fanout-report—302 Foundredirect to a presigned S3 URL when a report exists;404 Not Foundotherwise. The Lambda never loads the report body — the client follows the redirect straight to S3.POST /org/{spaceCatId}/brands/{brandId}/fanout-report— synchronously regenerates the report (DB reads → Semrush fan-out sweep → curation → S3 write), returns201 Createdwith no body. Caller follows up with aGETto fetch the presigned URL.FanoutReport,FanoutReportTopic,FanoutReportSubQuery,FanoutReportRanking.Full decision log:
tmp/fanout/questions.md.UI integration guide
The flow the UI sees:
GET /org/{org}/brands/{brand}/fanout-report302→ followLocationheader → fetch the gzipped JSON from S3 (browsers +fetchauto-decompress viaContent-Encoding: gzip).404→ no report yet; show empty state, optionally surface a "Generate report" CTA that callsPOSTon the same path.POSTreturns201, redo step 1 to fetch the URL.The S3 JSON body conforms to
FanoutReport. UI consumption mapping below — one section per figure inQuery Fanout in Project Serenity.pdf.Top-level (every figure)
FanoutReportfieldisoDatebrandName,brandDomainsbrandDomainsis the apex set the curation used for ranking-match — useful for "why didn't this rank?" debug.country("US"),llm("chatgpt"),windowDays(7)topics[]priorityScoredescending. May be empty[].Figure 1 — Auto-suggested topic cards
Each card on the entry screen maps to one element of
topics[]:topics[].namematchedTopicNamewhen Semrush rewrote the input.topics[].matchedTopicNamename.topics[].priorityScoreTopic volume: 2,400/motopics[].volumeCitation rate: 3% across tracked promptstopics[].citationRate× 100nullmeans no execution data; display as—.Coverage: 24% of fan-out query universesubQueries[].brandPosition.Coverage derivation (used in Fig 1 cards and Fig 2 bar):
Figure 2 — Topic baseline & fan-out coverage bar
Shown after the user picks a topic from Fig 1. All from
topics[selectedIndex]:Best CRM for SMB)topics[].name8 tracked prompts · 28 deduplicated fan-out sub-queries)topics[].promptsTotalandtopics[].subQueries.lengthTopic volume: 2,400 monthly searchestopics[].volumeMention rate: 47% of tracked promptstopics[].mentionRate× 100Citation rate: 3% of tracked promptstopics[].citationRate× 100Ranking for 7 of 28 sub-queries (25% coverage)(ranking + lowRank)andtotalfrom the snippet above.Coverage bar segments (MVP — 3 segments, not 4):
Ranking (pos. 1–5)rankingcountLow rank (pos. 6–10)lowRankcountNot ranking — competitor+Not ranking — 3rd partyNot rankingsegment in MVP — value =notRankingcountFigure 3 — Content opportunity cards
No (skipped for now)
Figure 4 — Sub-query table
Row per
subQueries[]element with three filter tabs:subQueries[].keywordsubQueries[].intentComparison,Commercial,Informational).subQueries[].volumesubQueries[].brandPositionnull→✗ Not ranking;1..5→✓ Pos. N;6..10→↓ Pos. N.subQueries[].topDomainCompetitor/3rd partychip — not in MVP (see Fig 2 note).Tabs are pure client-side filters:
brandPosition === null || brandPosition >= 6brandPosition === nullBehavior callouts the UI should be aware of
brandDomainsmatching is exact-hostname, withwww.stripped. Subdomains are kept as distinct entries (blog.acme.com≠acme.com). If a customer's brand-page only listsacme.combut the SERP credit lives onblog.acme.com, that ranking will not be attributed to the brand. Surface a customer-facing hint when this is likely (e.g. whenbrandDomains.length === 1and manysubQueries[].topDomainvalues share the brand's apex but differ at the subdomain).priorityScore" — UI may want to expose this as a tooltip or empty-state message.country = "US",llm = "chatgpt",windowDays = 7. If the UI surfaces a market/model picker, treat these as "the only options for now."topics: []means either (a) the brand has no tracked topics, or (b) every topic was dropped during curation (e.g. low Semrush similarity score). Don't conflate with 404.Test plan
npm run docs:lint— clean for new schemas (4 OpenAPI 3.1 errors fixed; 194 pre-existing warnings on other specs unchanged).npm run docs:build— assembled todocs/index.htmlwithout errors.docs/index.htmland confirm the new endpoints + schemas look correct under thellmotag.FanoutReportJSON to confirm the field mapping above is sufficient.🤖 Generated with Claude Code