References vs modular blocks vs extensions

Text Lesson8m 15sBeginnerReleased: July 31, 2026

References vs modular blocks vs extensions

TL;DR

  • References link to shared, independently managed entries (authors, categories) -- they require include[] to resolve in the API.
  • Modular blocks enable flexible page-builder layouts with inline data -- no extra API resolution needed.
  • Extensions (custom fields) provide custom editorial UI for specialized data (color pickers, external lookups) at higher build cost.
  • Match the mechanism to the problem: shared content = references, page composition = modular blocks, custom UI = extensions.

Contentstack gives you three distinct mechanisms for composing content: reference fields, modular blocks, and extensions (custom fields). Each solves a different composition problem, and choosing the wrong one forces workarounds that compound over time. This lesson maps each mechanism to the problem it solves, shows how each appears in API responses, and walks through a marketing landing page that uses all three together.

Why this matters

Composition decisions are where developer certification starts to feel practical. The wrong choice here creates messy APIs, editor friction, and unnecessary customization work later.

You will be able to

  • choose between references, modular blocks, and extensions based on the actual problem
  • predict how each option changes the editorial workflow and API response shape
  • apply those tradeoffs to a realistic Veda or campaign-page scenario

The composition problem space

Content composition is not one problem. It is at least three:

  1. Linking to shared content that lives independently and is reused across entries (an author profile, a category, a testimonial).
  2. Assembling flexible page layouts from a set of predefined sections where editors choose order and combination (hero banners, carousels, CTAs, text blocks).
  3. Capturing data that requires custom UI or external sources beyond what native field types provide (color pickers, map coordinates, third-party product lookups).

Reference fields solve problem one. Modular blocks solve problem two. Extensions solve problem three. When teams conflate these problems, they end up with reference fields pointing to single-use entries that should be inline blocks, or modular blocks trying to share content that should be referenced.

Reference fields

A reference field creates a pointer from one entry to one or more entries in another content type. The referenced entry exists independently: it has its own lifecycle, its own URL in the Management API, and its own publish state.

When to use references

Use reference fields when the target content:

  • is reused across multiple entries (an author appears on many articles)
  • has its own editorial lifecycle (a category is created, updated, and deleted independently)
  • benefits from centralized updates (changing an author bio propagates everywhere it is referenced)
  • represents a distinct domain entity in your content model (see lesson 2.1.2 on content types as API contracts)

Common reference patterns include: articles referencing authors, products referencing categories, pages referencing shared banner entries, and any entry linking to related entries of the same type.

Creating a reference field

In the Contentstack UI, navigate to Content Models > [Your Content Type] > Add Field > Reference. Configure the field to allow references to one or more specific content types. You control cardinality: a single-reference field points to one entry, while a multi-reference field allows an array of entries.

API response structure for references

By default, the Delivery API returns only the UID and content type of each referenced entry, not the full entry data:

{
  "entry": {
    "title": "Digital Dawn Landing Page",
    "testimonials": [
      {
        "uid": "blt8a3c9e2f1d4b7a60",
        "_content_type_uid": "testimonial"
      },
      {
        "uid": "blt2f7d4a1c8e3b9065",
        "_content_type_uid": "testimonial"
      }
    ]
  }
}

To get the full referenced entry data inline, you'll want to use the include[] parameter in your query:

GET /v3/content_types/page/entries/{entry_uid}
  ?include[]=testimonials
  &environment=production

The response then nests the complete referenced entries:

{
  "entry": {
    "title": "Digital Dawn Landing Page",
    "testimonials": [
      {
        "uid": "blt8a3c9e2f1d4b7a60",
        "_content_type_uid": "testimonial",
        "title": "Featured in Vogue",
        "quote": "This product transformed our workflow.",
        "company_logo": { "url": "https://images.contentstack.io/..." }
      },
      {
        "uid": "blt2f7d4a1c8e3b9065",
        "_content_type_uid": "testimonial",
        "title": "Editor's Pick - GQ",
        "quote": "Adoption was faster than we expected.",
        "company_logo": { "url": "https://images.contentstack.io/..." }
      }
    ]
  }
}

Common pitfall

Using modular blocks for content that needs independent lifecycle (like author bios) means every parent entry carries its own copy, and updating requires editing every entry individually.

