Skip to content

Static Generation at Scale: Why FH Builds Ship 800+ Pages Without a Headless CMS

Headless CMS is the wrong answer for most marketing sites. Static generation from TypeScript data is faster, cheaper, and easier to maintain.

John Cravey with EleviFounder4 min read

Every FH client site has 50–800 generated pages — location pages, service pages, blog posts, neighborhood pages, programmatic landing pages. None of them come from a headless CMS. They come from TypeScript data files compiled at build time. The result: zero database calls at request time, zero CMS subscription fees, full version control over content, and pages that serve in under 80ms from the CDN.

Why most SMB sites don’t need a CMS

The headless CMS pitch is editorial autonomy. The reality for SMBs: editorial autonomy is the engineering team adding content via a different UI. Most FH clients don’t have a non-technical content team. The owner sends us a doc, we paste the content into TypeScript, we deploy. Total time: 20 minutes. A CMS adds infrastructure, monthly cost, schema migration friction, and a permanent dependency we don’t need.

When a CMS does make sense: a client publishing weekly content with a non-technical team, or a content surface that needs frequent updates from multiple authors. For everything else, TypeScript data files win.

The pattern: typed data + generateStaticParams

A single TypeScript file exports an array of typed content. The route uses `generateStaticParams` to read that array and tell Next to pre-render one page per entry at build time. Each page reads its content by slug at build time and serves the resulting static HTML.

// lib/locations.ts
export interface Location {
  slug: string;
  city: string;
  state: string;
  neighborhoods: string[];
  serviceBlurb: string;
}

export const LOCATIONS: Location[] = [
  { slug: "dallas", city: "Dallas", state: "TX", neighborhoods: ["Highland Park", "Lakewood"], serviceBlurb: "…" },
  { slug: "fort-worth", city: "Fort Worth", state: "TX", neighborhoods: ["TCU", "Sundance Square"], serviceBlurb: "…" },
  // … 50+ entries …
];
// app/locations/[slug]/page.tsx
import { LOCATIONS } from "@/lib/locations";

export function generateStaticParams() {
  return LOCATIONS.map((l) => ({ slug: l.slug }));
}

export default async function LocationPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const location = LOCATIONS.find((l) => l.slug === slug);
  if (!location) return notFound();
  return <main>{/* … */}</main>;
}

Why this scales further than people expect

Next builds pages in parallel. 200 location pages, 50 service pages, 100 blog posts — the build still finishes in under 2 minutes on a 4GB VPS. Pre-rendered HTML is cached by the CDN and never re-rendered until the next deploy. The runtime cost per page is zero.

How to keep TypeScript-as-CMS sane

  • Type the content interface tightly. Optional fields are temptations — keep them rare.
  • Split content by topic into separate files, merged via a barrel module (we do this on FH’s blog).
  • Use slug-derived helpers (slugify, related, postsByTag) in the barrel so consumers don’t reinvent them.
  • Run `tsc` and `eslint` in CI — TypeScript catches the structural errors a CMS schema would catch later.
  • Lint for typos: a quick regex check on common words in content, or a copy-edit pass before deploy.

What about previewing changes?

Two patterns. For small edits, deploy preview environments on every PR — Coolify supports this, Vercel does it by default. The reviewer sees the change before it lands. For larger content drops, we sometimes build a `/admin/preview/[slug]` route gated by an auth cookie that renders the page from a draft branch. Either way, we don’t need a CMS preview surface.

Bulk-generating content: AI-assisted but human-edited

When a client needs 50 location pages in a sprint, we don’t hand-write each one. We use the Anthropic API to draft each page from a template and the location’s data, then a human reviewer edits each draft for voice, accuracy, and local detail. The draft cuts the writing time per page from 90 minutes to 15. The human review keeps the content from sounding generic.

Updating content without redeploying

If content changes are frequent, pair static generation with ISR. Set `revalidate = 3600` on the route, write content updates to a JSON file in storage, and have the page read from storage at revalidate time. We rarely need this for marketing pages but it’s the bridge if a client outgrows the pure-static pattern.

SEO benefits over CMS-backed sites

Pre-rendered HTML serves to crawlers without JavaScript execution. Googlebot indexes the full content immediately. Search Console picks up new pages within a day of deploy. CMS-backed sites that render content client-side often have indexing delays — we’ve audited two prospective clients whose CMS was the reason 40% of their pages were unindexed.

When CMSs do make sense

  • Multi-author teams publishing 5+ times per week (rare in SMB).
  • Content surfaces where non-technical reviewers approve drafts (also rare).
  • Localized content with complex translation workflows (some retail clients).
  • Anything where the content team is bigger than the engineering team.

If those apply, we route clients to Payload CMS or Sanity rather than rebuilding a CMS in-house. Both work cleanly with Next.js and both can still pre-render statically. The TypeScript-as-CMS pattern just covers more cases than people assume.

Pulling it together

If your SMB site is on a CMS you don’t need, you’re paying a tax for editorial autonomy you’re not using. Migrating to a static-generated Next.js site cuts your hosting bill, your page-load time, and your CMS subscription. Schedule a consultation — we’ll audit whether the CMS is earning its keep and quote the migration if it isn’t.

Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
Indexing Diagnostics: Why Your Pages Aren’t in Google (and How to Fix It)
Older post
Supabase Performance: Indexing, Connection Pooling, and the Postgres Settings That Matter
Keep reading

More from the blog

Next.js·6 min

Next.js 16.1 in Production: The Migration Playbook We Run on Every FH Site

Next 16.1 is the lean target. Here’s the exact migration we run, what breaks, and what to delete after.

AI·5 min

AI-Assisted Content: How to Use Claude for Drafts Without Sounding Like Every Other AI Site

AI drafts are 60% of the work and 0% of the voice. Here’s how to use the time savings without losing the brand.

Search Console·5 min

Sitemaps for Next.js Sites: The Pattern That Keeps Google Indexed

Sitemaps aren’t optional. Here’s the pattern that ships with every FH client build.