DEV Community

Moein Hosseini
Moein Hosseini

Posted on

πŸš€ Building a Dynamic Page Builder with Strapi, Next.js, and Varnish

One of the biggest challenges when building modern websites is giving content editors the flexibility to create new pages without asking developers to deploy code every time.

For one of my recent projects, I designed a page builder architecture using Strapi, Next.js, and Varnish Cache that allows editors to build complete pages by combining reusable components inside the CMS.

The result is a scalable architecture where:

  • Developers build UI components once.
  • Editors create unlimited pages without code.
  • Pages remain fast thanks to HTTP caching.

In this article, I'll walk through the architecture and implementation.


Tech Stack

  • Strapi – Headless CMS
  • Next.js – Frontend Framework
  • Varnish – HTTP Cache
  • REST API – Communication between CMS and Frontend

Overall Architecture

                    Content Editors
                          β”‚
                          β–Ό
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚     Strapi      β”‚
                 β”‚   Headless CMS  β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                     REST API
                          β”‚
                          β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚      Next.js         β”‚
              β”‚  Component Factory   β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                  React Components
                          β”‚
                          β–Ό
                    Rendered HTML
                          β”‚
                          β–Ό
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚     Varnish     β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
                       Browser
Enter fullscreen mode Exit fullscreen mode

Step 1 β€” Creating Pages in Strapi

Instead of creating a different collection type for every page (Home, About, Services, Contact, etc.), I created a single Pages collection.

Each entry represents one page.

Example:

Pages

Home
About
Contact
Pricing
Services
Blog
Landing Page
Enter fullscreen mode Exit fullscreen mode

Each page contains:

  • Title
  • Slug
  • SEO fields
  • Dynamic Zone (Components)

The Dynamic Zone is where the magic happens.

Editors simply choose which components they want to display.


Step 2 β€” Creating Reusable Components

Instead of storing page content as one large HTML field, every section is represented by a reusable Strapi Component.

Examples:

Hero

Title
Subtitle
Image
Buttons
Enter fullscreen mode Exit fullscreen mode
Features

Heading
Cards
Icons
Enter fullscreen mode Exit fullscreen mode
Testimonials

Customer Name
Photo
Quote
Enter fullscreen mode Exit fullscreen mode
FAQ

Question
Answer
Enter fullscreen mode Exit fullscreen mode

The homepage might look like this:

Hero

↓

Feature Cards

↓

Statistics

↓

Testimonials

↓

FAQ

↓

Call To Action
Enter fullscreen mode Exit fullscreen mode

While another page could be:

Hero

↓

Timeline

↓

Team

↓

Gallery

↓

Contact Form
Enter fullscreen mode Exit fullscreen mode

No new code is required.

Editors simply drag and reorder components.


Step 3 β€” Fetching Pages

When a user opens

/about
Enter fullscreen mode Exit fullscreen mode

Next.js requests the page from Strapi.

GET /api/pages?filters[slug][$eq]=about&populate=deep
Enter fullscreen mode Exit fullscreen mode

The API returns something like:

