Jan 6, 202515 min read

Migrating from React to Next.js in a Sitecore World: Overcoming the Component Factory Hurdle

Post thumbnail

Introduction

Have you ever looked at your project’s folder structure and realized that something as small as a single file named index.tsx could cause massive headaches? If you’ve worked with Sitecore JSS, React, and Next.js, you might be nodding in agreement right now. I’ve recently gone through the wild journey of migrating a React-based Sitecore application over to Next.js, and while the move seemed straightforward at first, it quickly turned into a developer “choose your own adventure” story. One of the big twist? The automatic component builder that Sitecore provides for Next.js—designed to make our lives simpler—ended up pulling in subcomponents (and even certain leftover files) as if they were full-fledged Sitecore renderings. Yikes.

In this blog post, I’m going to walk you through how I tackled that exact problem and customized our Sitecore Next.js plugin to build a component factory that matches our old React structure—without turning every single file into a “component” in Sitecore. We’ll talk about why this is important for your Sitecore JSS migration, the pitfalls of using the default generate script, and how you can do it yourself if you’re stuck in the same boat. So buckle up: if you’re a technical developer or architect making the leap to Next.js with Sitecore, this one’s for you.


Why Even Migrate from React to Next.js?

Let’s face it: Next.js has taken the React ecosystem by storm. Its robust server-side rendering (SSR) capabilities, out-of-the-box optimizations for SEO, and flexible routing make it an appealing framework for enterprise solutions—especially for Sitecore-based sites that demand top-tier performance and an easily maintainable codebase.

When you pair Next.js with Sitecore JSS, you get:

  1. Server-Side Rendering + Static Generation: Built-in SSR for better SEO and performance.
  2. Easy Page Routing: Next.js’ file-based routing system is more intuitive than setting up custom webpack configurations.
  3. Rich Developer Experience: Automatic code splitting, dynamic imports, and environment-based optimization.
  4. Improved Sitecore Integration: A synergy between the Sitecore headless services and Next.js’ approach to dynamic data fetching.

All these benefits make Next.js an ideal partner for Sitecore, especially if you’re running large-scale content-driven websites that require personalization, analytics, and all the bells and whistles Sitecore has to offer.

But as with all migrations, the devil is in the details. And one of the biggest details here is how Sitecore JSS wants you to register and map your Next.js components. Enter: the component factory.


The Sitecore JSS Component Factory: A Quick Overview

If you’ve been doing Sitecore JSS in React, you might already know about the concept of a “component factory” (sometimes called a “component builder”). Essentially, Sitecore needs a mapping between the renderings you create in its back-end (think of them as placeholders for dynamic content) and the actual React/Next.js components you’ve built out in your codebase. This allows Sitecore editors (and developers) to dynamically construct pages by choosing which “component” (as Sitecore sees it) should appear in which placeholder.

In an ideal world, you point Sitecore to a certain folder in your repository—like src/components—and it will automatically discover each of your React components and register them with the correct matching name in Sitecore. Quick and painless, right? Well, not always.


The Default Sitecore Next.js Plugin: When “Magic” Goes Wrong

When you spin up a new Sitecore JSS + Next.js project using the official boilerplate or sample, you’ll notice there’s a script or plugin that handles this “auto-generation” of your component factory. Here’s the usual flow:

  1. You specify a folder path (e.g., src/components) in the plugin configuration.
  2. The plugin scans every .tsx or .ts file under that folder.
  3. Each file becomes a “component” in the generated componentBuilder.ts (or a similarly named file).
  4. Sitecore sees these components and can map them to known renderings.

In a brand new project—where each folder likely has only a single .tsx file (often named after the component)—this can work smoothly. But in a real-world scenario, especially where you have:

  • Multiple subcomponents within a folder, such as src/components/Button/index.tsx, src/components/Button/variants/SpecialButton.tsx, src/components/Button/helpers.ts.
  • Shared utility files or partial components that aren’t intended as official Sitecore “renderings.”

This default approach can break. Suddenly, you’ll see logs indicating that every single file found under components/Button/… is recognized as a separate “component.” If you have an index.tsx (intended to re-export or serve as the parent component) plus other scripts, each of those might end up polluting your Sitecore dictionary with new, unintended renderings.


