Thin Content Detection at Scale: A Scoring Pipeline That Flags Pages Before Google Does
Google's Helpful Content signals punish thin pages before you see traffic drops. Here's the scoring pipeline we use to flag weak programmatic pages weekly — and what we do with the results.

If you run a programmatic site with more than a few thousand URLs, you already know the uncomfortable truth: a meaningful chunk of your index is thin, and you can't eyeball your way out of it. Google figures this out before you do, and the first symptom is usually a slow bleed in impressions on your long tail — not a dramatic drop. The fix isn't a one-off audit. It's a scoring pipeline that runs every week and tells you which pages to improve, merge, noindex, or delete.
This is how we build that pipeline for clients, what features actually correlate with quality signals, and the triage workflow we hand to content teams.
Why "thin" is the wrong word to optimise against
Most teams define thin content as "fewer than N words." That's a trap. We've seen 400-word pages rank perfectly fine and 2,000-word pages get deindexed inside a quarter. Google's quality systems care about whether a page satisfies the query better than alternatives — word count is a weak proxy at best.
A useful scoring pipeline measures three things:
- Uniqueness — how different is this page from its siblings in the same template?
- Substance — does it contain information a user actually came for?
- Engagement reality — are humans and crawlers behaving like it's useful?
If you score only the first two, you'll ship a pipeline that flags pages Google already loves. If you score only the third, you'll flag new pages that haven't had time to prove themselves. You need all three, weighted differently depending on page age.
The feature set that actually correlates with quality
After running this across e-commerce catalogues, directory sites, and location-based SaaS pages, here's the feature set we keep coming back to. None of these are proprietary — the value is in combining them.
Content features
- Unique token ratio: tokens on this page that don't appear on 80%+ of sibling pages in the same template. Boilerplate kills this.
- Entity density: named entities per 100 words, extracted with a small NER model. A city page that mentions no neighbourhoods, landmarks, or local businesses is thin regardless of word count.
- Structured data completeness: percentage of optional schema fields populated (not just required ones).
- Media presence: at least one non-stock image, video, or interactive element.
- Outbound citation count: links to authoritative external sources. Zero is a red flag on informational pages.
Behavioural features (from GA4 + GSC)
- Impressions-to-clicks ratio over trailing 28 days, compared against the template median.
- Average position decay: slope of average position over the last 12 weeks. A page sliding from 18 to 42 is screaming at you.
- Engaged sessions per entrance, excluding bots.
- Scroll depth median if you're tracking it (we recommend you do).
Technical features
- Internal link in-degree: pages with fewer than 3 internal links from meaningful contexts tend to underperform.
- Last meaningful update: timestamp of the last content change, not template change.
- Rendered vs raw content ratio: how much of the final DOM is JS-injected. Hydration gaps still matter in 2026.
The scoring model
We don't use ML for the first version. A weighted linear score is easier to debug, easier to explain to clients, and good enough to beat human triage. Save the gradient boosting for v2.
Here's a simplified version of the scoring query we run against a warehouse that already has GSC, GA4, and crawl data loaded:
WITH page_features AS (
SELECT
p.url,
p.template_id,
p.published_at,
p.word_count,
p.unique_token_ratio,
p.entity_density,
p.schema_completeness,
p.internal_in_degree,
g.impressions_28d,
g.clicks_28d,
g.avg_position_slope_12w,
a.engaged_sessions_per_entrance,
a.scroll_depth_median
FROM pages p
LEFT JOIN gsc_rollup g USING (url)
LEFT JOIN ga4_rollup a USING (url)
),
normalised AS (
SELECT
url,
template_id,
-- min-max within template to control for template-level variance
(unique_token_ratio - MIN(unique_token_ratio) OVER w)
/ NULLIF(MAX(unique_token_ratio) OVER w - MIN(unique_token_ratio) OVER w, 0) AS utr_n,
(entity_density - MIN(entity_density) OVER w)
/ NULLIF(MAX(entity_density) OVER w - MIN(entity_density) OVER w, 0) AS ent_n,
schema_completeness AS sch_n,
LEAST(internal_in_degree / 10.0, 1.0) AS link_n,
LEAST(engaged_sessions_per_entrance, 1.0) AS eng_n,
CASE WHEN avg_position_slope_12w < -5 THEN 0
WHEN avg_position_slope_12w > 0 THEN 1
ELSE 0.5 END AS pos_n
FROM page_features
WINDOW w AS (PARTITION BY template_id)
)
SELECT
url,
ROUND(
0.20 * utr_n +
0.15 * ent_n +
0.10 * sch_n +
0.15 * link_n +
0.20 * eng_n +
0.20 * pos_n
, 3) AS quality_score
FROM normalised
ORDER BY quality_score ASC;
Weights are a starting point, not gospel. Calibrate them against pages you already know are good or bad on your own site.
Handling the cold-start problem
Pages younger than 60 days don't have reliable behavioural signals. We give them a provisional score based only on content and technical features, and we flag them separately in the triage queue. Don't delete new pages because they haven't earned clicks yet — that's how teams accidentally kneecap their own launches.
From score to action: the triage buckets
A score on its own is a vanity metric. What matters is what you do with it. We bucket pages into four actions:
- Keep (score ≥ 0.6): no action, re-score next week.
- Improve (0.35 – 0.6): queue for content enrichment. Usually the biggest bucket and the one with the best ROI.
- Merge (0.2 – 0.35, with a strong semantic neighbour): redirect to a better-performing sibling. Useful on over-faceted catalogues.
- Prune (< 0.2, no clicks in 90 days, no backlinks): noindex first, then 410 after a crawl cycle.
We never delete in bulk on the first pass. Noindex for 4 – 6 weeks, confirm Google has dropped the URL from the index via the GSC API, then remove. This gives you a chance to catch false positives — and there will be false positives.
What "improve" actually means
The improve bucket is where most of the lift comes from, and it's where most teams give up because the work feels fuzzy. Make it concrete. For each page in the bucket, generate a diff list: which features scored below the template median, and what the minimum fix looks like. For a location page that scored low on entity density, that might be "add 5 named neighbourhoods and 2 local landmarks." For a product page with low unique token ratio, it's "rewrite the description without the shared boilerplate paragraph."
Hand that diff to writers or an LLM-assisted workflow with a human reviewer. Don't let a model rewrite pages unsupervised — you'll trade thin content for plausible-sounding slop, which Google is now very good at detecting.
Running it as a weekly job
We schedule the scoring job weekly, not daily. Behavioural signals need time to stabilise, and daily churn creates alert fatigue. The output is a single dashboard with four counts (keep / improve / merge / prune), a trend line per template, and a CSV export for the content team.
Two things we've learned the hard way:
Score drift is a feature, not a bug. If your "keep" bucket starts shrinking across templates, something structural is wrong — usually a template change that broke uniqueness or a tracking regression that broke engagement signals.
And second: version your weights. When you retune them, you want to be able to explain to a client why a page moved buckets. Store the weight vector alongside each score so historical scores remain reproducible.
Where this fits with the rest of your SEO stack
This pipeline sits downstream of your programmatic SEO data model and upstream of your editorial workflow. It assumes you already have GSC and GA4 flowing into a warehouse — if you don't, build that first, because without behavioural signals you're scoring in the dark. It also pairs well with log-file analysis: pages that score low and get crawled infrequently are the strongest prune candidates.
If you're running AdSense on these pages, there's a second reason to care. Thin pages hurt viewability-weighted RPM, and they drag down site-level quality scores that affect auction eligibility. Pruning the bottom 10% of a programmatic site has, in our experience, lifted both organic clicks and ad revenue on the surviving pages — because crawl budget and link equity concentrate where they belong.
Where we'd start
If you've got a programmatic site over 10k URLs and you've never scored it, don't boil the ocean. Pick one template — the one you suspect is weakest — and build the pipeline against just that template first. Use three content features, two behavioural features, and a single weighted score. Ship it in a week, run it for a month, and let the triage buckets argue with your intuition. You'll find out quickly whether your weights are sensible, and you'll have a working template to extend across the rest of the site. That beats a six-week audit that nobody reads.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Index Bloat on Programmatic Sites: A Pruning Playbook That Actually Lifts Traffic
Programmatic sites don't usually die from bad content — they drown in mediocre content. Here's the pruning workflow we run quarterly to cut index bloat without torching the pages that earn.

Content Freshness Signals on Programmatic Pages: What Actually Moves Rankings
Updating a timestamp isn't freshness. Here's how we decide which programmatic pages to refresh, what to actually change, and how to measure whether Google cares.

Internal Linking at Scale: The Graph Model That Beats Related-Posts Widgets
Related-posts widgets are how programmatic sites bleed authority. Here's how we model internal links as a graph, score edges, and ship a link plan that actually moves rankings.
