How to Build JAMstack Sites with Next.js? Code Sample Included

Steps to build Next.js JAMstack sites: 1: Setup a new project; 2: Organize pages & implement SSG/ISR; 3: Integrate a headless CMS; 4: Optimize performance; 5: Deploy.

How to Build JAMstack Sites with Next.js

Modern web development needs speed, scalability, and security. JAMstack meets these needs by decoupling the content layer from the presentation layer, so that pre-rendered pages to be served via CDNs while dynamic content is fetched through APIs.

With the recent updates to Next.js, which now runs seamlessly with React 19, we can build lightweight yet powerful web applications that load fast and are easy to maintain. In this guide, I'll take advantage of these benefits to build a JAMstack site with Next.js.

>> Explore more: Using Next.js with TypeScript to Upgrade Your React Development

Starting Your Project from Scratch

Begin by creating a fresh Next.js project that supports both file‑based routing and modern rendering strategies such as Static Site Generation (SSG) and Incremental Static Regeneration (ISR).

Run the following commands:

typescript
# Create a new project using the latest create-next-app
npx create-next-app my-jamstack-site
cd my-jamstack-site
# Optionally, if you want to experience the blazing-fast development mode:
npm run dev -- --turbo

This will launch a local development server. You can verify the setup by visiting http://localhost:3000, where you should see your default landing page.

Organizing Routes and Pages

Next.js uses a file‑based routing system. You can choose between the traditional /pages directory or adopt the new /app folder for enhanced features like nested layouts. Here’s a unique example using the /pages directory for a homepage:

typescript
// pages/index.tsx
import Link from 'next/link';
export default function HomePage() {
  return (
    <main style={{ padding: "2rem", fontFamily: "sans-serif" }}>
      <h1>Welcome to the JAMstack Revolution</h1>
      <p>This page is pre-rendered at build time for maximum speed.</p>
      <Link href="/about">
        <a>Learn more about JAMstack</a>
      </Link>
    </main>
  );
}

For dynamic routes, you can generate pages using SSG with revalidation. For example, create a product detail page:

typescript
// pages/products/[id].tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
  const response = await fetch(`https://api.example.com/products/${params.id}`, { cache: 'force-cache' });
  const product = await response.json();
  return (
    <article style={{ padding: "2rem" }}>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </article>
  );
}
export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
  const paths = products.map((p: any) => ({ params: { id: p.id.toString() } }));
  return { paths, fallback: 'blocking' };
}
export async function getStaticProps({ params }: { params: { id: string } }) {
  const response = await fetch(`https://api.example.com/products/${params.id}`, { cache: 'force-cache' });
  const product = await response.json();
  return {
    props: { product },
    revalidate: 60,
  };
}

Integrating with a Headless CMS

To keep content management separate from code, many projects integrate a headless CMS. In this example, we’ll use Sanity to demonstrate a fresh approach to headless integration.

Setting Up Sanity

  • Install the Sanity CLI (if you haven’t already):
typescript
npm install -g @sanity/cli
  • Create a new Sanity project:
typescript
sanity init --coupon nextjs2025
  • Configure your project and set up your content schema via the Sanity Studio.

Connecting Sanity to Your Next.js App

In your Next.js project, install the Sanity client:

typescript
npm install @sanity/client @portabletext/react

Then, create a helper file for your Sanity configuration:

typescript
// lib/sanityClient.ts
import sanityClient from '@sanity/client';
export const client = sanityClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || 'yourProjectId',
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
  apiVersion: '2025-02-24', // use current date for latest API version
  useCdn: true,
});

Use this client in your pages to fetch and render data. For instance, a blog listing page might look like this:

typescript
// pages/blog.tsx
import { client } from '../lib/sanityClient';
export default function Blog({ posts }: { posts: any[] }) {
  return (
    <section style={{ padding: "2rem" }}>
      <h1>Our Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post._id}>
            <a href={`/blog/${post.slug.current}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </section>
  );
}
export async function getStaticProps() {
  const posts = await client.fetch(`*[_type == "post"]{ _id, title, slug }`);
  return {
    props: { posts },
    revalidate: 60,
  };
}

Build Optimization & Dynamic Content Handling

Modern sites need to balance static content with dynamic interactivity. With the latest improvements, non-interactive components can be rendered on the server, reducing the client-side bundle size. Consider this example of a non-interactive pricing table:

typescript
// pages/pricing.tsx
export default async function Pricing() {
  const response = await fetch('https://api.example.com/pricing', { cache: 'force-cache' });
  const pricing = await response.json();
  return (
    <div style={{ padding: "2rem" }}>
      <h1>Pricing</h1>
      <table>
        <thead>
          <tr>
            <th>Plan</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>
          {pricing.map((plan: any) => (
            <tr key={plan.id}>
              <td>{plan.name}</td>
              <td>{plan.price}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

By pre-rendering static elements and using client components only where necessary, you ensure that pages load quickly and efficiently.

Deployment Strategies

Hosting Options

For production deployments, platforms such as Vercel and Netlify are the go-to choices. They offer native support for incremental static regeneration and edge caching, ensuring your site remains fast worldwide.

Vercel Example

  • Push your code to GitHub.
  • Import your repository in Vercel.
  • Deploy with automatic configurations: Vercel detects your Next.js setup and applies optimized build and cache settings.

Performance Tuning

  • Image Optimization: Use the built-in image component to automatically resize and serve images in modern formats.
  • Caching & Revalidation: Leverage explicit caching options in your fetch calls and set up ISR to balance fresh content with performance.
  • Monitoring: Integrate observability tools (like Vercel Analytics or Lighthouse) to keep track of performance metrics and user experience.

Conclusion

In this guide, we explored the power of JAMstack using the latest Next.js framework. By setting up a modern project, integrating a headless CMS like Sanity, optimizing for both static and dynamic content, and deploying on leading platforms, you can build fast, scalable, and secure web applications.

By following these guidelines and leveraging the latest improvements, you’ll be well on your way to creating a state-of-the-art JAMstack site that delights users and simplifies your development process.

Additional Resources & Checklist

>>> Follow and Contact Relia Software for more information!

  • coding
  • web development