Programmatic SEO: a practical guide for headless CMS sites
Programmatic SEO means publishing many pages from one repeatable pattern, each aimed at a query people actually type. Done well, it is how a small team covers hundreds of real searches. Done badly, it is thousands of near-identical pages Google crawls once and never shows. This guide covers the two ways to do it, how to check demand first, and how to build it on a headless CMS like Contentful.
Updated
What programmatic SEO is
Most sites grow page by page: someone decides on a topic, writes it, and publishes it. Programmatic SEO starts from a pattern instead. You notice that people search a family of similar queries, like "[tool] Slack integration", "[city] coworking spaces", or "how to [task] in [product]", and you publish one page for each member of the family from a shared structure.
The pattern is what makes it efficient. The inputs are what make it work. Each page needs facts of its own that answer its query better than a generic page would. Without that, you have one page repeated a thousand times, and search engines treat it that way.
The two shapes: templates and article queues
Almost every programmatic SEO project is one of two things, and choosing the wrong one is the most expensive mistake you can make.
| Template pages | Article queue | |
|---|---|---|
| Unit of work | A row of data | A topic |
| Typical query | "Stripe to Notion integration", "coworking in Lisbon" | "how to structure a blog content model", "headless CMS SEO checklist" |
| What makes each page unique | Its own data: prices, specs, steps, locations, reviews | Its own explanation, examples and advice |
| Where the content comes from | A database or a CMS content type | A writer, human or AI, working from a brief |
| How many pages | Hundreds to thousands | Tens to hundreds |
| Main risk | Pages that differ by one word | Posts that are generic or off topic |
A simple test decides it. Take one query from the family and imagine the best possible page for it. If that page is mostly structured facts that vary from row to row, it is a template. If it is mostly explanation that would have to be written differently each time, it is an article.
Common patterns, and what each page needs
| Pattern | Example query | What each page needs to be worth opening |
|---|---|---|
| Integrations | "[your product] + [tool]" | What the integration does, the setup steps, the data that syncs, limits |
| Comparisons | "[product A] vs [product B]" | A real feature and pricing comparison, and who each one is for |
| Alternatives | "[competitor] alternatives" | Why people leave that product, and specific options for each reason |
| Locations | "[service] in [city]" | Local providers, prices, availability, and anything that differs by place |
| Templates | "[document type] template" | The template itself, a preview, and guidance on filling it in |
| Glossary | "what is [term]" | A clear definition, an example, and how the term is used in your field |
| How-to questions | "how to [task] in [product]" | Working steps for that exact task, with the settings named |
Integrations, comparisons, locations and templates are almost always template pages. Glossaries sit in between. How-to questions and anything that needs judgment are usually better as articles.
Step 1: check demand before you build anything
A pattern is only worth building if people search it. This is where most projects should stop and do not, because the template is more fun to build than the spreadsheet is to fill.
Use Keyword Planner, and read the ranges correctly
Google Keyword Planner is free with a Google Ads account, and you do not have to run a campaign. Paste 10 to 20 real members of the family, not just the head term. Until an account has spent money, Planner shows buckets rather than exact numbers: 0, 50, 500, 5,000. A pattern where every variant shows 50 can still add up across hundreds of pages. A pattern where most variants come back blank has no demand, however logical it looks.
Check for searches that are really about something else
Some patterns look popular because a different meaning dominates. A query that shares a word with a technical term, a brand, or a famous product can show volume that has nothing to do with you. Search each head term yourself and read the first page of results. If none of them are the kind of page you plan to build, the volume belongs to someone else.
Use what you already rank for
If your site is live, the Performance report in Google Search Console shows the queries you already appear for. Families of queries you show up for with no dedicated page are the safest place to start, because Google already connects you with them.
Step 2: decide what makes each page worth opening
Google's spam policies name scaled content abuse: many pages generated primarily to manipulate search rankings instead of helping people, whether they are produced by automation, by people, or both. The method is not the problem. The sameness is.
Before you build, write down the fields that will differ on every page. For an integration page that might be the setup steps, the objects that sync, the trigger options and the plan it requires. If the only field that changes is the name in the heading, do not build the pattern. If you cannot get the data, do not build it yet.
- Would this page still be useful if you deleted the keyword from the heading?
- Does it answer the exact query better than your homepage does?
- Would you link a customer to it?
- Are at least half of the words on it specific to this page?
Step 3: model it in Contentful and render it in your front end
On a headless CMS, template pages split cleanly in two. Contentful holds the data, one entry per page. Your front end holds the template, one route for every entry.
For an integrations directory, the content type might be integration with a name, a slug, a logo, a short description, a list of use cases, setup steps as rich text, and references to related integrations. Those references matter later, because they become the links between pages.
import { createClient } from 'contentful';
import { notFound } from 'next/navigation';
const client = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN,
});
const getIntegration = async (slug) => {
const { items } = await client.getEntries({
content_type: 'integration',
'fields.slug': slug,
limit: 1,
});
return items[0] ?? null;
};
export async function generateStaticParams() {
const { items } = await client.getEntries({
content_type: 'integration',
select: ['fields.slug'],
limit: 1000,
});
return items.map(item => ({ slug: item.fields.slug }));
}
export async function generateMetadata({ params }) {
const { slug } = await params;
const integration = await getIntegration(slug);
if (!integration) return {};
return {
title: `${integration.fields.name} integration`,
description: integration.fields.description,
};
}
export default async function IntegrationPage({ params }) {
const { slug } = await params;
const integration = await getIntegration(slug);
if (!integration) notFound();
// Render name, use cases, setup steps and related integrations here.
}Add a webhook in Contentful on Entry publish that revalidates the route, and a new entry becomes a live page without a deploy. Include every route in your sitemap, generated from the same query.
Step 4: link the pages together
A page nobody links to is a page search engines find late or not at all. Every programmatic page should be reachable in a few clicks from a hub, like an integrations index or a glossary index, and should link across to its closest neighbours. Store those relationships as references in Contentful rather than hard coding them, so a new entry can point at its neighbours the moment it is published.
Articles need the same thing. Each new post should link to the earlier posts it relates to, on a natural phrase, and the earlier posts should gain links as the blog grows.
Step 5: launch small and watch indexing
Publish your strongest 20 to 50 pages first, with the richest data and the clearest demand. Submit the sitemap in Search Console and watch the Page indexing report for the next few weeks.
| Status | What it usually means |
|---|---|
| Discovered, currently not indexed | Google knows the URL but has not crawled it yet. Common on new sites. Better internal links and patience help. |
| Crawled, currently not indexed | Google read the page and chose not to index it. On programmatic pages this is usually a quality signal: the pages are too similar or too thin. |
| Duplicate without user-selected canonical | Google considers the page a copy of another one. Pages differ too little, or canonicals are missing. |
If most of the first batch lands in "crawled, currently not indexed", publishing the next thousand will not help. Fix the template or the data first.
Step 6: measure, then improve or prune
In the Performance report, filter by URL prefix, such as /integrations/, to see the whole pattern at once. Give it eight to twelve weeks. Pages with impressions but few clicks usually need a better title and meta description. Pages with no impressions at all either target a query nobody searches, or are not indexed. Improve them, merge them into a stronger page, or remove them. A smaller set of pages that all earn impressions beats a large set that mostly does not.
Where Frogpost fits
Frogpost is the article queue, for Contentful. You add topics, and each one becomes a post of at least 1,500 words with its own headings, SEO title and meta description, written from what your website says about your product. It is created as a Contentful entry in your blog content type, with images uploaded as assets and links to your earlier posts. You can keep posts as drafts, or publish a queue on a schedule.
It does not build template pages from a dataset. If your pattern is integrations, locations or comparisons, build the template in your front end as above, and use Frogpost for the articles that sit around it: the guides and how-to posts that link into those pages and give them context.
Mistakes that sink programmatic SEO projects
- Building the template before checking that the pattern is searched.
- Launching thousands of pages at once, so a quality problem affects all of them.
- Pages that differ only by the keyword in the heading and the title tag.
- No hub pages and no links between neighbours, so most pages are orphans.
- Leaving pages with no impressions live for a year instead of improving or removing them.
- Using a data template for queries that need an explanation, or articles for queries that need data.
Questions
- What is programmatic SEO?
- Publishing many pages from one repeatable pattern, each aimed at its own search query. The pattern is usually a template filled from a dataset, such as one page per integration or per city, or a queue of articles written from a list of topics.
- Is programmatic SEO against Google guidelines?
- Not in itself. Google targets scaled content abuse, meaning many pages produced mainly to manipulate rankings rather than to help people, whether they are made by automation, by people, or both. Pages that each answer their query with facts of their own are ordinary content published at scale.
- How many pages should I launch with?
- Start with the 20 to 50 pages with the clearest demand and the richest data. Watch how many get indexed and whether they earn impressions in Search Console before you publish the rest. Launching thousands of thin pages at once is the most common way to get most of them ignored.
- Can I do programmatic SEO with Contentful?
- Yes. Model each entity as a content type, keep one entry per page, and let your front end generate a route for every entry. Contentful holds the data and your framework renders the template.
- Is a blog queue programmatic SEO?
- It is the article form of it. Each topic becomes its own post with its own structure, published on a schedule. It suits queries that need explanation, where a data template would produce the same paragraph with one word swapped.
- Can Frogpost generate programmatic landing pages?
- No. Frogpost writes SEO blog posts and publishes them to Contentful. Template pages built from a dataset belong in your front end, with the data in Contentful.

