Fix: Thin content
Pages with less than 200 words of primary text fail the content-length rule. Search engine crawlers parse your raw HTML, strip out the tags, script blocks, and styling, and evaluate the remaining text nodes. When this extracted text is too brief, search engines classify the route as "thin content." This is an active liability for your application. Under Google's Helpful Content system, quality is evaluated at the domain level. If your Next.js app dynamically generates 5,000 user profile pages with 15 words each, the negative quality signal from those thin pages will drag down the search performance of your core 50 landing pages.
To pass the rule, a page needs at least 300 words of substantive, unique text. Filler text or duplicated boilerplate does not count. Search engines look for semantic relevance—text that answers user intent through descriptions, features, use cases, or frequently asked questions.
How do you detect the content-length warning?
Run npx indxel check against your local build or npx indxel crawl against your staging URL. The CLI outputs exact word counts for every route and flags pages that fall below the 300-word threshold.
npx indxel crawl https://staging.example.comThe CLI outputs warnings in the same format as ESLint — one line per issue, with the file path, rule ID, and current word count.
app/products/[id]/page.tsx
✖ content-length: ERROR (12 words). Minimum required: 50.
app/features/analytics/page.tsx
⚠ content-length: WARNING (140 words). Target: 300+.
app/about/page.tsx
✓ content-length: PASS (412 words).Indxel applies two severity levels for the content-length rule. Pages with 50-199 words trigger a WARNING (weight: 5/100). Pages with under 50 words trigger a critical ERROR, as these pages are completely unindexable.
How do you fix thin content in Next.js?
Fixing thin content requires rendering actual text nodes in the DOM. You must pull in additional data fields from your CMS or database—such as long-form descriptions, feature lists, and FAQs—and map them into your JSX.
Next.js App Router
In this bad example, the dynamic product page only renders the product name, price, and an image. The resulting HTML payload contains exactly 14 words of text.
// ❌ BAD: 14 words total. Fails content-length rule.
import { getProduct } from '@/lib/db';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<main>
<h1>{product.name}</h1>
<img src={product.imageUrl} alt={product.name} />
<p>${product.price}</p>
<button>Add to Cart</button>
</main>
);
}To fix this, fetch the expanded relational data for the product. Render the description, map over the features array, and render an faq section. This easily pushes the page past the 300-word threshold with highly relevant, indexable text.
// ✅ GOOD: 300+ words of structured, relevant content.
import { getProduct, getProductFAQs } from '@/lib/db';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
const faqs = await getProductFAQs(params.id);
return (
<main>
<header>
<h1>{product.name}</h1>
<p className="lead">{product.shortDescription}</p>
</header>
<section className="long-description">
<h2>About this product</h2>
{/* Render the 200+ word long description */}
<div dangerouslySetInnerHTML={{ __html: product.longDescription }} />
</section>
<section className="features">
<h2>Technical Specifications</h2>
<ul>
{product.features.map((feature) => (
<li key={feature.id}>
<strong>{feature.name}:</strong> {feature.details}
</li>
))}
</ul>
</section>
<section className="faq">
<h2>Frequently Asked Questions</h2>
{faqs.map((faq) => (
<article key={faq.id}>
<h3>{faq.question}</h3>
<p>{faq.answer}</p>
</article>
))}
</section>
</main>
);
}Next.js Pages Router
If you are maintaining a Pages Router application, the exact same principle applies inside getServerSideProps or getStaticProps. You must fetch the extended text data on the server and pass it as props.
// ✅ GOOD: Passing rich text data via getStaticProps
import { GetStaticProps } from 'next';
import { getArticle } from '@/lib/cms';
export default function ArticlePage({ article }) {
return (
<main>
<h1>{article.title}</h1>
<div className="content" dangerouslySetInnerHTML={{ __html: article.body }} />
<aside className="author-bio">
<h3>About {article.author.name}</h3>
<p>{article.author.biography}</p>
</aside>
</main>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
const article = await getArticle(context.params.slug);
return {
props: { article },
revalidate: 3600
};
};Plain HTML
For static sites, ensure your .html files contain actual paragraph tags with descriptive content, not just links and images.
<!-- ❌ BAD: 18 words -->
<body>
<h1>Data Sync Tool</h1>
<p>The best way to sync your data.</p>
<a href="/download">Download Now</a>
</body>
<!-- ✅ GOOD: 300+ words -->
<body>
<main>
<h1>Data Sync Tool</h1>
<p>The Data Sync Tool provides bi-directional replication between your Postgres database and your Redis cache, ensuring zero-downtime migrations...</p>
<h2>How it works</h2>
<p>When a transaction commits to the primary database, the replication slot captures the WAL (Write-Ahead Log) event...</p>
<h2>Common Use Cases</h2>
<ul>
<li><strong>E-commerce inventory:</strong> Keep stock counts perfectly aligned across all edge nodes.</li>
<li><strong>Session management:</strong> Invalidate stale sessions globally within 50 milliseconds.</li>
</ul>
</main>
</body>The Indxel SDK approach
While you must render the physical text in your React components, you use the Indxel SDK's createMetadata to ensure search engines understand the context of that text. Rich content paired with missing metadata still results in poor indexing.
Bind your rich content directly to your metadata definition.
import { createMetadata } from '@indxel/sdk';
import { getProduct } from '@/lib/db';
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
// createMetadata validates your tags before rendering
return createMetadata({
title: `${product.name} | Technical Specs & Features`,
// Use the first 160 characters of your rich content for the description
description: product.longDescription.substring(0, 160),
openGraph: {
type: 'website',
url: `https://example.com/products/${params.id}`,
}
});
}
export default async function Page({ params }) {
// Render the 300+ words of content here
}How to prevent thin content from reaching production?
Add npx indxel check --ci to your build pipeline. This commands fails the build step if any static route contains an ERROR (under 50 words) or WARNING (under 200 words).
In your GitHub Actions workflow, add the check immediately after your build step.
name: SEO CI Guard
on: [push, pull_request]
jobs:
validate-seo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
- run: npm ci
- name: Build Next.js app
run: npm run build
- name: Run Indxel SEO validation
# The --ci flag exits with code 1 if any rule fails
# The --diff flag only checks routes changed in this PR
run: npx indxel check --ci --diffIf you deploy on Vercel, you can intercept thin content before the deployment goes live by modifying your Build Command in the Vercel Dashboard.
- Go to Settings > General > Build & Development Settings.
- Override the Build Command:
npm run build && npx indxel check --ciUse the --diff flag in CI environments. This ensures the build only fails if the current pull request introduces new thin pages, rather than failing on legacy pages you haven't fixed yet.
What are the edge cases for content length?
Developers frequently ship empty HTML payloads because of Client-Side Rendering (CSR). If you fetch your page content inside a useEffect hook, the server sends an empty <div> to the browser. When the Indxel crawler (and Googlebot) parses the initial HTML, the word count is zero. You must use Server-Side Rendering (SSR) or Static Site Generation (SSG) to ensure the text is present in the raw HTML payload.
Another common failure point is text hidden inside images. Text rendered within a .png or .svg is completely invisible to the DOM parser. While alt text is required for accessibility, it does not count toward the primary content length of the page. You must extract critical text from images and render it as HTML text.
Finally, boilerplate pollution skews your perceived word count. Your site might have a 400-word mega-menu and a 200-word footer. Indxel explicitly strips <nav>, <footer>, <aside>, and <header> tags before calculating the content-length rule. If you place your primary article text inside an <aside> tag by mistake, Indxel and search engines will ignore it, resulting in a thin content penalty. Always wrap your primary text in <main> or <article> tags.
Related rules
missing-h1: Thin pages often lack a primary heading. Every page requires exactly one<h1>tag to define its structural outline.missing-meta-description: If a page lacks enough text for thecontent-lengthrule, it almost certainly lacks a valid<meta name="description">.duplicate-content: Thin pages generated from templates often trigger the duplicate content rule, as the only text on the page is the shared navigation and footer boilerplate.
Frequently asked questions
What is the minimum word count for SEO?
There is no official minimum, but pages with under 200 words of primary text are classified as thin content. Indxel requires 300 words to pass the content-length rule, issues warnings for 50-199 words, and throws errors for under 50 words.
Does word count directly affect rankings?
Not directly. However, thin content provides zero value to users, which destroys engagement signals. Google's Helpful Content system evaluates quality at the domain level, meaning a large batch of thin pages will penalize your high-quality pages.
Do hidden tabs and accordions count towards content length?
Yes, if the text is present in the initial HTML payload. Search engines index text inside hidden <div> elements or CSS-toggled accordions, provided the text is in the DOM on initial load and not fetched client-side upon clicking.
How does Indxel count words on a page?
Indxel fetches the raw HTML, strips all script tags, style blocks, and boilerplate semantic tags (<nav>, <footer>, <aside>). It extracts the text nodes from the remaining <main> and <body> elements and splits the string by whitespace.
How do I bypass the thin content warning for utility pages?
Add the utility route to your indxel.config.js ignore list. Pages like /login, /reset-password, or /app/dashboard inherently have low word counts and should be excluded from SEO validation via the ignoreRoutes array.
Frequently asked questions
What is the minimum word count for SEO?
There's no official minimum, but pages with under 200 words are generally classified as thin content. Indxel uses 300 words as the pass threshold, with warnings for 50-199 words and errors for under 50.
Does word count directly affect rankings?
Not directly. But thin content provides less value to users, which reduces engagement signals. Google's Helpful Content system evaluates content quality at the site level.