Static Site Generation with JAMstack Architecture
Why performance, security, and developer experience are converging on the JAMstack model right now

The last few years have changed how we ship web experiences. Users expect instant load times on mobile networks, marketing teams want to iterate without a deployment pipeline, and security teams are tired of patching CMS vulnerabilities at 2 a.m. Static site generation, wrapped inside JAMstack architecture, has emerged as a practical answer to all three pressures. It is not a silver bullet, but it is a disciplined approach that rewards teams who value simplicity and measurable outcomes over complexity for its own sake.
In this post, I will walk you through what JAMstack with static site generation actually looks like in practice, where it shines, where it struggles, and how to get started without drowning in tooling choices. I have used this approach for content-heavy sites, documentation portals, and marketing campaigns. The approach has saved me from midnight deploys and cut hosting bills, but it also forced me to confront content freshness, dynamic functionality, and the realities of integrating with legacy systems. Let us build a clear picture of the stack, its tradeoffs, and a realistic path forward.
What JAMstack means in real work
JAMstack is a way to build and deliver websites that decouple the front end from the server and database at runtime. The term was popularized by Netlify’s founder Mathias Biilmann, who described it as “modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup” (see Netlify’s JAMstack definition). The core idea is simple: ship prebuilt assets to a content delivery network and call APIs for dynamic bits when needed. When people talk about static site generation, they are often referring to a subset of JAMstack where markup is generated at build time, but dynamic functionality can still be layered in via client-side JavaScript and APIs.
In contrast to a traditional server-rendered CMS, your site is a collection of HTML, CSS, and JavaScript files. There is no server-side templating at request time. That changes the performance profile, the security model, and the deployment workflow in ways that tend to favor lean teams and predictable scaling.
Where static generation fits today
Static site generation is no longer limited to personal blogs. I have seen it power documentation sites that serve millions of requests per month, marketing pages that need subsecond loads, and even e-commerce storefronts that outsource checkout to a third party. Here is where it shows up most often:
- Documentation and knowledge bases. Tools like Docusaurus and MkDocs are standard for developer docs because they integrate with version control and CI, making updates as easy as a pull request.
- Content-driven marketing sites. Static generators excel at pages that mix structured content with reusable components. SEO and performance budgets are easier to enforce.
- Jammy front ends with dynamic backends. Think e-commerce front ends backed by Shopify or BigCommerce APIs, with product data hydrated at build time and real-time inventory fetched client-side.
- Static export from frameworks. Next.js and Nuxt can export static builds to fit a JAMstack flow while retaining the component model and developer experience of the framework.
Teams using this approach typically include frontend engineers, technical writers, and designers. They value Git-driven workflows, preview environments, and predictable deploys. Compared to a fully server-rendered application, the tradeoff is less dynamic server logic and more reliance on APIs or edge functions for interactive features. Compared to a single-page app hitting a heavy backend at runtime, the tradeoff is more initial performance and less complexity around data fetching during navigation, at the cost of freshness strategies.
Core concepts and practical examples
Let’s ground the concepts with a simple blog that renders Markdown posts into HTML at build time, adds a sitemap, and deploys to a CDN. I will use Node.js and a minimal static generator approach, without heavy frameworks, so you can see the mechanics.
Project setup and folder structure
A clean folder structure keeps the build predictable and the content easy to maintain.
my-static-blog/
├─ content/
│ ├─ posts/
│ │ ├─ first-post.md
│ │ └─ second-post.md
│ └─ data/
│ └─ site.json
├─ src/
│ ├─ templates/
│ │ ├─ base.html
│ │ ├─ post.html
│ │ └─ index.html
│ ├─ assets/
│ │ ├─ css/
│ │ │ └─ main.css
│ │ └─ js/
│ │ └─ app.js
├─ scripts/
│ └─ build.js
├─ dist/
└─ package.json
Build script that turns Markdown into HTML
The core of static generation is a build script that reads content, applies templates, and outputs HTML. The following Node.js script demonstrates a minimal generator with front matter parsing and a sitemap. It uses marked for Markdown rendering and fs for file operations.
// scripts/build.js
const fs = require("fs");
const path = require("path");
const matter = require("gray-matter"); // For parsing front matter in Markdown
const marked = require("marked"); // Markdown to HTML
const dayjs = require("dayjs"); // Date formatting
const contentDir = path.join(__dirname, "../content/posts");
const distDir = path.join(__dirname, "../dist");
const templatesDir = path.join(__dirname, "../src/templates");
// Read template files
const baseTemplate = fs.readFileSync(path.join(templatesDir, "base.html"), "utf8");
const postTemplate = fs.readFileSync(path.join(templatesDir, "post.html"), "utf8");
const indexTemplate = fs.readFileSync(path.join(templatesDir, "index.html"), "utf8");
// Utility to write files safely
function writeFile(filePath, content) {
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filePath, content, "utf8");
}
// Parse a post file and return metadata plus HTML
function parsePost(filePath) {
const raw = fs.readFileSync(filePath, "utf8");
const { data: frontMatter, content } = matter(raw);
const html = marked.parse(content);
const slug = path.basename(filePath, ".md");
return {
slug,
title: frontMatter.title || slug,
date: frontMatter.date || new Date().toISOString(),
description: frontMatter.description || "",
html,
};
}
// Render a full HTML page using the base template
function renderPage({ title, content, description, extraHead }) {
return baseTemplate
.replace("{{title}}", title)
.replace("{{description}}", description || "")
.replace("{{extraHead}}", extraHead || "")
.replace("{{content}}", content);
}
// Render a single post page
function renderPost(post) {
const postContent = postTemplate
.replace("{{title}}", post.title)
.replace("{{date}}", dayjs(post.date).format("YYYY-MM-DD"))
.replace("{{body}}", post.html);
return renderPage({
title: post.title,
description: post.description,
content: postContent,
});
}
// Render the homepage listing posts
function renderIndex(posts) {
const listItems = posts
.sort((a, b) => new Date(b.date) - new Date(a.date))
.map(p => {
const date = dayjs(p.date).format("YYYY-MM-DD");
return `<article>
<h2><a href="/posts/${p.slug}/">${p.title}</a></h2>
<time datetime="${p.date}">${date}</time>
${p.description ? `<p>${p.description}</p>` : ""}
</article>`;
})
.join("\n");
const indexContent = indexTemplate.replace("{{list}}", listItems);
return renderPage({
title: "My Static Blog",
description: "A fast, secure blog generated statically.",
content: indexContent,
extraHead: `<link rel="stylesheet" href="/assets/css/main.css">`
});
}
// Build posts and homepage
function build() {
// Read post files
const files = fs.readdirSync(contentDir).filter(f => f.endsWith(".md"));
const posts = files.map(f => parsePost(path.join(contentDir, f)));
// Write each post page
posts.forEach(post => {
const html = renderPost(post);
writeFile(path.join(distDir, "posts", post.slug, "index.html"), html);
});
// Write homepage
const indexHtml = renderIndex(posts);
writeFile(path.join(distDir, "index.html"), indexHtml);
// Copy assets
const assetsSrc = path.join(__dirname, "../src/assets");
const assetsDest = path.join(distDir, "assets");
if (fs.existsSync(assetsSrc)) {
fs.cpSync(assetsSrc, assetsDest, { recursive: true });
}
// Generate a sitemap
const baseUrl = "https://example.com";
const urls = [
{ loc: `${baseUrl}/`, changefreq: "daily", priority: 1.0 },
...posts.map(p => ({
loc: `${baseUrl}/posts/${p.slug}/`,
lastmod: dayjs(p.date).format("YYYY-MM-DD"),
changefreq: "weekly",
priority: 0.8
}))
];
const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(u => ` <url>
<loc>${u.loc}</loc>
${u.lastmod ? `<lastmod>${u.lastmod}</lastmod>` : ""}
<changefreq>${u.changefreq}</changefreq>
<priority>${u.priority}</priority>
</url>`).join("\n")}
</urlset>`;
writeFile(path.join(distDir, "sitemap.xml"), sitemapXml);
console.log("Build complete:", distDir);
}
build();
Example content file with front matter
Front matter is a YAML block at the top of a Markdown file. It gives you structured metadata without a database.
---
title: "Why I switched to static generation"
date: "2025-03-02"
description: "A look at performance and security tradeoffs when moving from a CMS to JAMstack."
---
Static generation changed how I ship features. I can reason about every request because there are no moving parts on the server side for page loads. Dynamic functionality still exists, but it is intentionally limited and isolated.
The biggest surprise was how much faster my pages felt even without heavy optimization. The CDN does the heavy lifting, and the HTML is lean.
Package configuration
This package.json shows the minimal dependencies for our build script.
{
"name": "my-static-blog",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "node scripts/build.js",
"serve": "npx serve dist"
},
"dependencies": {
"dayjs": "^1.11.10",
"gray-matter": "^4.0.3",
"marked": "^12.0.1"
},
"devDependencies": {
"serve": "^14.2.0"
}
}
This setup is intentionally simple, but it captures the essence of static generation: content in version control, a deterministic build, and static assets ready for a CDN. In a real project, you would add tests, linting, and continuous integration. For instance, a GitHub Actions workflow can run the build and push the dist folder to Netlify or GitHub Pages. You can also integrate an image pipeline to compress and resize images during the build.
Handling dynamic needs without a server
Static sites often need dynamic features like search, comments, or personalization. You can solve these without server-side rendering:
- Search: precompute a search index at build time and query it client-side. Tools like
flexsearchorlunrare common. - Comments: embed a third-party service, or build a serverless function that accepts POST requests and stores comments in a database.
- Forms: use a form handler like Netlify Forms or a serverless function to capture submissions.
- Personalization: fetch user-specific data client-side from an API and hydrate the UI.
Here is an example of precomputing a search index at build time for the blog:
// scripts/search-index.js
const fs = require("fs");
const path = require("path");
const matter = require("gray-matter");
const contentDir = path.join(__dirname, "../content/posts");
const distDir = path.join(__dirname, "../dist");
function buildSearchIndex() {
const files = fs.readdirSync(contentDir).filter(f => f.endsWith(".md"));
const index = files.map(file => {
const raw = fs.readFileSync(path.join(contentDir, file), "utf8");
const { data, content } = matter(raw);
const slug = path.basename(file, ".md");
return {
id: slug,
title: data.title || slug,
description: data.description || "",
// Keep body lightweight for client-side search
body: content.slice(0, 500),
};
});
fs.writeFileSync(
path.join(distDir, "search-index.json"),
JSON.stringify(index, null, 2)
);
}
buildSearchIndex();
On the client, you can load search-index.json and query it using a small library. This keeps the build deterministic and avoids runtime server calls for search.
Strengths, weaknesses, and tradeoffs
Static generation has strong points and blind spots. It is best to evaluate these early so you do not end up fighting the architecture.
Strengths:
- Performance. HTML is prebuilt and delivered from CDN. Time-to-first-byte is low, and caching is straightforward.
- Security. No server-side database or CMS to exploit for most pages. Attack surface shrinks dramatically.
- Scalability. CDN delivery scales with traffic. There is no app server to autoscale.
- Developer experience. Git-driven workflows, preview environments per branch, and automated pipelines make teams faster.
- Cost. Hosting is cheap. You pay for build minutes and CDN traffic rather than always-on compute.
Weaknesses:
- Content freshness. If your content changes often, you need frequent builds or incremental updates. Some platforms support partial builds, but it adds complexity.
- Dynamic features. Real-time data and complex user flows require APIs, edge functions, or third-party services. This can reintroduce state management and latency concerns.
- Build time. Large sites with thousands of pages can have long build times. This affects preview cycles and deploy speed.
- Tool churn. The ecosystem moves fast. Pick stable tools with clear upgrade paths.
- SEO pitfalls. Client-side-only routing can hurt indexing if not handled carefully. Pre-rendering solves most issues, but you need to be mindful.
Tradeoffs to consider:
- Use static generation for content-heavy pages and lean marketing sites. If the site is highly dynamic, use hybrid rendering or server-side rendering for specific routes. For example, Next.js supports per-page rendering strategies, which can be a pragmatic middle ground.
- Use client-side JavaScript sparingly. Each script adds to the main thread and can degrade performance on low-end devices. Prefer small, focused libraries.
- Choose a build tool that fits your team. If you have strong TypeScript skills and love component architecture, a framework like Astro might fit. If you value simplicity and minimal JavaScript, a generator like Eleventy might be better.
Personal experience and common mistakes
I have learned the most about static generation by breaking it. One early mistake was assuming that pre-rendering solves all SEO problems. It does not if you rely entirely on client-side rendering for critical content. I fixed this by ensuring the first meaningful paint is HTML, then adding interactive elements progressively.
Another common mistake is overfetching at build time. I once generated thousands of pages from an API that returned heavy payloads. Builds became slow and unpredictable. I solved it by introducing caching of API responses and building a lightweight incremental pipeline that only rebuilt changed content. This is not trivial, but it is manageable with a good CI strategy.
Static generation proved especially valuable on a documentation portal where the content lived in multiple repositories. By consolidating content during the build and generating a unified search index, we delivered instant search without a backend. The winning moment was seeing support tickets drop because the docs were easier to find and loaded in under a second.
Getting started with a workflow and mental model
If you are new to this approach, think in three phases:
- Content: keep content in version control as Markdown or JSON. Define a front matter schema early and stick to it.
- Build: create a deterministic build pipeline. Cache external data when possible. Generate HTML, sitemaps, and search indexes. Test the output locally.
- Deploy: push the output to a CDN. Set up preview environments per branch. Configure headers, redirects, and caching rules at the CDN level.
A typical folder layout for a real project looks like this:
project/
├─ content/
│ ├─ posts/
│ ├─ pages/
│ └─ data/
├─ src/
│ ├─ components/ # Reusable UI parts if using a framework
│ ├─ layouts/ # HTML wrappers
│ ├─ templates/ # Page templates for generator
│ └─ assets/
├─ scripts/
│ ├─ build.js
│ └─ search-index.js
├─ dist/
├─ netlify.toml # CDN configuration
└─ package.json
For CI, a simple GitHub Actions workflow can look like this:
name: Build and Deploy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: site
path: dist
If you use Netlify, you can connect the repository and set the publish directory to dist. Netlify will build on every commit and create branch previews. For edge functions, you can place serverless logic in a designated folder and invoke it from client-side code without managing servers.
When choosing tools, keep the mental model simple:
- Prefer generators that support incremental builds and partial hydration if your site is large.
- Use edge functions sparingly for dynamic pieces like form handling or A/B testing.
- Treat images as part of the build pipeline. Generate multiple sizes and use responsive markup. The
sharplibrary is a standard choice for Node-based image pipelines.
Free learning resources
- Netlify’s JAMstack definition: a clear primer on the architecture and its benefits. https://jamstack.org/
- Astro documentation: a practical guide to an island architecture approach that pairs well with static generation. https://docs.astro.build/
- Eleventy documentation: a flexible static site generator that favors simplicity and extensibility. https://www.11ty.dev/docs/
- Next.js static export and rendering modes: useful for hybrid approaches. https://nextjs.org/docs/app/building-your-application/rendering/server-components
- MkDocs and Docusaurus: for documentation-focused sites with built-in search and versioning. https://docusaurus.io/ https://www.mkdocs.org/
- Netlify Edge Functions: dynamic capabilities at the edge without managing servers. https://docs.netlify.com/netlify-labs/edge-functions/overview/
Who should use it and who might skip it
Use static generation with JAMstack if:
- Your site is content-heavy or mostly informational, and you want strong performance and security.
- Your team is comfortable with Git-based workflows and wants fast preview environments.
- You can rely on third-party APIs or edge functions for dynamic parts.
- You need predictable scaling without managing servers.
Consider skipping or choosing a hybrid model if:
- Your application is highly dynamic with real-time collaboration or complex state that must be server-rendered for correctness.
- You have large amounts of frequently changing content that require immediate updates without rebuilds.
- Your team prefers a monolithic backend with tightly integrated CMS features and minimal DevOps overhead.
Summary
Static site generation inside a JAMstack architecture delivers speed, security, and simplicity when applied to the right problems. It is not a return to the early web; it is a modern approach that leverages CDNs, APIs, and edge functions to deliver rich experiences without heavy servers. The sweet spot is content-centric projects, documentation, and marketing sites, but it can extend to e-commerce front ends and hybrid apps when combined with careful client-side data fetching.
For teams that value measurable outcomes and developer happiness, the approach is compelling. For teams building deeply dynamic systems, hybrid rendering might be the better path. Start with a small, real project, iterate on the build pipeline, and measure the results. If you see faster deploys, lower costs, and fewer incidents, you will know you are on the right track.
If you want to go deeper, pick one generator, build a tiny blog or documentation site, and add a dynamic feature with an edge function. The constraints will teach you more than any documentation ever could.




