The Programmatic SEO Content Model: Designing the Database Before You Write a Single Template
Programmatic SEO fails at the data layer, not the template. Here's how we design the content model — entities, attributes, joins, and quality gates — before anyone touches a Handlebars file.
Most programmatic SEO projects die in the template. But the autopsy almost always points back to the database. If your entities are wrong, your attributes are thin, or your joins produce garbage combinations, no amount of clever templating will save the pages from being pruned by Google or ignored by users.
This is a walkthrough of how we design the content model for a pSEO site before anyone opens a template file. It's boring on purpose. Boring is what ships 40,000 pages that actually rank.
Start with the query, not the page
The first mistake teams make is sketching a page layout. "We'll have a hero, a table, some FAQs, a related section." That's a wireframe, not a strategy. The right starting point is the search query pattern you intend to own.
Write the query template out longhand:
[service] in [city][framework] vs [framework] for [use case]how to [task] with [tool] on [platform]
Each slot in that template is an entity in your database. Each entity needs its own table, its own uniqueness constraints, and its own quality bar. If you can't fill a slot with at least 50 distinct, defensible values, that dimension isn't worth building around.
The slot-to-entity mapping
For a query like best [category] tools for [industry] under [budget], you have three entities: category, industry, budget_tier. Multiplied naively that's a lot of pages, but most combinations are nonsense (best CRM tools for beekeepers under $5). The content model has to encode which combinations are valid, not just which are possible.
Rule of thumb: if a human editor would refuse to write the page, the database should refuse to generate it.
Model entities before attributes
Once you know your slots, define the entity tables. Keep them narrow and canonical. A common failure is stuffing everything into a single wide pages table because it feels faster. It isn't. You lose the ability to update one attribute across thousands of pages, and you lose referential integrity the moment a value changes.
Here's a minimal schema for a [tool] alternatives in [year] site:
CREATE TABLE tools (
id SERIAL PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
category_id INT REFERENCES categories(id),
pricing_model TEXT,
founded_year INT,
hq_country TEXT,
is_active BOOLEAN DEFAULT TRUE,
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE tool_features (
tool_id INT REFERENCES tools(id),
feature_id INT REFERENCES features(id),
strength SMALLINT CHECK (strength BETWEEN 0 AND 3),
PRIMARY KEY (tool_id, feature_id)
);
CREATE TABLE tool_alternatives (
tool_id INT REFERENCES tools(id),
alternative_id INT REFERENCES tools(id),
similarity NUMERIC(3,2),
reason TEXT,
PRIMARY KEY (tool_id, alternative_id)
);
Notice what's not there: no page_title, no meta_description, no hero_copy. Those belong in the template layer. The database stores facts. The template composes prose.
Attribute depth is what makes pages defensible
A page that lists a tool's name, category, and pricing model is a stub. Google has seen ten thousand of those. What separates a page that ranks from one that gets deindexed is attribute density — the number of genuinely useful, verifiable facts you can render.
When we audit a client's pSEO plan, we count attributes per entity. Under 8 usable attributes per row, and we push back. A defensible programmatic page usually needs 15–25 attributes it can pull from, even if only 10 render on any given page. That variance is what stops every page looking like every other page.
Where the attributes actually come from
Three honest sources:
- First-party data — user submissions, your own product telemetry, editorial research. Slowest to build, hardest to copy.
- Licensed or API data — vendor APIs, licensed datasets. Fast but expensive and shared with competitors.
- Computed attributes — derivations from the other two. Things like
avg_review_sentiment,feature_overlap_score,price_percentile_in_category. This is where most of the moat lives.
Computed attributes are underrated. They're cheap to generate, unique to your model, and give you infinite text hooks: "X scores in the top 15% of CRMs for pipeline automation but the bottom 30% for reporting depth." No competitor's scraper produces that sentence.
Uniqueness gates: the pre-publish quality filter
Every pSEO system needs a gate between "row exists in database" and "page gets published." Skipping this is how sites end up with 80,000 URLs and 400 that rank.
We implement gates as SQL views or scheduled jobs that materialise a publishable_pages table. A row only lands there if it passes checks like:
SELECT t.id, c.slug AS category_slug, t.slug AS tool_slug
FROM tools t
JOIN categories c ON c.id = t.category_id
WHERE t.is_active = TRUE
AND (SELECT COUNT(*) FROM tool_features WHERE tool_id = t.id) >= 6
AND (SELECT COUNT(*) FROM tool_alternatives WHERE tool_id = t.id) >= 3
AND t.updated_at > now() - INTERVAL '180 days'
AND EXISTS (
SELECT 1 FROM tool_reviews r
WHERE r.tool_id = t.id AND r.word_count > 40
);
The thresholds are opinionated. Six features, three alternatives, a review with substance, and freshness under six months. If a row can't meet that, it doesn't get a URL. It doesn't get a sitemap entry. It doesn't get internal links.
This is unglamorous work, and it's the single biggest lever on crawl efficiency and ranking quality we see.
Schema.org mapping belongs in the model
Structured data shouldn't be an afterthought bolted onto templates. Map each entity to its schema.org type at the model layer, then let the template render JSON-LD deterministically.
For the tools example:
tools→SoftwareApplicationcategories→ThingwithadditionalTypetool_reviews→Reviewnested in the parenttool_alternatives→ItemListofSoftwareApplication
Storing the mapping alongside the entity definition — even as a simple config file — means every new attribute has an obvious home in the markup, and you never end up with orphan fields the crawler doesn't understand.
Versioning and the refresh contract
Programmatic content decays. Prices change, tools get acquired, features ship. Your model needs to know when a row is stale, not just when it was created.
We give every entity two timestamps: data_verified_at and content_regenerated_at. The first is when a human or trusted source last confirmed the facts. The second is when the rendered page was last rebuilt. If data_verified_at is older than 90 days for volatile categories, the row drops out of the publishable set until a refresh job runs.
This pairs neatly with sitemap lastmod values, GSC's freshness signals, and any refresh queue you're operating.
The join table is where creativity lives
Single-entity pages (/tools/notion) are table stakes. The real programmatic surface area is in join pages: /compare/notion-vs-coda, /tools/notion/alternatives-for-startups, /category/note-taking/best-for-teams-under-10.
These join pages are only defensible if the join itself carries information. A comparison page is worthless if it just renders two entity records side by side. It becomes useful when the model can compute:
- Which features overlap and which are unique to each
- Where prices intersect at typical team sizes
- Which user segments prefer which, based on review clustering
These are all queries against the model. If your schema can't answer them cleanly, the join pages will be thin regardless of how pretty the template is.
Where we'd start
If you're standing up a new pSEO project this quarter, resist the urge to open a template file. Instead:
- Write out five real search queries you want to own, with every slot filled by a specific value.
- Design the entity tables for those slots, aiming for at least 15 attributes per entity.
- Identify three computed attributes per entity that competitors can't easily replicate.
- Write the
publishable_pagesgate query before you write any HTML. - Map every entity to a schema.org type and store the mapping in version control.
Do that, and the templates almost write themselves. Skip it, and you'll spend the next year explaining to stakeholders why 90% of your URLs sit in "Crawled — currently not indexed." If you want a second pair of eyes on a content model before it ships, that's the kind of work our team does under content engineering engagements.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading
Log File Analysis for Programmatic SEO: What Googlebot Actually Does With Your 100k URLs
GSC tells you what Google indexed. Log files tell you what Googlebot actually did on the way there. Here's how we pull signal from server logs to fix crawl waste on large programmatic sites.
Indexing Budget Math: Deciding Which Programmatic Pages Deserve to Exist
Most programmatic SEO sites don't have a crawl problem, they have a bloat problem. Here's the math we use to decide which templated pages earn their spot in the index and which should never ship.
Canonical Tag Wars: Debugging Google's 'Duplicate, Google Chose Different Canonical' at Scale
When Google ignores your rel=canonical on thousands of pages, the fix isn't a tag change — it's an evidence problem. Here's how we debug it on programmatic sites.
