AboutBlogContact
Frontend EngineeringApril 17, 2019 8 min read 142Updated: June 22, 2026

JAMstack in Production: Why We Rebuilt a News Site With Gatsby and a Headless CMS

AunimedaAunimeda
📋 Table of Contents

A client ran a regional news site on WordPress. It worked until a story got picked up by a major aggregator and drove 50,000 concurrent visitors. The server melted. Not because it lacked capacity - they'd already over-provisioned for this. It melted because WordPress's architecture (PHP generating pages from MySQL on every request) couldn't handle the load without caching configured exactly right.

The solution wasn't "add more servers." The solution was removing the server from the equation entirely. That's JAMstack.


What JAMstack Actually Means

JAM = JavaScript, APIs, Markup.

The core insight: if your content doesn't change per-user request, build the HTML once at deploy time and serve it from a CDN. No PHP, no database query, no server-side rendering at runtime. The CDN serves a pre-built HTML file. A CDN node in Frankfurt serves German visitors at 20ms latency regardless of how many concurrent users there are.

This isn't new - it's how the web worked in 1995. JAMstack brings modern tooling to the same fundamental model: fast, secure, cheap to serve, survives traffic spikes.

Where it doesn't work: user-authenticated content, real-time data, shopping carts, anything that's truly dynamic per-user. These become client-side JavaScript calling APIs (the "A" in JAMstack).


The Stack

  • Gatsby 2.x - React-based static site generator. Build process fetches data, renders React components, outputs HTML/CSS/JS files.
  • Contentful - Headless CMS. Content editors use a clean web UI; developers consume content via REST or GraphQL API. No PHP, no database, no WordPress UI.
  • Netlify - CI/CD + CDN. Push to GitHub → Netlify builds Gatsby → deploys to global CDN in ~3 minutes.
  • Netlify Functions - Serverless functions for the dynamic parts (newsletter signup, contact form).

Why "Headless" CMS?

Traditional CMS (WordPress): content storage + content editing UI + templating engine all in one. The "head" (the front-end presentation) is coupled to the backend.

Headless CMS: just the content storage and editing UI. It exposes content via API. Your front-end (Gatsby, Next.js, a native app, anything) consumes the API. The CMS has no opinion about how content is displayed.

This decoupling is the key benefit. Content editors get a modern, purpose-built editing interface. Developers build the front-end with whatever tools are appropriate. You can have multiple front-ends consuming the same CMS (website + mobile app + email newsletter).


Gatsby: How Static Generation Works

Gatsby's build process:

1. Source plugins fetch data (Contentful API, local Markdown files, databases)
2. Gatsby creates a "data layer" - a unified GraphQL schema over all data sources
3. React components query this data layer at build time
4. Gatsby renders every page to static HTML + generates JavaScript for client-side hydration
5. Output: a directory of .html files, .js chunks, images

A page template in Gatsby:

// src/templates/article.js
import React from 'react';
import { graphql } from 'gatsby';

// This GraphQL query runs at BUILD TIME, not on every page request
export const query = graphql`
  query ArticleQuery($slug: String!) {
    contentfulArticle(slug: { eq: $slug }) {
      title
      publishedAt
      author {
        name
      }
      body {
        raw  # Rich text from Contentful
      }
      heroImage {
        file {
          url
        }
      }
    }
  }
`;

export default function ArticlePage({ data }) {
  const article = data.contentfulArticle;
  
  return (
    <article>
      <h1>{article.title}</h1>
      <time>{article.publishedAt}</time>
      <RichTextRenderer content={article.body} />
    </article>
  );
}

Gatsby calls gatsby-node.js during build to programmatically create pages:

// gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions;
  
  const result = await graphql(`
    {
      allContentfulArticle {
        nodes {
          slug
        }
      }
    }
  `);

  result.data.allContentfulArticle.nodes.forEach(article => {
    createPage({
      path: `/news/${article.slug}`,
      component: path.resolve('./src/templates/article.js'),
      context: { slug: article.slug },
    });
  });
};

Result: 1,200 articles → 1,200 static HTML files built once. CDN serves each HTML file. Zero database queries during traffic spikes.


Contentful: The Editing Experience

Contentful's content modeling is type-based. We defined content types for the editorial team:

Article:
  - title (Short text)
  - slug (Short text, unique)
  - body (Rich text)
  - author (Reference → Author content type)
  - publishedAt (Date & time)
  - category (Reference → Category content type)
  - heroImage (Media)
  - seoDescription (Short text, max 160 chars)

Author:
  - name (Short text)
  - bio (Long text)
  - photo (Media)

Editors work in a clean interface - similar to Notion or Medium. They don't touch templates, CSS, or code. A new article published in Contentful triggers a Netlify build webhook, which rebuilds and deploys the site in ~3 minutes.


Build Optimization: 1,200 Pages in 90 Seconds

Initial build times were 12 minutes. Unacceptable for an editorial workflow where "publish" needs to mean "live in under 5 minutes."

Problem 1: Image processing. Gatsby's gatsby-image (now gatsby-plugin-image) processed every image at build time - resizing, converting to WebP, generating srcsets. 800 images × 6 sizes = 4,800 image operations.

Solution: use Contentful's built-in image transformation API instead of local processing:

// Instead of downloading and processing locally:
// https://images.ctfassets.net/..../photo.jpg?w=800&h=600&fit=fill&f=faces&fm=webp
// Contentful transforms on their CDN. Build time: ~0ms per image.