Our Migration Reality Check: Too Many “Components”

During our migration from React to Next.js, we kept our same folder structure:

src
 └── components
      ├── Button
      │    ├── index.tsx
      │    ├── variants
      │    │    └── SpecialButton.tsx
      │    └── helpers.ts
      └── Card
           ├── index.tsx
           ├── variants
           │    └── FeaturedCard.tsx
           └── ...

In the old React (non-Next.js) project, we treated Button as a single Sitecore “rendering.” Everything else under that folder was either a helper function or a subcomponent that only made sense inside the parent button concept. We never intended to make them separate components in Sitecore. But in the new Next.js-based setup, the Sitecore plugin that scans our folder would see Button/index.tsx, Button/variants/SpecialButton.tsx, and Button/helpers.ts each as a separate entity. That’s obviously not what we wanted.

No matter how many times I tried to hack around the default plugin (e.g., ignoring certain paths, renaming subcomponents), the underlying problem was that the default plugin runs a standard function to generate these mappings, and it does it in a very prescriptive way. It expects “one component per folder,” and it can’t gracefully handle subcomponents or nested .tsx files.


The (Failed) Attempts at Customization

Sitecore’s plugin system is pretty extensible in theory. You can add a custom plugin or override the existing ones to achieve your own logic. So my first instinct was: “Great, I’ll just override the generate method, filter out anything named index.tsx from subfolders, and call it a day.”

Unfortunately, it wasn’t that simple. By the time the plugin system got to my custom code, it had already triggered the default generation method, meaning my custom plugin logic would run first, and then the default logic would run afterward. The result? My carefully curated list of components ended up appended by a bunch of noisy extras. Even if I tried to forcibly remove them after the fact, the generation code from Sitecore is somewhat “all or nothing.” You either let it do its work or you skip it entirely. So skipping it entirely started to sound like a good plan.


The Lightbulb Moment: Overwrite the Generation Process

After multiple false starts trying to get the official function to ignore my subcomponents, I decided to do what, in retrospect, I should have done from the start:

  1. Stop using the default generate function entirely.
  2. Write my own script that scans only for top-level folders containing an index.tsx.
  3. Treat each top-level folder as a single Sitecore “component” with the folder name as the component’s name.

That way, all of my subcomponents or helper files—like Button/helpers.ts or Button/variants/SpecialButton.tsx—would be ignored by default. They’re not recognized as unique components because they don’t live at the top folder level or they don’t contain an index.tsx file.

Why an index.tsx?

By enforcing an index.tsx as the point of entry for each component folder, we create a clear standard. If you want a folder to be recognized as a Sitecore component, you name it meaningfully (e.g., Button or Card) and ensure it has an index.tsx. If it doesn’t have an index.tsx, we skip it. This keeps the mapping simple and ensures no extra one-off files become full-blown components.


The Final Custom Implementation: How It Works

Below is a simplified, anonymized version of what the final script or plugin logic looks like (I’ve replaced any proprietary naming to keep this generic). Note that you’ll likely create something like customComponentBuilderPlugin.ts or customComponentBuilder.ts in your scripts folder. For demonstration purposes, I’m going to paraphrase and annotate:

/** 
 * Pseudocode for a custom Next.js + Sitecore JSS component builder plugin.
 * 
 * This code forcibly overwrites the default Sitecore generate function,
 * scanning only the top-level folders in the `src/components` directory.
 * If a folder has an `index.tsx`, we treat that as the main component file.
 */

import fs from 'fs';
import path from 'path';

export interface ComponentInfo {
  path: string;        // e.g. "../components/Button"
  moduleName: string;  // e.g. "Button"
  componentName: string; // e.g. "Button" or "CMSButton" (depending on naming scheme)
}

function toValidModuleName(folder: string): string {
  // Remove hyphens and convert folder names to valid JS variable names
  // e.g., "CMS-Button" becomes "CMSButton"
  return folder
    .split('-')
    .map((part, index) =>
      index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)
    )
    .join('');
}

