DEV Community

hc_xshh
hc_xshh

Posted on

My content-gap detector found 38 gaps. Two-thirds were bugs in the detector.

I write a small English blog about China — 70 posts, a domain registered on 11 October 2026. Like anyone with a new site, I wanted to know what to write next, and the cheapest signal available is autocomplete: send a seed phrase to Google's and Bing's suggestion endpoints, collect what people are actually typing, then compare that against what you have already published.

So I wired it into a status dashboard I check each morning. This morning it ran in 5.2 seconds and collected 466 unique suggestions from 56 requests (51 returned; the other five were dropped by a proxy or a rate limit).

Then it told me I had 38 content gaps.

That number was wrong, and the way it was wrong turned out to be more useful than the number.

The scoring, in one paragraph

Each query is lowercased, tokenised, stemmed, and stripped of question words and place-or-language words — "how", "what", "china", "english", "foreigner" and so on. What is left is a bag of content words. Each post gets the same treatment, but only from its slug and title. The score is matched content words divided by total content words, plus 0.15 if an adjacent pair also matches. Above 0.60 counts as covered, 0.30–0.59 as partial, below that as a gap.

def content(q):
    ws = [stem(w) for w in re.findall(r"[a-z0-9]+", q.lower())]
    return [w for w in ws if w not in GEO and w not in ASK and len(w) > 1]
Enter fullscreen mode Exit fullscreen mode

Three thresholds, one table, a number I can put in a commit message. What could go wrong.

The 38 gaps, by cluster

Cluster Queries Examples
Meituan app in English 12 "meituan app english version", "how to change language in meituan app"
Didi 5 "how to call didi in china", "didi alternatives in china"
China bank foreign exchange 4 "china bank foreign exchange policy"
1688 3 "translate 1688 as a year in history"
Company registration 3 "business registration in china"
Remote work 2 "working remotely from another country"
China Mobile 2 "does china mobile support esim"
Hukou, in Dutch or German 2 "wat is het hukou systeem", "was ist das hukou system"
Hukou, in hip hop 2 "hukou has hip hop music"
Fapiao check 1 "china tax bureau fapiao check"
Customs law 1 "customs law of the people's republic of china"
One apartment 1 "apartment in china that holds 30000 people"

Failure one: my stopword list ate the query

Take "how to change language in meituan app". After filtering, the content words are change, language. "Meituan" sat in my geography set, because I had added it while tuning noise from "meituan vs eleme" comparisons. "English" sat there too, for the same reason. "App" was in the ask set, because it appears in half of all autocomplete suggestions and carries no topic.

So "meituan app english download" reduced to download. "login to meituan english" reduced to login. "meituan waimai app english" reduced to waimai.

Those queries then scored 0.00 against every post I have, including one titled Chinese apps in English: what Meituan and Dianping ship, which answers them. The word that would have matched was deleted before the matching started.

It gets better. The seed I typed for that topic was "meituan english app". It produced zero content words, so the loop skipped it entirely — 465 of the 466 suggestions were scored, and the one that was dropped was my own seed phrase.

All twelve Meituan queries were that bug. Same word list, same week, same collector.

Failure two: matching titles instead of pages

The Didi cluster is the opposite mistake. didi survived the filter, but posts are indexed from slug and title only, and my ride-hailing post is called "Ride-hailing in China: licences, fares and invoices". The body mentions Didi repeatedly. The title does not. One content word, zero matches, five queries into the gap bucket.

Both bugs point the same way: the score measures how well my titles match a query, not whether a page answers it.

What survived a human pass

I read all 38 by hand. Twelve were the stopword bug. Five were title-only matching. Two were the same question asked in Dutch and German. Six were entity collisions — Google reading 1688 as a year, "hukou" as hip hop, and one viral-sounding apartment query with no matching page. That is 25 of 38, about two-thirds, that were noise from four causes, none of them editorial.

Three clusters were left that are worth acting on:

  • China bank foreign exchange — four queries, and no page whose title names the policy.
  • Registering a company — "business registration in china" and "how long does it take to register a company in china". I have a page titled as a process, not as a duration.
  • China Mobile — "china mobile international customer service number" and "does china mobile support esim", both answered in sections on a page whose title does not contain either phrase.

That is the useful output: three clusters, not 38 gaps, and the reason each one was invisible.

What I changed in the tool, not the site

  • Index the body, not only slug and title. The posts are markdown on disk; a bag of body words per post costs nothing and would have caught the Didi cluster.
  • Never strip a token that appears in fewer than N posts. Brand and language words are exactly the ones that carry intent. If I need to suppress noise, suppress it after matching, not before.
  • Report clusters, not counts. "38 gaps" is a number I can quote. "Twelve of them are one app's language settings" is the information.
  • Keep the human pass. The tool's job is to turn 466 suggestions into a list I can read in ten minutes. It is a triage queue, not an editorial calendar.

The collector still runs every morning at 07:00, because as a triage queue it works. It just should not be allowed to name a number by itself.

Method notes, so the arithmetic can be checked

  • 28 seed phrases × 2 engines = 56 requests. 51 returned a body. The 466 suggestions are unique after lowercasing.
  • Scoring is hits / content_words + 0.15 if any adjacent pair matches; it is a lexical overlap score, not a semantic one. A page that answers a question in different words scores zero.
  • Read 2026-10-26, from the same dashboard run that produced the site's traffic numbers. No API key is involved: both suggestion endpoints are public.
  • The blog is at notebookfield.com, and the Meituan page the scorer missed is here.

Top comments (0)