const imageUrl = `${asset.url}?w=${width}&h=${height}&fit=fill&fm=webp&q=80`;

Problem 2: No build cache. Gatsby 2.5 introduced persistent disk cache - unchanged pages skip regeneration. Cache stored in .cache/ between builds. Rebuild after a single article publish: ~45 seconds (only the changed article + homepage regenerated).

Problem 3: GraphQL query complexity. 47 components each running their own GraphQL queries. Refactored to page-level queries with fragments:

// Shared fragment, not a separate query
export const authorFragment = graphql`
  fragment AuthorFields on ContentfulAuthor {
    name
    bio
    photo { file { url } }
  }
`;

Final build stats:

  • Full rebuild (all 1,200 articles): 92 seconds
  • Incremental rebuild (1 changed article): 41 seconds

Netlify: Deploy Previews Changed the Workflow

Every pull request automatically got a deploy preview URL - a full working version of the site with those changes. The editorial team could review template changes at a preview URL before merge. No staging server needed, no "deploy to staging" step.

# netlify.toml
[build]
  command = "gatsby build"
  publish = "public"

[build.environment]
  GATSBY_CONTENTFUL_SPACE_ID = "..."
  NODE_VERSION = "12"

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-XSS-Protection = "1; mode=block"
    Cache-Control = "public, max-age=0, must-revalidate"

[[headers]]
  for = "/static/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

Gatsby outputs content files as page-slug/index.html (no .html extension in URLs) and static assets with content hashes in filenames (app-1a2b3c.js). Hashed assets get 1-year cache headers; HTML files get no cache (always fresh from CDN).


The Dynamic Parts: Netlify Functions

A JAMstack site isn't purely static - it has dynamic behavior via client-side JavaScript calling APIs. For this project:

Newsletter signup: Calls a Netlify Function that proxies to Mailchimp API. The API key stays server-side, not exposed to the browser.

// netlify/functions/newsletter-subscribe.js
const Mailchimp = require('@mailchimp/mailchimp_marketing');

exports.handler = async function(event) {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  const { email } = JSON.parse(event.body);
  
  Mailchimp.setConfig({ apiKey: process.env.MAILCHIMP_API_KEY, server: 'us1' });

  try {
    await Mailchimp.lists.addListMember(process.env.MAILCHIMP_LIST_ID, {
      email_address: email,
      status: 'pending',  // Double opt-in
    });
    return { statusCode: 200, body: JSON.stringify({ success: true }) };
  } catch (err) {
    return { statusCode: 400, body: JSON.stringify({ error: err.message }) };
  }
};

Article search: Client-side search using a pre-built search index. At build time, we generate a JSON file with all articles' titles, slugs, and tags. On the client, Fuse.js does fuzzy search over this JSON. No server, no Elasticsearch, no Algolia for this use case (1,200 articles × ~200 bytes = 240KB index - acceptable).


Before vs After

Metric WordPress Gatsby + Contentful
Time to First Byte 380ms (cached), 1.8s (uncached) 18ms (CDN edge)
10,000 concurrent requests Server failure No impact (CDN)
Hosting cost €45/month (2vCPU VPS + MySQL) €0 (Netlify free tier) + $39/month Contentful
Editor publishing workflow Instant (WordPress) 45 seconds to live
PageSpeed mobile 58 96
Security surface WordPress core, plugins, PHP Netlify CDN (no attack surface)

The cost was higher (Contentful isn't free at scale), but the performance, reliability, and security were transformed.


When JAMstack Is Wrong

We had a follow-up project: an e-commerce site with 80,000 products, real-time inventory, and personalized recommendations. We started with Gatsby. At 80,000 products × build time overhead, full builds took 28 minutes. We abandoned it for Next.js with ISR - which regenerates pages on demand rather than building all pages upfront.

JAMstack's sweet spot in 2019: content sites with hundreds to low thousands of pages that update a few times per day. E-commerce, large catalogs, user-generated content - consider ISR-based frameworks instead.

In 2024, the line has blurred. Next.js App Router's React Server Components effectively do build-time and runtime rendering in the same framework. But the core JAMstack insight - move computation upstream to build time, serve pre-built assets from CDN - remains one of the most impactful architectural decisions for content-heavy sites.


Aunimeda builds modern web frontends - from single-page applications to complex multi-locale sites.

Contact us to discuss your frontend project. See also: Web Development, Corporate Website Development

Read Also

Next.js 13/14: Server Actions and the New App Router (2023)aunimeda
Frontend Engineering

Next.js 13/14: Server Actions and the New App Router (2023)

React is coming full circle. In 2023, Next.js Server Actions are bringing back the simplicity of PHP and Ruby on Rails with modern React components.

React Server Components: Decoding the Wire Format (2023)aunimeda
Frontend Engineering

React Server Components: Decoding the Wire Format (2023)

RSC is finally stable in Next.js 13.4. But what's actually happening in that .rsc stream? Let's decode the secret language of the server-client bridge.

Zero-Runtime CSS-in-JS: The Performance King (2023)aunimeda
Frontend Engineering

Zero-Runtime CSS-in-JS: The Performance King (2023)

Is Styled Components dead? In 2023, zero-runtime CSS-in-JS is taking over. No more runtime script, no more style re-calculation.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Web Development

Get Consultation All articles