export function customGenerateComponentBuilder() {
  const componentsDir = path.resolve(__dirname, '../../../src/components'); // or your custom path

  if (!fs.existsSync(componentsDir)) {
    console.error(`Components directory not found at path: ${componentsDir}`);
    return [];
  }

  const componentFolders = fs
    .readdirSync(componentsDir, { withFileTypes: true })
    .filter((dirent) => dirent.isDirectory())  // only read directories
    .map((dirent) => dirent.name);

  const components: ComponentInfo[] = [];

  componentFolders.forEach((folder) => {
    const indexFilePath = path.join(componentsDir, folder, 'index.tsx');
    if (fs.existsSync(indexFilePath)) {
      const relativePath = `../components/${folder}`;
      const moduleName = toValidModuleName(folder);

      components.push({
        path: relativePath,
        moduleName: moduleName,
        componentName: folder, // the name Sitecore uses
      });

      console.log(`Registering JSS Component ${moduleName}`);
    } else {
      console.warn(`Warning: No index.tsx found for "${folder}". Skipping.`);
    }
  });

  // Now generate the file content with only these top-level components
  generateComponentBuilderFile(components);

  return components;
}

// The actual file generation step
function generateComponentBuilderFile(components: ComponentInfo[]) {
  const outputDir = path.resolve(__dirname, '../../../src/temp');
  const outputPath = path.join(outputDir, 'componentBuilder.ts');

  fs.mkdirSync(outputDir, { recursive: true });

  // Create the import statements, e.g.:
  // import * as Button from '../components/Button';
  const importStatements = components
    .map(({ moduleName, path: componentPath }) => {
      return `import * as ${moduleName} from '${componentPath}';`;
    })
    .join('\n');

  // Then create a map of them
  // e.g., components.set('Button', Button);
  const componentSetStatements = components
    .map(({ moduleName, componentName }) => {
      return `components.set('${componentName}', ${moduleName});`;
    })
    .join('\n');

  const fileContent = `/* eslint-disable */
// Auto-generated by customGenerateComponentBuilder.

import { ComponentBuilder } from '@sitecore-jss/sitecore-jss-nextjs';

${importStatements}

export const components = new Map();

${componentSetStatements}

export const componentBuilder = new ComponentBuilder({ components });
export const moduleFactory = componentBuilder.getModuleFactory();
`;

  fs.writeFileSync(outputPath, fileContent, 'utf8');
  console.log(`componentBuilder.ts generated successfully at ${outputPath}`);
}

Voilà! This simple script ensures that only folders containing an index.tsx get treated as Sitecore renderings. Everything else—like subcomponent .tsx files, helper utilities, or random scripts—get ignored. This perfectly mimics our old React structure where each folder was treated as one rendering and everything else was a child of that rendering.


Adapting Your Existing Codebase

If you’re in a situation where some of your components don’t have an index.tsx, you might need to do a bit of housekeeping first:

  1. Consolidate code: If your folder was previously something like Button/main.tsx, Button/helpers.ts, Button/otherStuff.tsx, rename main.tsx to index.tsx (and ensure you’re exporting your main React component from index.tsx).
  2. Review naming collisions: If your naming convention had collisions (e.g., multiple folders named card, Card, or card2), consider standardizing them before generation.
  3. Update your imports: If other parts of your codebase reference Button/main.tsx, you might need to switch it to Button/index.tsx or rely on auto-import from the directory name.

Once you’re consistent about each folder having a single entry point file, the custom script will do the heavy lifting for you.


SEO + Sitecore + Next.js = Why This Matters

So, you might be asking: “Why should I care about subcomponents turning into renderings? Doesn’t that just add some extra entries in Sitecore?” Well, a few reasons:

  1. Clean Page Editing Experience: Having dozens of extraneous “renderings” in Sitecore’s editor can confuse content authors. They might see “helpers” or “subcomponents” in the list of available components and try to drop them onto a page, leading to weird content structures.
  2. Performance and Maintenance: Each extra rendering in Sitecore can add overhead—both in performance (Sitecore has to keep track of them) and in developer maintenance. A lean list of legitimate components is much easier to manage.
  3. SEO & Deployment Pipelines: Next.js thrives when your code structure is organized. If your builds start pulling in code from subcomponent files as top-level pages or renderings, you risk messing up your route structures, dynamic imports, or performance optimizations. A well-structured codebase ensures your site remains SEO-friendly, with minimal bloat.

Keeping the mapping tight ensures that your editorial team can easily build pages in a WYSIWYG fashion while your Next.js app remains fast, consistent, and free from mysterious “phantom components.”


