Most HubSpot sites publish pages one at a time. But a large share of commercial search demand is patterned — 'X for Y', 'X in [city]', 'X vs Y', 'X integration with Z' — and writing those by hand is slow enough that teams never finish.
HubDB plus dynamic page templates solves that natively. One table, one template, and every row becomes its own URL with its own title, description, schema and internal links.
The catch is that the same mechanism produces doorway pages if the only thing that changes between rows is a swapped noun. This guide covers the build and the editorial line, and pairs with our HubSpot technical SEO work.
When programmatic pages are the right answer
Use them where each row has genuinely distinct facts: real inventory, real locations you serve differently, real integrations with different setup steps, or real comparison data. The test is whether a knowledgeable person would write a materially different page for each row.
Do not use them to spin one service page across fifty towns you have never worked in. Search engines have been demoting that pattern for a decade and AI answer engines summarise it away entirely.
A useful heuristic: if you cannot supply at least three unique, verifiable facts per row, the row should be a section on a hub page rather than its own URL.
| Pattern | Works when | Fails when |
|---|---|---|
| Location pages | Real presence, staff, pricing or projects per location | Same copy with the city name swapped |
| Integration pages | Distinct setup steps, screenshots, limits per tool | Generic 'we integrate with X' boilerplate |
| Comparison pages | Real feature and pricing data you maintain | Opinion with no data, duplicated per competitor |
| Product or inventory | Live specs, availability, images | Placeholder specs pending 'phase 2' |
| Glossary / definitions | Genuine depth, examples, related terms | Two-sentence dictionary entries |
Designing the HubDB table
Model the page, not the spreadsheet. Every element that varies — H1, meta title, meta description, intro, three to five body blocks, FAQ pairs, image, canonical override — should be its own column, so editors never touch HubL to change copy.
Include a published flag and a last-reviewed date column. Rows that are not ready should not be discoverable, and you want to be able to report on stale rows the way you would with a blog.
Add a slug column and keep it stable. Regenerating slugs from a name column means a typo fix silently changes a URL, which is how programmatic sets quietly lose their rankings.
{% if dynamic_page_hubdb_row %}
{% set row = dynamic_page_hubdb_row %}
<h1>{{ row.h1 }}</h1>
<p>{{ row.intro }}</p>
<section>
<h2>{{ row.section_1_heading }}</h2>
{{ row.section_1_body }}
</section>
<section>
<h2>{{ row.section_2_heading }}</h2>
{{ row.section_2_body }}
</section>
{% endif %}Wiring the dynamic page template
In the template settings, point the page at the HubDB table so HubSpot generates a listing page at the parent URL and a detail page per row. The listing page is not an afterthought — it is the hub that passes authority to every row.
Set the head tags from row data, not from the template defaults. A programmatic set where every page shares one meta title is the single most common way these builds fail an audit.
Handle the missing-row case explicitly with a 404 rather than rendering an empty shell, otherwise every mistyped URL becomes an indexable blank page.
{% set row = dynamic_page_hubdb_row %}
{% if row %}
<title>{{ row.meta_title }}</title>
<meta name="description" content="{{ row.meta_description }}">
<link rel="canonical" href="https://www.example.com/locations/{{ row.hs_path }}">
{% endif %}Schema for programmatic pages
Each row type has an obvious schema match: LocalBusiness or Service for locations, Product or SoftwareApplication for integrations, FAQPage where the row carries genuine questions, and BreadcrumbList everywhere.
Generate it from the same row fields that render the visible content, so the markup can never drift from the page. Schema that describes content the visitor cannot see is a violation, not a shortcut.
The mechanics — where to place the script, how to escape values, how to validate — are covered in the JSON-LD implementation guide for HubSpot.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.example.com/" },
{ "@type": "ListItem", "position": 2, "name": "Locations", "item": "https://www.example.com/locations" },
{ "@type": "ListItem", "position": 3, "name": "{{ row.name|escapejs }}" }
]
}
</script>Internal linking is what makes the set rank
Orphaned rows do not get crawled reliably. The listing page should link to every row, each row should link back to the hub and to two or three sibling rows, and your main navigation or footer should link to the hub itself.
Add contextual links from existing high-authority pages into the most commercially valuable rows. A programmatic set with no editorial links pointing into it behaves like a separate, weaker site.
Include the rows in your sitemap and check crawl stats after launch. If Google is only fetching a fraction of the set, your linking is too flat, not your content too thin.
Performance at scale
Listing pages with hundreds of rows need pagination, lazy-loaded images and a sensible page size. A single unpaginated listing rendering 800 rows is a Core Web Vitals problem before it is an SEO one.
Keep row images in HubSpot's CDN with explicit dimensions, and avoid client-side filtering that hides content from crawlers. Filters should update the URL and render server-side where the filtered view has search demand.
The wider performance checklist is in our post on HubSpot Core Web Vitals.
Governance: how these sets rot
Programmatic sets decay faster than hand-written pages because nobody owns any single row. Assign an owner, set a review cadence, and use the last-reviewed column to build a stale-row report.
Prune aggressively. Rows that have had no impressions after two quarters should be merged into the hub or removed and redirected — a smaller set of genuinely useful pages outperforms a large set of near-duplicates.
Before scaling from twenty rows to two hundred, confirm the first twenty are actually earning impressions. Publishing at scale is only leverage when the unit works.
