Headless CMS Content Modeling: Reusable Blocks for Landing Pages and Marketing Sites

How to model reusable page sections (hero, features, FAQ, CTA) as blocks in a headless CMS. Practical field schemas, a component-map rendering pattern for Next.js, and tips to keep editors productive and design consistent.

R
Raşit Apalak
6 min read

Reusable sections—hero, features, testimonials, FAQs, CTAs—are how modern marketing sites scale without duplicating work. The trick is to model these page sections as blocks in your headless CMS so editors can assemble pages, while your frontend renders a consistent design system.

This guide shows a clean, scalable approach you can use in ElmapiCMS (or any headless CMS) and render in frameworks like Next.js, Nuxt, or Astro.


Table of Contents


What “blocks” are (and when to use them)

Blocks are structured, reusable page sections that can be ordered and repeated on a page. Examples:

  • Hero
  • Features grid
  • Pricing table
  • Testimonials
  • FAQ accordion
  • Call-to-action (CTA)

Use blocks when:

  • You have many pages with similar patterns (landing pages, solution pages).
  • You want non-developers to assemble pages from approved building blocks.
  • You want to reuse sections across pages without copy/paste drift.

Avoid blocks for:

  • Long-form editorial posts (use rich text/MDX).
  • Highly bespoke pages where the cost of modeling outweighs the benefit.

Two core approaches: flexible vs constrained

There are two viable ways to model sections:

1) Flexible blocks (best for marketing teams)

  • A Page contains an ordered array/list of Sections
  • Each section has a type (hero, faq, cta, etc.)
  • Each section type has its own fields

Pros: maximum flexibility and reuse.
Cons: you must enforce consistency with validation + frontend component mapping.

2) Constrained pages (best for strict design systems)

  • A Page has fixed fields: hero, features, faq, cta
  • You decide what every page must include

Pros: easy rendering, fewer states.
Cons: harder for editors, less composable, more duplication across page types.

Most teams end up with flexible blocks but constrain them with:

  • a limited set of section types
  • per-page allowed section types
  • “required sections” rules in editorial workflow

Here’s a clean mental model (works even if your CMS stores it differently):

  • Page
    • title
    • slug
    • seo (meta title, description, canonical, OG image)
    • sections[] (ordered)

Each item in sections[]:

  • type (string enum): hero, features, testimonials, faq, cta, …
  • id (unique)
  • data (fields for that type)

If your CMS supports relations, you can also model it as:

  • Page ↔ many PageSections
  • PageSection ↔ one SectionType + fields

The goal is the same: you can add, reorder, reuse, and version sections.


How to model common sections

Below are field sets that scale well and keep editors productive.

Hero section

  • headline (string)
  • subheadline (text)
  • primaryCtaLabel (string)
  • primaryCtaHref (string)
  • secondaryCtaLabel (string, optional)
  • secondaryCtaHref (string, optional)
  • image (media, optional)
  • variant (enum: centered, split, minimal)

Features grid

  • title (string, optional)
  • items[]
    • title (string)
    • description (text)
    • icon (string or media)

Testimonials

  • title (string, optional)
  • items[]
    • quote (text)
    • name (string)
    • role (string, optional)
    • company (string, optional)
    • avatar (media, optional)

FAQ

  • title (string, optional)
  • items[]
    • question (string)
    • answer (rich text or text)

CTA

  • headline
  • body (optional)
  • ctaLabel
  • ctaHref
  • variant (enum: primary, secondary, subtle)

Tip: keep section schemas small. Large “do everything” sections become hard to render and harder to maintain.


Editor UX tips

To make blocks usable in practice:

  • Use section previews: show type + headline/title so reordering is safe.
  • Add sensible defaults: prefill labels like “Get started” and empty arrays like items.
  • Limit variants: 2–3 variants per section is plenty.
  • Create “allowed sections per page type” rules: e.g. Pricing pages can use pricing + faq + cta; blog pages can’t.
  • Provide content guidance: placeholder text and short field descriptions (“Keep headline under 60 characters”).

Frontend rendering pattern (component map)

The standard approach is a type-to-component mapping:

  • Fetch a page by slug (and include its sections[]).
  • Loop through sections[].
  • Render the component that matches section.type.

Example pseudo-code:

const SECTION_COMPONENTS = {
  hero: HeroSection,
  features: FeaturesSection,
  testimonials: TestimonialsSection,
  faq: FaqSection,
  cta: CtaSection,
} as const;

return sections.map((section) => {
  const Component = SECTION_COMPONENTS[section.type] ?? UnknownSection;
  return <Component key={section.id} {...section.data} />;
});

Two important implementation details:

  • Forward-compat: add UnknownSection so new section types don’t crash production.
  • Type safety: if you use TypeScript, make section.type a union of known strings and validate server-side.

SEO considerations

Blocks are great for SEO when:

  • You model SEO fields per page (title, description, canonical).
  • You model headings intentionally:
    • Hero headline is usually H1
    • Section titles are H2/H3 depending on structure
  • You avoid rendering empty/placeholder sections.

For structured data:

  • FAQs can output FAQ schema JSON-LD
  • Breadcrumbs can output Breadcrumb schema

(We’ll cover JSON-LD in a dedicated post.)


Pitfalls to avoid

  • One mega-block: a single “content” section with 50 optional fields becomes unmaintainable.
  • Too many section types: editors get decision fatigue; devs get component sprawl.
  • No validation: require key fields for each type (e.g. hero headline, CTA href).
  • Client-only fetching: keep CMS tokens server-side; use server components or API routes.

Related posts:

Share this post:

Related posts