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
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
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
Features
Heading
Cards
Icons
Testimonials
Customer Name
Photo
Quote
FAQ
Question
Answer
The homepage might look like this:
Hero
β
Feature Cards
β
Statistics
β
Testimonials
β
FAQ
β
Call To Action
While another page could be:
Hero
β
Timeline
β
Team
β
Gallery
β
Contact Form
No new code is required.
Editors simply drag and reorder components.
Step 3 β Fetching Pages
When a user opens
/about
Next.js requests the page from Strapi.
GET /api/pages?filters[slug][$eq]=about&populate=deep
The API returns something like:
{
"title": "About",
"slug": "about",
"components": [
{
"__component": "sections.hero",
"title": "About Us"
},
{
"__component": "sections.team"
},
{
"__component": "sections.faq"
}
]
}
The important property is:
__component
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;
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}
/>
);
})}
</>
);
}
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
Fields:
- Title
- Plans
- Prices
- Features
In Next.js
Create
PricingTable.tsx
Then register it.
componentFactory["sections.pricing-table"] =
PricingTable;
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") {
...
}
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
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
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)