Vipin Banka
Jul 21, 2026
visibility 58
star star star star star
(0 votes)

Parallel Development in Optimizely CMS SaaS: A Smarter Way to Register Components

📌 A note before you read: The approach described in this article is not a replacement for Optimizely’s recommended out-of-the-box component registration pattern — which is clean, explicit, and exactly right for most projects. This is written for teams of 4 or more developers working on parallel feature branches who are finding that a shared registry file is becoming a source of repeated, low-value merge conflicts. If that’s not your situation yet, file this away for when it is.


When Optimizely introduced code-first content modeling for CMS SaaS, they solved one of the biggest headaches in modern CMS development. Being able to define your content types in standard .tsx files and push them via the CLI is a massive developer experience win.

But modeling is only half the equation. On the frontend, you must map those content types to actual React components so the SDK can resolve and render them.

The standard approach uses a central registration file — typically in your Next.js layout or a dedicated registry file — where you manually import every single component and content type, registering them with initContentTypeRegistry and initReactComponentRegistry:

// The classic static registration (e.g., src/components/registry.ts)
import { initContentTypeRegistry } from '@optimizely/cms-sdk';
import { initReactComponentRegistry } from '@optimizely/cms-sdk/react/server';

import HeroBanner, { HeroBannerContentType } from './HeroBanner';
import ArticlePage, { ArticlePageContentType } from './ArticlePage';
import GridContainer, { GridContainerContentType } from './GridContainer';

export function registerComponents() {
  initContentTypeRegistry([
    HeroBannerContentType,
    ArticlePageContentType,
    GridContainerContentType,
  ]);

  initReactComponentRegistry({
    resolver: {
      HeroBanner,
      ArticlePage,
      GridContainer,
    },
  });
}

This explicit registration is an excellent default design. It's clean, type-safe, and self-documenting.

But as your engineering team grows from one developer to five, ten, or twenty working in parallel, this central registry file starts to feel a bit crowded.

Let's talk about the friction this creates — and how a small shift in thinking can clean up your git history forever.


The Tragedy of the Common Registry File

Imagine a typical day on an enterprise development team.

  • Developer A is on a feature branch building a new PromoCard component.

  • Developer B is on a separate branch creating an AuthorBio content type.

  • Developer C is refactoring the NavigationMenu.

Each developer creates their standalone component file. But to make it work in their respective sandbox environments, they all have to touch the exact same central file — src/components/registry.ts — to import and register their work.

Everything works beautifully in isolation. The local tests pass, and the PRs are opened.

But when the time comes to merge these branches back into main, Git grinds to a halt. There's a merge conflict in registry.ts. Because everyone modified the exact same import list on the exact same lines, the automated merge fails.

Dealing with merge conflicts on actual code logic is a normal part of engineering. Dealing with merge conflicts on a list of import statements is just tax. It slows down your CI/CD pipelines, frustrates developers, and adds zero business value.


The Mind Shift: Static Imports vs. Runtime Discovery

The default behavior of manually importing components in a static file is excellent for small projects or stable schemas. It provides absolute certainty about what is being loaded — Optimizely's design here is intentional and solid.

But as you scale parallel development, the question changes: "Do we really need to declare what files exist when our file system already knows?"

This is the mental model shift: move from manual, static registration to automated, runtime discovery.

If a developer places a valid component — with its matching contentType definition — inside the src/components folder, the project should automatically detect and register it. Your folder structure is the source of truth; your registry file should simply reflect that truth dynamically at build time.

By generating this registry file automatically, we preserve all the power, safety, and type-safety of Optimizely's SDK while eliminating manual list maintenance — and the inevitable merge conflicts that come with it — entirely.

The question for your team to consider: is the overhead of manually maintaining the registry file appropriate for the size and pace of your team? For a solo developer or a two-person squad, the static approach is perfect. For five or more developers running parallel feature tracks, automation becomes the right call.


Implementing Registry Generation

Instead of writing complex dynamic imports that can sometimes complicate React server-side rendering or bundle splitting, the most robust approach is a lightweight pre-build codegen script that automatically compiles your registry.ts file right before your app starts or builds.

This is a well-established industry pattern that works perfectly with standard Next.js and React environments.

1. The Registry Generator Script

Create a native Node.js script in your project root: generate-registry.mjs. It recursively scans src/components, finds every component file, and writes a fully-resolved registry.ts.

// generate-registry.mjs
import fs from 'fs';
import path from 'path';

const COMPONENTS_DIR = './src/components';
const OUTPUT_FILE = './src/components/registry.ts';

function getComponentFiles(dir, filesList = []) {
  if (!fs.existsSync(dir)) return filesList;

  const items = fs.readdirSync(dir);

  for (const item of items) {
    const fullPath = path.join(dir, item);
    const stat = fs.statSync(fullPath);

    if (stat.isDirectory()) {
      getComponentFiles(fullPath, filesList);
    } else if ((item.endsWith('.tsx') || item.endsWith('.ts')) && !item.includes('registry')) {
      // Expects each file to export a default component + a named ContentType
      // e.g. export const HeroBannerContentType = contentType(...)
      filesList.push(fullPath);
    }
  }
  return filesList;
}

