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 manage invoices, contracts & proposals in one place? Try Eonebill free — no credit card required.
Start Free →Missed the April deadline? Understand the 5 percent failure-to-file vs 0.5 percent failure-to-pay penalty difference, how Form 4868 extensions help, and the four-step protocol for catching up on years of unfiled returns.
Failing to file 1099 forms on time triggers escalating IRS penalties from 60 to 660 dollars per form. Learn the four-tier schedule, how to fix a missed filing, and when First-Time Penalty Abatement applies.
Learn how to write a professional gentle reminder email that gets results. Includes 8 ready-to-use templates for invoices, meetings, deadlines, and more.
Join the community
Subscribe to our newsletter for the latest news and updates