SERP Topic Gap Monitor
Pricing
Pay per usage
SERP Topic Gap Monitor
Find the topics your competitors rank for that your site is missing. This SEO content gap analyzer accepts pre-fetched SERP data, extracts topics from competitor pages, and returns a scored gap report for content planning and competitive analysis.
Find the topics your competitors rank for that your site is missing. This SEO content gap analyzer takes pre-fetched SERP data, extracts topics from competitor pages, and returns a scored gap report you can act on immediately.
Who is this for?
- SEO strategists building data-driven content calendars
- Content teams deciding what to write next based on competitive gaps
- Agencies running repeatable competitor content analysis across client portfolios
- Growth marketers prioritizing keyword clusters by competitive opportunity
What it does
Give it your domain and a set of search results. The Actor compares what your competitors rank for against what your site covers, then returns a ranked list of topic gaps -- the content your competitors publish that you have not.
You control the SERP data source. This Actor processes structured search results that you provide, rather than fetching them itself. That means predictable costs (no surprise API calls), full control over which keywords and markets you analyze, and compatibility with any SERP data source -- Google SERP API actors, your own scraping pipeline, or exported spreadsheets.
Use cases
- SEO content planning -- Find topics your competitors rank for that you have not covered yet
- Content gap analysis -- Identify which keyword clusters have the weakest coverage on your domain
- Competitor content analysis -- See which competitor pages cover topics you are missing
- Editorial prioritization -- Rank content gaps by severity to focus on the highest-impact topics first
How it works
The Actor runs a four-step deterministic pipeline. Identical input always produces identical output.
- Tokenize -- Page content (or title + snippet when no content is available) is lowercased, stripped of punctuation, and filtered through a standard English stopword list. Tokens under 3 characters are removed.
- Extract topics -- For each keyword's SERP results, tokens that appear on 2 or more competitor pages are identified as topics. Your target domain's pages are checked for coverage but do not count toward the competitor threshold.
- Detect gaps -- Topics are merged across keywords. Competitor URLs are deduplicated. If your domain covers a topic in any keyword's results, it is marked as covered.
- Score and rank -- Each gap receives a score:
gapScore = uniqueCompetitorPages / totalUniqueCompetitorPages, clamped to [0, 1] and rounded to 2 decimal places. Gaps are sorted by score (highest first), then alphabetically by topic.
Input
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
targetDomain | string | Yes | -- | Domain to evaluate (e.g., myblog.com). Matches the hostname and subdomains (e.g., www.myblog.com, blog.myblog.com). |
serpResults | array | Yes | -- | Pre-fetched SERP data. Each entry represents one keyword's search results. See Input format below. |
locale | string | No | en | Locale of the SERP data (e.g., en, de, ja). Stored in output metadata for downstream tracking. Set this when analyzing non-English markets so your reports stay labeled correctly. |
country | string | No | us | Country code for the SERP data (e.g., us, gb, de). Stored in output metadata for downstream tracking. Useful when you run gap analyses across multiple markets. |
minGapScore | number | No | 0 | Minimum gap score threshold (0--1). Gaps scoring below this are excluded from output. Raise this (e.g., 0.3) to filter out low-signal gaps and focus on topics where many competitors outrank you. |
Input format
Each entry in serpResults represents one keyword's organic search results:
{"keyword": "content marketing strategy","organicResults": [{"url": "https://competitor.com/guide","title": "Ultimate Content Marketing Guide","snippet": "Learn how to build a content marketing strategy...","pageContent": "Full extracted text of the page..."}]}
urlandtitleare required for each organic resultsnippetis optional -- used as fallback text whenpageContentis missingpageContentis optional but strongly recommended -- without it, topic extraction relies only on titles and snippets, which limits accuracy
Example input
{"targetDomain": "myblog.com","serpResults": [{"keyword": "content marketing strategy","organicResults": [{"url": "https://myblog.com/strategy","title": "Content Strategy Guide","pageContent": "content marketing strategy planning editorial calendar audience research"},{"url": "https://competitor-a.com/guide","title": "Ultimate Content Marketing Guide","pageContent": "content marketing strategy seo optimization keyword research analytics tracking performance metrics"},{"url": "https://competitor-b.com/tips","title": "Marketing Tips","pageContent": "content marketing strategy distribution channels social media analytics tracking engagement"}]}],"minGapScore": 0.3}
Output
The Actor stores its output in the default key-value store under the OUTPUT key as a single JSON object. Use this data to decide what content to create, which topics to expand, and where your competitors have the strongest coverage advantage.
Output fields
| Field | Type | Description |
|---|---|---|
targetDomain | string | The domain that was analyzed |
keywordsAnalyzed | number | Number of keywords processed |
totalGaps | number | Number of gaps in the output (after minGapScore filtering) |
gaps | array | Ranked list of topic gaps (see below) |
serpSummary | array | Per-keyword summary of SERP data processed |
meta | object | Processing metadata (locale, country, minGapScore) |
notes | array | Informational notes (e.g., filter warnings) |
Gap object
Each gap in the gaps array contains:
| Field | Type | Description |
|---|---|---|
topic | string | The topic token (normalized, lowercase) |
gapScore | number | Coverage ratio (0--1). Higher means more competitors cover this topic. |
competitorCoverage | number | Number of unique competitor pages covering this topic |
competitorUrls | string[] | URLs of competitor pages covering this topic (sorted) |
targetCoverage | boolean | Whether your domain covers this topic in any keyword's results |
relatedKeywords | string[] | Keywords where this topic appeared (sorted) |
SERP summary object
Each entry in serpSummary contains:
| Field | Type | Description |
|---|---|---|
keyword | string | The keyword analyzed |
resultsFetched | number | Total organic results provided for this keyword |
pagesFetched | number | Results that included pageContent |
targetDomainRank | number | null | Position of target domain in results (1-indexed), or null if not found |
Example output
{"targetDomain": "myblog.com","keywordsAnalyzed": 2,"totalGaps": 3,"gaps": [{"topic": "analytics","gapScore": 0.8,"competitorCoverage": 4,"competitorUrls": ["https://competitor-a.com/guide","https://competitor-a.com/seo","https://competitor-b.com/tips","https://competitor-c.com/blog"],"targetCoverage": false,"relatedKeywords": ["content marketing strategy", "seo keyword research"]},{"topic": "tracking","gapScore": 0.8,"competitorCoverage": 4,"competitorUrls": ["https://competitor-a.com/guide","https://competitor-a.com/seo","https://competitor-b.com/tips","https://competitor-c.com/blog"],"targetCoverage": false,"relatedKeywords": ["content marketing strategy", "seo keyword research"]},{"topic": "strategy","gapScore": 0.6,"competitorCoverage": 3,"competitorUrls": ["https://competitor-a.com/guide","https://competitor-b.com/tips","https://competitor-c.com/blog"],"targetCoverage": true,"relatedKeywords": ["content marketing strategy"]}],"serpSummary": [{"keyword": "content marketing strategy","resultsFetched": 4,"pagesFetched": 4,"targetDomainRank": 1},{"keyword": "seo keyword research","resultsFetched": 3,"pagesFetched": 3,"targetDomainRank": 3}],"meta": {"locale": "en","country": "us","minGapScore": 0},"notes": []}
How to read the results
- High
gapScore+targetCoverage: false-- Strong signal to create content. Many competitors cover this topic and you do not. - High
gapScore+targetCoverage: true-- You cover this topic, but competitors cover it more broadly. Consider expanding your content. - Low
gapScore-- Only a few competitors mention this topic. Lower priority unless strategically important to your roadmap. relatedKeywords-- Shows which search queries surface this gap. More keywords means more search visibility at stake.
Error handling
| Error | Cause | Fix |
|---|---|---|
No input provided. | Actor received no input | Provide input via the Apify console or API |
Input must include "targetDomain" and a non-empty "serpResults" array. | Missing or empty required fields | Ensure targetDomain is a non-empty string and serpResults has at least one entry |
Malformed URLs in organicResults are handled gracefully -- they are treated as competitor pages (not matched as target domain) and do not crash the Actor.
Running locally
# Install dependenciesnpm install# Set up input -- edit storage/key_value_stores/default/INPUT.json# Then run:npm run start:dev# Run testsnpm test# Build for productionnpm run build
Architecture
This Actor is designed as a composable building block in a larger SEO pipeline. Each processing step is a pure function with no side effects.
serpResults --> tokenize --> extractTopics --> detectGaps --> scoreAndRank --> output
Why this matters for your workflow:
- Composable -- Pair it with any SERP-fetching Actor upstream and any reporting tool downstream. No vendor lock-in on data sources.
- Reusable data -- Fetch SERP data once, then run multiple gap analyses against different target domains without additional API costs.
- Deterministic -- Same input always produces the same output. Safe to use in automated pipelines, scheduled runs, and CI/CD workflows.
- Testable -- Pure-function internals mean you can validate behavior without network access.