try {
  console.log('[Optimizely Registry] Scanning components...');
  const files = getComponentFiles(COMPONENTS_DIR);

  const imports = [];
  const contentTypes = [];
  const resolvers = [];

  files.forEach((file) => {
    const relativePath = './' + path.relative(path.dirname(OUTPUT_FILE), file)
      .replace(/\\/g, '/')
      .replace(/\.tsx?$/, '');

    const componentName = path.basename(file, path.extname(file));
    const contentTypeConst = `${componentName}ContentType`;

    imports.push(`import ${componentName}, { ${contentTypeConst} } from '${relativePath}';`);
    contentTypes.push(`  ${contentTypeConst}`);
    resolvers.push(`    ${componentName}`);
  });

  const output = `// ==========================================================================
// AUTO-GENERATED — do not edit manually. Changes will be overwritten.
// Run: node generate-registry.mjs  |  Source: generate-registry.mjs
// ==========================================================================

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

${imports.join('\n')}

export function registerComponents() {
  initContentTypeRegistry([
${contentTypes.join(',\n')},
  ]);

  initReactComponentRegistry({
    resolver: {
${resolvers.join(',\n')},
    },
  });
}
`;

  fs.writeFileSync(OUTPUT_FILE, output, 'utf8');
  console.log(`[Optimizely Registry] Done — ${files.length} component(s) registered.`);

} catch (err) {
  console.error('[Optimizely Registry] Generation failed:', err.message);
  process.exit(1);
}

2. Wire It Into Your Lifecycle

Hook the script into your package.json using npm's built-in pre hooks. It runs automatically before both dev and build — no manual step required.

"scripts": {
  "predev": "node generate-registry.mjs",
  "prebuild": "node generate-registry.mjs",
  "dev": "next dev",
  "build": "next build",
  "start": "next start"
}

Then add the generated file to .gitignore, since it is a build artifact:

# .gitignore
src/components/registry.ts

From this point on, no developer ever touches registry.ts again. It is generated fresh on every npm run dev and every CI/CD build run.


Why This Works on Local and CI/CD Alike

Because the generator runs as part of the standard dev/build lifecycle, it scales flawlessly across all stages without any developer intervention.

Scenario

Static Registry

Generated Registry

Local Sandbox

Developers must manually add imports every time they create a component.

Creating a .tsx file and running npm run dev registers it automatically.

Feature Branch

Every branch modifies the shared registry.ts, creating Git conflicts on merge.

Branches only contain the component file itself. registry.ts is Git-ignored. Merges are 100% clean.

CI/CD Pipeline

Static imports are read as-is. A missed import causes a silent runtime failure.

The generator scans the actual workspace. If the file exists in the repo, it gets registered.


Is This the Right Move for Your Team?

Runtime registry generation isn't a universal answer — it's a tool for the right context. Here's how to think about it:

Stick with the static registry if:

  • Your team is small (one to three developers)

  • Your schema is stable and rarely changes

  • You value absolute explicitness and prefer to control every import

Consider dynamic generation if:

  • You have four or more developers working on parallel feature branches

  • You're frequently hitting merge conflicts on a single registration file

  • You want your CI/CD pipeline to be self-healing rather than dependent on human memory

Neither approach is wrong. The static model is exactly what Optimizely recommends out of the box — and for good reason. The generated model is a team-scale evolution of that same idea, where automation takes on the maintenance burden.


Wrapping Up

Optimizely's SDK gives you a clean, type-safe, and explicit way to map content types to React components. That design is intentional and it's solid.

But as teams grow, automation should replace manual list-keeping. By shifting your mental model toward dynamic registry generation, you get the best of both worlds: the reliability and structure of Optimizely's component resolution, paired with a conflict-free, scalable parallel development workflow.

It's a simple script with a meaningful impact on your daily developer experience. Give it a try on your next feature sprint and let the filesystem do the organizing.


Are you running into registry conflicts on your Optimizely CMS SaaS projects? Have you found other ways to automate the component wiring? Share what's working for your team in the comments — we'd love to hear it.

Jul 21, 2026

Comments

error Please login to comment.
Latest blogs
How I Deployed My Optimizely Content JS SDK Next.js App on Vercel (Hello Opti World)

📌 Scope: This post covers Optimizely CMS (SaaS) only, using the official content-js-sdk with Next.js 15 deployed to Vercel. This is a practitioner...

Kiran Patil | Jul 21, 2026 |

Finding Thomas Part 5 - The Closed Loop

Five weeks. Five layers. One Thomas. If you've followed this series from the start — thank you. If you're just landing here, the short version:...

Ritu Madan | Jul 20, 2026

Extending the Optimizely Product Recommendations Feed to Include Custom Product Types

A practical way to extend the Optimizely Product Recommendations catalog feed so the export scheduled job also includes custom catalog types, like...

Wojciech Seweryn | Jul 20, 2026 |