Step 5 of 10 (50% complete)

The Content Type Registry — Your DI Container

Step Code

The code for this specific step can be found on the following branch:

Click on a link to view the code for this step on GitHub.

One of the most deliberate architectural decisions in this starter is where and how content types and React components are registered. Let's look at the file that makes it all work.

The lib/optimizely/init.ts File

This is the single entry point for all registrations. It calls three SDK functions:

import { initContentTypeRegistry, initDisplayTemplateRegistry } from '@optimizely/cms-sdk'
import { initReactComponentRegistry } from '@optimizely/cms-sdk/react/server'

// 1. Register schemas — tells the SDK what content types exist
initContentTypeRegistry([
  HeroBlockContentType,
  CMSPageContentType,
  FooterContentType,
  // ... all other types
])

// 2. Register React components — maps content type key to component
initReactComponentRegistry({
  resolver: {
    HeroBlock,
    CMSPage,
    Footer,
    // ... all other components
  },
})

// 3. Register display templates — optional variants of a component
initDisplayTemplateRegistry([ProfileBlockDisplayTemplate])

And in app/layout.tsx, there is one import:

import '@/lib/optimizely/init'

That is it. This import runs once on every request and ensures the SDK registry is populated before any page renders.

Why a Separate File?

You might wonder why this does not live directly in app/layout.tsx. The answer is about separation of concerns.

If you placed all these registrations directly in app/layout.tsx, the root layout would import every single component, every content type, and every display template in the application. That component would become an enormous configuration file mixed with JSX — hard to read, hard to maintain, and polluted with concerns that have nothing to do with the layout itself.

By keeping registrations in a dedicated init.ts file, the root layout stays clean:

// app/layout.tsx — clean and focused
import '@/lib/optimizely/init'  // one line, all registrations handled
import './globals.css'

export default function RootLayout({ children }) {
  return <>{children}</>
}

The .NET Analogy

If you come from a .NET background, this pattern will feel immediately familiar. Think of it like this:

  • lib/optimizely/init.ts is your AddServices call in Startup.cs — this is where you register everything into the container
  • app/layout.tsx is your application entry point — it calls AddServices once at startup, then delegates to the framework
  • OptimizelyComponent is your dependency injection container — when you ask it to render content, it resolves the correct component from the registry

Just as you would never put builder.Services.AddScoped<IMyService, MyService>() directly in a controller, you do not put content type registrations in your layout component.

The Rule

Every time you add a new content type, there are exactly two places you must update:

  1. initContentTypeRegistry([..., MyNewContentType]) — register the schema
  2. initReactComponentRegistry({ resolver: { ..., MyNewComponent } }) — register the component

If you add a content type but forget to register the React component, OptimizelyComponent will silently render nothing. If you register the component but forget the content type, the CLI will not push it to CMS. Both registrations are required.

Display Templates

Display templates are an optional but useful feature for creating visual variants of the same content type. For example, the ProfileBlock in this starter has a display template that renders the same content with a different colour scheme:

initiDisplayTemplateRegistry([ProfileBlockDisplayTemplate])

Editors can then choose which display template to use directly in the CMS without needing different content types for what is essentially the same content.

Have questions? I'm here to help!

Contact Me