{
  "title": "About",
  "slug": "about",
  "components": [
    {
      "__component": "sections.hero",
      "title": "About Us"
    },
    {
      "__component": "sections.team"
    },
    {
      "__component": "sections.faq"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The important property is:

__component
Enter fullscreen mode Exit fullscreen mode

It tells Next.js which React component should be rendered.


Step 4 β€” Building a Component Factory

This is the heart of the application.

Instead of creating page-specific rendering logic, we map Strapi components to React components.

import Hero from "@/components/Hero";
import FAQ from "@/components/FAQ";
import Team from "@/components/Team";
import Features from "@/components/Features";

const componentFactory = {
  "sections.hero": Hero,
  "sections.faq": FAQ,
  "sections.team": Team,
  "sections.features": Features,
};

export default componentFactory;
Enter fullscreen mode Exit fullscreen mode

Now rendering becomes very simple.

import componentFactory from "./componentFactory";

export default function Page({ components }) {
  return (
    <>
      {components.map((component, index) => {
        const Component =
          componentFactory[component.__component];

        if (!Component) return null;

        return (
          <Component
            key={index}
            {...component}
          />
        );
      })}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The frontend doesn't need to know whether it's rendering a Home page, an About page, or a Landing page.

It simply renders whatever Strapi returns.


Adding a New Component

Suppose the marketing team wants a new Pricing Table.

The process is simple.

In Strapi

Create a component:

Pricing Table
Enter fullscreen mode Exit fullscreen mode

Fields:

  • Title
  • Plans
  • Prices
  • Features

In Next.js

Create

PricingTable.tsx
Enter fullscreen mode Exit fullscreen mode

Then register it.

componentFactory["sections.pricing-table"] =
  PricingTable;
Enter fullscreen mode Exit fullscreen mode

That's it.

Editors can immediately use it on every page.

No routing changes.

No page modifications.

No duplicated code.


Why Use the Factory Pattern?

Without the factory, your application quickly becomes difficult to maintain.

You end up writing code like:

if (page.slug === "home") {
   ...
}

if (page.slug === "about") {
   ...
}

if (page.slug === "pricing") {
   ...
}
Enter fullscreen mode Exit fullscreen mode

As the number of pages grows, this becomes harder to maintain.

Instead, the factory delegates rendering to the correct component automatically.

Benefits include:

  • Cleaner code
  • Better separation of concerns
  • Easier testing
  • Better scalability
  • Less duplication

Performance with Varnish

Dynamic websites often generate many API requests.

To improve performance, I placed Varnish in front of the application.

Browser

↓

Varnish

↓

Next.js

↓

Strapi
Enter fullscreen mode Exit fullscreen mode

If a page already exists in cache, Varnish immediately serves it.

Next.js and Strapi aren't even contacted.

Benefits:

  • Lower server load
  • Faster response time
  • Better Time To First Byte (TTFB)
  • Higher scalability
  • Reduced API requests

When editors publish new content, the cache is invalidated so users always receive fresh content.


Project Structure

My Next.js project looks something like this.

src/

β”œβ”€β”€ app
β”œβ”€β”€ components
β”‚   β”œβ”€β”€ Hero
β”‚   β”œβ”€β”€ FAQ
β”‚   β”œβ”€β”€ Team
β”‚   β”œβ”€β”€ Features
β”‚   └── PricingTable
β”‚
β”œβ”€β”€ factory
β”‚   └── componentFactory.ts
β”‚
β”œβ”€β”€ services
β”‚   └── strapi.ts
β”‚
└── types
Enter fullscreen mode Exit fullscreen mode

This keeps everything organized as the number of components grows.


Advantages of This Architecture

βœ… Editors can build pages without developers.

βœ… Components are reusable.

βœ… No hardcoded layouts.

βœ… Easy to add new sections.

βœ… Clean frontend architecture.

βœ… Better SEO with Next.js.

βœ… Excellent performance using Varnish.

βœ… Scales well for enterprise applications.


Final Thoughts

Using Strapi Dynamic Zones together with a Component Factory in Next.js has been one of the most maintainable approaches I've used for CMS-driven websites.

Developers focus on creating reusable UI components, while content editors assemble those components into rich pages directly from the CMS. The frontend remains clean because it simply renders whatever Strapi returns, and Varnish ensures that dynamic content is delivered with excellent performance.

As the project grows, adding new page sections becomes a predictable process: create a Strapi component, build the corresponding React component, register it in the factory, and it's immediately available for editors to use. This separation of content, presentation, and caching makes the architecture flexible, scalable, and easy to maintain.

If you're building a content-heavy website with Next.js, I highly recommend considering this pattern. It has helped our team reduce development time, empower content editors, and deliver a fast user experience without sacrificing maintainability.

Happy coding! πŸš€

Top comments (0)