Building a Programmatic SEO Engine: How We Generated 500+ Template Pages
04/05/2026
Building a Programmatic SEO Engine: How We Generated 500+ Template Pages

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.

What Is Programmatic SEO?

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:

  • Traditional SEO: Write one excellent article, target one keyword cluster
  • pSEO: Write one template, generate hundreds of pages, target hundreds of keywords

The strategy works when:

  1. Keywords share a predictable structure (e.g., [industry] invoice template)
  2. Search intent is consistent across pages
  3. You can auto-generate content that's genuinely useful, not thin

Invoice templates check all three boxes.

The Architecture

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                   │
└─────────────────────────────────────────────────────────┘

1. Template Definitions

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.

2. The Data Generator

Here's where it gets interesting. We don't hardcode 500 page routes. Instead, we generate them from a combination of:

  • Industry list: 40+ industries (photography, consulting, cleaning, HVAC, legal, etc.)
  • Format options: PDF, Google Docs, Excel, Word
  • Features: Free/Paid, With/Without Logo, Simple/Detailed
  • Locale: US, UK, CA, AU

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.

3. Dynamic Route Generation in Next.js

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.

4. Automated Sitemap & Metadata

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.

Hard Lessons Learned

Lesson 1: Title uniqueness matters more than you think

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)
}

Lesson 2: Thin content will kill you

Programmatic doesn't mean lazy. Each generated page needs at minimum:

  • A unique H1 (industry + template type)
  • 200+ words of specific content about that industry
  • Industry-specific fields and line items
  • A clear CTA

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.

Lesson 3: Internal linking can't be automated away

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:

  • Same industry, different format
  • Same format, different industry
  • Trending templates in the category

This created a natural link graph that boosted page authority across the entire template library.

Results After 6 Months

  • 523 invoice template pages generated and indexed
  • 31% of organic search traffic comes from template-related keywords
  • Average ranking position for target keywords: 8.3 → 3.1
  • Template page conversion rate: 4.2% (vs. 1.8% for static pages)

The investment in building a proper pSEO engine paid off in under 6 months.

Is pSEO Right for You?

pSEO is not a silver bullet. It works when:

  • ✅ Your product naturally maps to many specific keywords
  • ✅ You can auto-generate genuinely useful content (not just keyword-stuffed shells)
  • ✅ You have the engineering resources to build it correctly
  • ✅ You're willing to invest in ongoing quality control

It fails when:

  • ❌ Keywords don't map naturally to pages (one-offs, long-tail without pattern)
  • ❌ You try to game rankings with low-quality auto-generated content
  • ❌ You don't have engineering resources to maintain the system

The Code

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 →

Ready to automate your invoicing? Try Eonebill free — no credit card required.

Start Free →

Newsletter

Join the community

Subscribe to our newsletter for the latest news and updates