Lessons Learned: A Developer’s Journey

  1. Sitecore Tools Are Opinionated: The default plugin approach is great for a brand-new project with a strict “one file = one component” approach. But real-world code rarely fits one strict pattern.
  2. Migration Means Re-Evaluation: If you’re jumping from React to Next.js, it’s a perfect moment to rethink your folder structure. Some legacy patterns might not translate well, and that’s okay.
  3. Don’t Fear the Custom Script: Overriding default behavior might sound scary, but often it’s simpler than hacking around the official approach. The key is to own your build pipeline.
  4. Communication with Stakeholders: When your content authors see “SpecialButtonVariant” or “ButtonHelpers” as separate components in the Sitecore UI, they might get confused. Keep the editorial experience in mind to maintain healthy synergy between devs and content teams.

Potential Pitfalls and How to Avoid Them

  1. Missing Dependencies: If your custom script references Node packages not installed in your environment, your build will fail. Ensure your environment is set up with the same Node version and dependencies.
  2. Hardcoded Paths: Notice how I explicitly used path.resolve(__dirname, '../../../src/components') in the snippet above? Adjust it to match your actual project structure. And if you rename your component directory in the future, don’t forget to update this.
  3. Inconsistent Folder Naming Conventions: If your folders have spaces, capital letters, or odd punctuation, your auto-conversion method might break. Standardize naming for all your top-level component folders.
  4. Running at the Wrong Phase of the Build: Make sure your custom plugin runs at the correct stage of your build lifecycle. Otherwise, you might end up with a partial or outdated componentBuilder.ts.

Real-World Use Cases

  • Multi-brand Websites: If your Sitecore instance powers multiple brand sites, you might have separate folders in components for each brand. By filtering only those with an index.tsx, you can maintain a neat separation of concerns.
  • Complex UI Libraries: Sometimes large teams create a shared UI library with multiple subcomponents. With the default plugin, each subcomponent might pop up as a top-level rendering. A custom generation script ensures your Sitecore list doesn’t spiral out of control.
  • Gradual Migration: If you’re gradually migrating from an older React project, you may already have a structure in place where each folder has subcomponents. This approach makes it smoother to onboard your existing code rather than forcing a complete folder overhaul.

Final Thoughts: The Power of Customization

Writing custom scripts or plugins might feel like an extra chore—especially when Next.js and Sitecore JSS promise a ready-made developer experience. But sometimes, the biggest time saver is ironically to roll your own solution. We overcame the default plugin’s limitations by simply ignoring it and building a custom pipeline that understands how we define components. That small shift in perspective saved us from endless hacks, partial solutions, and overhead in our code.

If you’re embarking on a Sitecore + Next.js migration journey, my advice is to keep an open mind. The default tooling is awesome when your folder structure is brand-new, neat, and standard. But most real-world codebases come with baggage, meaning you’ll likely need to customize some part of the generation process. Don’t be afraid to get in there, read the source code of the default plugin, and figure out how to do it yourself. You’ll learn a ton about how Sitecore JSS binds React/Next.js components to Sitecore items—and you’ll maintain the folder structure that suits your team’s workflow best.


Wrapping Up

This migration path from React to Next.js for Sitecore can be both exciting and a little nerve-racking. On the one hand, you get all the benefits of Next.js’ SSR and performance optimizations. On the other hand, you might stumble into weird quirks—like the default component factory generation that lumps every file into your Sitecore environment as a “rendering.” By sharing my story, I hope I’ve shed some light on how to gracefully handle this situation.

Key Takeaways:

  • Understand that the default Sitecore Next.js plugin is opinionated.
  • Identify whether your existing folder structure matches that opinion—or not.
  • Customize the component generation process if you have nested subcomponents or a non-standard approach.
  • Maintain a consistent editorial experience. Unintended renderings in Sitecore can confuse your content authors and degrade performance.

With these insights, you can confidently build your own plugin or script, adapt your folder structure, and keep your code lean. This not only preserves your team’s established patterns but also sets you up for success in the future—especially as Next.js and Sitecore continue to evolve.

Happy coding, and good luck with your Next.js + Sitecore migration!

Share:

follow us

© 2025 frankcostoya.com. All rights reserved.