This is an important distinction: references require explicit resolution. If you forget include[], your frontend receives UIDs instead of content. Lesson 3.2.2 covers include[] depth and chaining in detail.

Modular blocks

Modular blocks enable page-builder-style composition. You define a set of block types within a single field, and editors assemble pages by adding, removing, and reordering those blocks. Each block type is a group of fields defined inline within the content type schema.

When to use modular blocks

Use modular blocks when:

  • editors need to compose flexible page layouts from predefined section types
  • the composed sections are not reused independently across other entries
  • the block data is tightly coupled to the parent entry (a hero banner on this specific page, not a shared hero entry)
  • you want to give editors ordering control without creating separate entries for each section

Classic modular block patterns include: landing pages built from hero, feature grid, testimonial strip, CTA, and rich text blocks. The editor picks which blocks to include and arranges them in the desired sequence.

Defining modular blocks

In the Contentstack UI, navigate to Content Models > [Your Content Type] > Add Field > Modular Blocks. Within the modular blocks field, define each block type with its own set of fields. For example, a page_sections modular blocks field might contain:

  • Hero block: heading (single line), subheading (single line), background_image (file), cta_label (single line), cta_url (single line)
  • Carousel block: slides (group, multiple), each with image (file) and caption (single line)
  • CTA block: heading (single line), button_text (single line), button_url (single line), style (select: primary/secondary)
  • Text block: body (JSON RTE)

API response structure for modular blocks

Modular block data is always returned inline with the parent entry. There is no include[] needed and no lazy loading. The full block content ships with every response:

{
  "entry": {
    "title": "Digital Dawn Landing Page",
    "page_sections": [
      {
        "hero": {
          "heading": "Spring into savings",
          "subheading": "Limited time offers across all categories",
          "background_image": { "url": "https://images.contentstack.io/..." },
          "cta_label": "Shop now",
          "cta_url": "/products/digital-dawn"
        }
      },
      {
        "text_block": {
          "body": {
            "type": "doc",
            "children": [...]
          }
        }
      },
      {
        "cta": {
          "heading": "Ready to get started?",
          "button_text": "Contact sales",
          "button_url": "/contact",
          "style": "primary"
        }
      }
    ]
  }
}

Notice the structure: page_sections is an array, and each element is an object with a single key identifying the block type. This means your rendering code switches on the block type key to determine which component to render.

Rendering pattern for modular blocks

A common frontend pattern maps block types to components:

const blockComponents: Record> = {
  hero: HeroSection,
  carousel: CarouselSection,
  cta: CtaSection,
  text_block: TextSection,
};

function PageRenderer({ sections }: { sections: any[] }) {
  return (
    <>
      {sections.map((block, index) => {
        const [blockType] = Object.keys(block);
        const Component = blockComponents[blockType];
        if (!Component) return null;
        return ;
      })}
    
  );
}

This pattern keeps rendering logic decoupled from content structure. When a new block type is added in the content model, you register a new component in the map.

Extensions and custom fields

Extensions (also called custom fields or app-based custom fields) allow you to replace Contentstack's native field UI with a completely custom interface. The custom UI runs inside the entry editor, and the data it produces is stored as JSON within the entry.

When to use extensions

Use extensions when:

  • the data requires a specialized input UI that native fields cannot provide (a color picker with brand palette enforcement, a map coordinate selector, an interactive layout tool)
  • the field needs to interact with a third-party service during editing (a product search against an external PIM, a DAM browser, a translation preview)
  • validation or data transformation rules are complex enough to warrant custom code during the editing experience

Extensions are the most powerful composition mechanism, but also the most expensive to build and maintain. Prefer native fields and modular blocks before reaching for extensions.

How extensions work architecturally

An extension is a small web application (HTML/JS/CSS) hosted externally or within Contentstack's App Framework. It communicates with the entry editor through the Contentstack App SDK (@contentstack/app-sdk). The extension can read and write its field value, access stack metadata, and respond to entry-level events.

The resulting data is stored as a JSON blob within the entry:

{
  "entry": {
    "title": "Digital Dawn Landing Page",
    "color_theme": {
      "primary": "#1a73e8",
      "secondary": "#f4f4f4",
      "accent": "#ff6d00",
      "palette_name": "Spring Vibrance"
    }
  }
}

From the API response perspective, extension data is inline just like modular blocks. The difference is entirely in the editorial UI and the data shape flexibility.

