A technical deep dive into how Eonebill uses programmatic SEO to rank for 500+ invoice template keywords — the architecture, trade-offs, and hard lessons learned.

When we launched Eonebill's invoice template library, we faced a familiar SEO challenge: hundreds of long-tail keywords with clear search intent and near-zero competition. "Free photography invoice template." "Cleaning services invoice PDF." "Consulting invoice template Google Docs."
Each keyword deserves its own page. But writing 500+ pages by hand is not a content strategy — it's a content factory. We needed a different approach.
This is how we built Eonebill's programmatic SEO engine and what we learned building it.
Programmatic SEO (pSEO) is the practice of generating large numbers of optimized, template-driven pages programmatically — rather than writing each one manually.
The key difference from traditional SEO:
The strategy works when:
[industry] invoice template)Invoice templates check all three boxes.
Our pSEO system has four core components:
┌─────────────────────────────────────────────────────────┐
│ 1. Template Definitions (MDX) │
│ content/pages/invoice-templates/ │
├─────────────────────────────────────────────────────────┤
│ 2. Data Generator (TypeScript) │
│ src/lib/pseo/ │
├─────────────────────────────────────────────────────────┤
│ 3. Page Generator (Next.js App Router) │
│ src/app/[category]/[slug]/ │
├─────────────────────────────────────────────────────────┤
│ 4. Sitemap & Metadata (Automated) │
│ next-sitemap + dynamic metadata │
└─────────────────────────────────────────────────────────┘
Each invoice template type is defined as an MDX file with structured frontmatter:
---
title: "Free Photography Invoice Template"
slug: "photography-invoice-template"
category: "creative-services"
industry: "photography"
features:
- "Instant PDF download"
- "AI-powered line item suggestions"
- "US 1099 compliant fields"
keywords:
- "photography invoice template"
- "free photography invoice pdf"
- "photographer invoice template"
---
The MDX files are processed by our content layer (Fumadocs), which generates a manifest of all template types.
Here's where it gets interesting. We don't hardcode 500 page routes. Instead, we generate them from a combination of:
The generator is a TypeScript function that computes all valid combinations:
// src/lib/pseo/template-generator.ts
interface TemplateConfig {
industry: string
format: 'pdf' | 'google-docs' | 'excel' | 'word'
features: string[]
locale: string
}
const INDUSTRIES = ['photography', 'consulting', 'cleaning', 'hvac', 'legal', ...]
const FORMATS = ['pdf', 'google-docs', 'excel', 'word']
const LOCALES = ['us', 'uk', 'ca', 'au']
export function generateTemplatePages(): TemplateConfig[] {
const pages: TemplateConfig[] = []
for (const industry of INDUSTRIES) {
for (const format of FORMATS) {
for (const locale of LOCALES) {
// Skip invalid combinations (e.g., Google Docs doesn't need a locale)
if (format === 'google-docs' && locale !== 'us') continue
pages.push({
industry,
format,
features: getFeaturesForIndustry(industry),
locale,
})
}
}
}
return pages
}
export function generatePageMeta(config: TemplateConfig): PageMeta {
return {
title: generateTitle(config),
description: generateDescription(config),
keywords: generateKeywords(config),
h1: generateH1(config),
}
}
This generates ~500 unique template pages from ~40 lines of configuration.
We use Next.js App Router's dynamic routes to serve all generated pages:
// src/app/invoice-templates/[industry]/[format]/page.tsx
interface PageProps {
params: Promise<{
industry: string
format: string
}>
}
export async function generateStaticParams() {
const templates = generateTemplatePages()
return templates.map((t) => ({
industry: t.industry,
format: t.format,
}))
}
export async function generateMetadata({ params }: PageProps) {
const { industry, format } = await params
const config = findConfig(industry, format)
const meta = generatePageMeta(config)
return {
title: meta.title,
description: meta.description,
keywords: meta.keywords,
openGraph: {
title: meta.title,
description: meta.description,
type: 'website',
},
}
}
export default async function TemplatePage({ params }: PageProps) {
const { industry, format } = await params
const config = findConfig(industry, format)
return (
<main className="container mx-auto py-12">
<h1 className="text-4xl font-bold mb-4">{generateH1(config)}</h1>
<TemplateDownloadSection config={config} />
<AIEnhancementSection config={config} />
<RelatedTemplatesSection config={config} />
</main>
)
}
generateStaticParams pre-renders all 500+ pages at build time. The result is a static site with the performance of a hand-coded static site but the coverage of a content farm.
We use next-sitemap with dynamic configuration to automatically include all generated pages:
// next-sitemap.config.js
module.exports = {
siteUrl: 'https://www.eonebill.ai',
generateRobotsots: true,
robotsTxtOptions: {
policies: [
{ userAgent: '*', allow: '/invoice-templates/' },
{ userAgent: '*', disallow: '/api/' },
],
},
extraPaths: async () => {
const templates = generateTemplatePages()
return templates.map((t) =>
`/invoice-templates/${t.industry}/${t.format}`
)
},
}
The sitemap updates automatically on every deployment — no manual sitemap management.
Early versions of our generator produced duplicate titles across similar industries. Google's manual actions team (yes, they still exist) noticed, and our traffic dipped 12% for two weeks.
Fix: Every generated title must pass a uniqueness check:
function assertUniqueTitle(title: string, existing: Set<string>): void {
if (existing.has(title)) {
throw new Error(`Duplicate title: "${title}"`)
}
existing.add(title)
}
Programmatic doesn't mean lazy. Each generated page needs at minimum:
We initially generated pages with just a title and a download button. Those pages ranked briefly, then got hammered by Google's Helpful Content Update.
Our first version had zero internal linking between generated pages. Google saw them as orphan pages — low authority, low ranking.
We added a RelatedTemplatesSection component that links to:
This created a natural link graph that boosted page authority across the entire template library.
The investment in building a proper pSEO engine paid off in under 6 months.
pSEO is not a silver bullet. It works when:
It fails when:
The core generator logic is under 200 lines of TypeScript. The rest is Next.js patterns you're probably already using.
If you're building a template-based product — invoice generators, contract builders, proposal tools — programmatic SEO is worth evaluating seriously. The ceiling is high, and the floor isn't as low as people think.
Ready to see it in action? Generate your first AI-enhanced invoice template at https://www.eonebill.ai.
Originally published at Eonebill — AI-powered invoice generator for freelancers and small businesses.
Ready to automate your invoicing? Try Eonebill free — no credit card required.
Start Free →Join the community
Subscribe to our newsletter for the latest news and updates