All articles
SEO & GrowthSeptember 11, 2026 6 min read

The Programmatic SEO Data Model: Designing Entities Before You Write a Single Template

Most programmatic SEO projects fail at the schema, not the template. Here's how we design entities, attributes, and relationships so pages have something real to say.

The Programmatic SEO Data Model: Designing Entities Before You Write a Single Template

Every failed programmatic SEO project we've inherited has the same root cause, and it isn't the templates or the CMS. It's the data model. Someone spun up 40,000 pages from a spreadsheet with three columns and wondered why Google indexed 2,000 of them and ranked none.

Programmatic SEO is a data problem dressed up as a content problem. If your entities are thin, your pages will be thin, and no amount of clever templating or internal linking will save them. This is how we approach the modeling phase before anyone opens a .tsx file.

Start With the Query, Not the Entity

The temptation is to open your database, look at what you already have, and ask "what pages can we generate from this?" That's backwards. It produces pages that exist because data existed, not because demand existed.

Instead, start with the search query pattern you want to own. Something like:

  • [job title] salary in [city]
  • [framework] vs [framework] for [use case]
  • best [tool category] for [company size]

Each pattern implies an entity graph. [job title] salary in [city] needs a JobTitle entity, a City entity, and a relationship between them that carries at least one meaningful attribute — median salary, sample size, source, freshness date. If you can't populate that relationship with real, defensible data, kill the pattern before you build the template.

The "thin page" test

Before we commit to a pattern, we write out what a single instance of the page would contain if a human wrote it. If the honest answer is "three sentences and a table with two rows," the entity is too thin. Either enrich it with more attributes, merge it with a sibling entity, or drop it.

The Three-Layer Entity Model

We model programmatic sites in three layers. This isn't a framework we invented — it's just how relational data naturally organises itself when you stop fighting it.

Layer 1: Primary entities. The nouns your users search for. Products, cities, job titles, integrations, frameworks. Each one gets a stable slug and a canonical URL.

Layer 2: Modifiers. The adjectives and qualifiers that create long-tail intent. Company size, use case, price tier, industry, experience level. Modifiers rarely deserve their own pages, but they filter and slice primary entities.

Layer 3: Facts. The actual content payload — numbers, quotes, comparisons, timestamps, sources. Facts are what make a page worth indexing. Everything else is chrome.

A rough TypeScript sketch of what this looks like in practice:

type PrimaryEntity = {
  id: string;
  slug: string;
  type: 'job_title' | 'city' | 'tool' | 'integration';
  canonicalName: string;
  aliases: string[];
  createdAt: Date;
  lastVerifiedAt: Date;
};

type EntityRelationship = {
  fromId: string;
  toId: string;
  relationshipType: 'available_in' | 'integrates_with' | 'compared_to';
  facts: Fact[];
};

type Fact = {
  key: string;
  value: string | number;
  unit?: string;
  source: string;
  observedAt: Date;
  confidence: 'high' | 'medium' | 'low';
};

Notice that every fact carries a source and an observation date. This is non-negotiable. Without it, you can't answer "is this page stale?" and you can't defend the content to a manual reviewer.

Attribute Density Is the Real Ranking Signal

Google doesn't have a documented "attribute density" metric, but in our experience the pages that survive on programmatic sites are the ones with the most unique, verifiable attributes per URL. A page about "React developer salary in Austin" that only has a median number will get outranked by one that has median, 25th percentile, 75th percentile, YoY change, sample size, top employers, and a comparison to two nearby metros.

When we design the schema, we set an attribute floor per entity type. Something like:

  • Every JobTitle x City page must resolve at least 8 distinct facts
  • Every Tool x Tool comparison page must resolve at least 12
  • Every Integration page must resolve at least 6, including one code example

If a specific combination can't hit the floor, it doesn't get a page. It gets a redirect to the parent entity or gets excluded from the sitemap entirely. This is where most pSEO projects hemorrhage crawl budget — they publish everything the cross-join produces, regardless of whether the payload justifies a URL.

Sparse combinations are your enemy

A cross-join of 500 job titles and 300 cities gives you 150,000 potential pages. Realistically, maybe 40,000 of those combinations have enough underlying data to be worth publishing. The other 110,000 will either be near-duplicates of a more populated sibling or will trigger soft-404 treatment. Model the sparsity honestly and prune at the data layer, not the render layer.

Relationships Are Where Uniqueness Lives

Two pages about the same primary entity — say, "Stripe" — will look identical unless the relationship layer differentiates them. Stripe x Shopify and Stripe x WooCommerce should feel like fundamentally different pages, not the same page with two logos swapped.

That means the relationship itself needs its own attributes: setup complexity, supported currencies, webhook coverage, known limitations, community sentiment. If you model relationships as junction tables with only foreign keys, every generated page will read the same. If you model them as first-class entities with their own facts, each page has a reason to exist.

A pragmatic rule: the relationship table should have more columns than either of the entities it joins.

Freshness as a First-Class Field

We treat lastVerifiedAt as mandatory on every fact, not just every page. Page-level freshness is a lie when 90% of the content is static template copy and only one number was refreshed last month.

Store observation timestamps at the fact level, then compute page-level freshness as a weighted function of the facts that actually matter to users. For a salary page, the salary numbers should be weighted heavily; the city population sitting in your sidebar can be a year old and nobody cares.

This lets you build a proper re-crawl priority queue: pages where the high-weight facts are aging get regenerated first, and low-priority pages don't waste your writer budget or your ISR quota.

Content Model, Not Content Templates

One mistake we see repeatedly: teams design a single Handlebars-style template and force every entity through it. Then when a specific vertical needs a different section — say, integration pages need a code example but salary pages don't — they hack conditional logic into the template until nobody can read it.

Better approach: model page sections as their own entities, keyed to entity type.

type PageSection = {
  id: string;
  entityType: string;
  slotName: 'hero' | 'stats' | 'comparison' | 'faq' | 'code_example';
  requiredFacts: string[];
  renderComponent: string;
  order: number;
};

Now adding a new section to a vertical is a data change, not a code change. And you can A/B test section ordering per vertical without redeploying.

Governance: Who Owns the Data

The last piece is the one engineers forget. A programmatic SEO data model is a living system — sources deprecate, APIs change, facts go stale. Assign ownership per fact type. Salary data owned by one person, integration coverage owned by another, city metadata pulled from a vendor with a documented refresh cadence.

Without owners, the model rots in about six months. We've walked into audits where the underlying data hadn't been refreshed since launch and the client couldn't figure out why rankings had drifted downward for a year straight. It wasn't an algorithm update. It was neglect at the data layer.

Where We'd Start

If you're building a programmatic SEO surface from scratch, spend the first two weeks on the data model and zero time on templates. Specifically:

  1. Write out 5 – 10 target query patterns and the entity graph each one implies.
  2. Sketch the primary entities, modifiers, and facts. Set an attribute floor per page type.
  3. Model relationships as first-class tables with their own attributes, not bare junction tables.
  4. Instrument every fact with source and observedAt from day one.
  5. Only then start on templates, sections, and rendering strategy.

If you want a second pair of eyes on an existing content engine — the entity graph, the fact coverage, the freshness governance — that's the kind of audit we do inside our SEO and growth engagements. Get the model right and the templates almost write themselves. Get it wrong and no amount of template polish will rescue the traffic.

#programmatic-seo#data-modeling#content-engineering#seo

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project