Extension example: color theme picker

A color theme extension might enforce brand palette choices:

// Inside the extension's initialization
import ContentstackAppSDK from "@contentstack/app-sdk";

const sdk = await ContentstackAppSDK.init();
const field = sdk.location.CustomField;

// Read current value
const currentTheme = field?.field?.getData();

// When editor selects a palette
function onPaletteSelect(palette: {
  primary: string;
  secondary: string;
  accent: string;
  palette_name: string;
}) {
  field?.field?.setData(palette);
}

The value written by setData is what appears in the API response. Contentstack stores it as an opaque JSON value attached to the field.

Comparison matrix

DimensionReference fieldsModular blocksExtensions
Data locationSeparate entriesInline in parentInline in parent
Reuse across entriesYes (core purpose)No (page-specific)No (field-specific)
API resolutionRequires include[]Always inlineAlways inline
Editor experienceEntry pickerBlock composerCustom UI
Ordering controlArray orderingDrag-and-drop orderingN/A
Schema definitionSeparate content typeInline block definitionsCustom JSON shape
Build costLow (native)Low (native)High (custom code)
Payload impactControlled via include[]Always present in fullAlways present in full

Worked example: marketing landing page using all three

Consider a marketing landing page content type that combines all three composition mechanisms:

Content type: page (e.g., a campaign landing page)

  • title (single line text)
  • slug (single line text, unique)
  • testimonials (reference field, multi-reference to testimonial content type)
  • page_sections (modular blocks with hero, carousel, cta, and text_block types)
  • color_theme (custom field extension, color palette picker)

The editorial workflow:

  1. Editor creates the landing page entry, sets title and slug.
  2. Editor picks existing testimonial entries from the reference field picker. These testimonials might also appear on the homepage and product pages.
  3. Editor assembles the page layout using modular blocks: adds a hero, two text blocks, and a CTA in the desired order.
  4. Editor opens the color theme extension, selects a brand-approved palette.

The API query:

GET /v3/content_types/page/entries/{entry_uid}
  ?include[]=testimonials
  &environment=production

The response combines all three data shapes:

{
  "entry": {
    "title": "Spring Campaign Landing Page",
    "slug": "spring-campaign",
    "testimonials": [
      {
        "uid": "blt8a3c9e2f1d4b7a60",
        "title": "Featured in Vogue",
        "quote": "This product transformed our workflow."
      }
    ],
    "page_sections": [
      { "hero": { "heading": "Spring into savings", "cta_label": "Shop now" } },
      { "text_block": { "body": { "type": "doc", "children": [] } } },
      { "cta": { "heading": "Ready?", "button_text": "Contact us" } }
    ],
    "color_theme": {
      "primary": "#1a73e8",
      "accent": "#ff6d00",
      "palette_name": "Spring Vibrance"
    }
  }
}

Your rendering code consumes all three: resolves testimonials into a testimonial strip component, iterates page_sections to render the block sequence, and applies color_theme values as CSS custom properties.

Common mistakes

1. Using references for content that is never reused

Creating a separate hero_banner content type and referencing it from a page, when that hero only ever appears on one page, adds unnecessary indirection. The entry has its own lifecycle, publish state, and API resolution cost for no reuse benefit. Use a modular block instead.

2. Using modular blocks for content that needs independent lifecycle

Embedding author data as a modular block within an article means every article carries its own copy of the author information. Updating an author bio requires editing every article. This is the exact problem reference fields solve.

3. Building extensions for problems that native fields handle

A select dropdown with fixed options does not need a custom extension. An image field does not need a custom file picker (unless integrating a specific external DAM). Extensions carry maintenance cost: they are custom code that must be hosted, versioned, and updated. Only build them when native fields genuinely cannot support the data or UX requirement.

Practice in Contentstack

Use one real or hypothetical page in your stack and break it into three buckets:

  1. content that should be reused independently across entries
  2. content that belongs only to one page layout
  3. content that would genuinely need a custom editorial UI

Then map each bucket to references, modular blocks, or extensions and write one sentence explaining why.

Summary

References are for shared entities with their own lifecycle. Modular blocks are for inline page composition. Extensions are for specialized editorial UI that native fields cannot provide. Matching the mechanism to the job keeps both the editor experience and the API contract healthier.