Vue Storefront is now Alokai! Learn More
Using Multiple CMS Systems

Using Multiple CMS Systems

While most projects use a single CMS, Alokai supports running multiple CMS integrations simultaneously. This is useful for:

  • PoC/Evaluation: Testing different CMS platforms before committing to one
  • Migration: Gradually transitioning from one CMS to another
  • Multi-brand: Different brands using different CMS systems

Prerequisites

Before adding a second CMS, ensure you have:

Installation

A generated Alokai project always comes with one CMS pre-installed. To add a second CMS, simply install the additional module:

npx @vsf-enterprise/storefront-cli add-module cms-<cms-name>

Replace <cms-name> with your desired CMS. For instance builderio, contentful, contentstack, smartedit, or storyblok.

For detailed installation instructions for each CMS, refer to the CMS modules documentation.

The installation script will:

  • Install the CMS API Client in the Middleware
  • Install the CMS SDK module in the Storefront
  • Add CMS-specific configuration
  • Set up example environment variables

Configuration

After installation, both CMS integrations will be registered in your Middleware and SDK. Each CMS will have:

  • Middleware integration in apps/storefront-middleware/middleware.config.ts
  • SDK module in apps/storefront-unified-nextjs/sdk/sdk.config.ts
  • Environment variables in apps/storefront-middleware/.env

No additional configuration is needed - both CMS systems work independently.

Using Multiple CMS Systems

The key to using multiple CMS systems is controlling which CMS components you import on each page. Both connectCmsPage and RenderCmsContent are CMS-specific and must be imported from the correct module.

Choosing the CMS per Page

To use a specific CMS on a page, import its wrapper components from the corresponding module:

import { connectCmsPage, RenderCmsContent } from '@sf-modules/cms-<cms-name>/components';

Replace <cms-name> with your CMS identifier (e.g., contentful, smartedit, amplience, storyblok, etc.).

Example: Separate Routes for Each CMS

The most common pattern is to use different route paths for each CMS.

You can find the default dynamic page implementation for each CMS in apps/storefront-unified-nextjs/sf-modules/cms-<cms-name>/app/[locale]/(cms)/[[...slug]]/page.tsx. Use these as reference when creating your own CMS pages.

Contentful pages at /[locale]/(cms)/[[...slug]]/page.tsx:

import { notFound } from 'next/navigation';
import { Suspense } from 'react';

import { connectCmsPage, RenderCmsContent } from '@sf-modules/cms-contentful/components';
import type { DynamicPage } from '@/types/cms';

async function DynamicPage({ page }: { page?: DynamicPage }) {
  if (!page) {
    return notFound();
  }

  const { componentsAboveFold, componentsBelowFold } = page;

  return (
    <>
      <RenderCmsContent aboveFold item={componentsAboveFold} />
      <Suspense>
        <RenderCmsContent item={componentsBelowFold} />
      </Suspense>
    </>
  );
}

export default connectCmsPage(DynamicPage, {
  getCmsPagePath: ({ params }) => `/${params.slug?.join('/') ?? ''}`,
});

SmartEdit pages at /[locale]/smartedit/[[...slug]]/page.tsx:

import { notFound } from 'next/navigation';

import connectCmsPage from '@sf-modules/cms-smartedit/components/connect-cms-page';
import { PageTemplates } from '@sf-modules/cms-smartedit/components/templates';

export default connectCmsPage(
  (props) => {
    const { page, params, template, ...rest } = props;

    if (!page) {
      notFound();
    }

    const Template = PageTemplates[template as keyof typeof PageTemplates];
    const { $raw: _, ...slots } = page;

    if (!Template) {
      throw new Error(`Template "${template}" not found`);
    }

    return <Template slots={slots} {...rest} />;
  },
  {
    getCmsPagePath: ({ params }) => `/${params.slug?.join('/') ?? ''}`,
  },
);

Example: Fallback CMS Pattern

Imagine you're migrating from SmartEdit to Contentful. You want to create new pages in Contentful while keeping existing SmartEdit pages available. This pattern lets your application check Contentful first, then automatically fall back to SmartEdit if content isn't found there yet.

1

Create the fallback wrapper

Create a new file app/[locale]/(cms)/[[...slug]]/connect-cms-page-with-fallback.tsx:

This implementation combines logic from both CMS systems' connectCmsPage wrappers. You can find the original implementations in apps/storefront-unified-nextjs/sf-modules/cms-<name>/components/connect-cms-page.tsx for reference or customization.

import { prepareConfig } from '@alokai/connect/sdk';
import type { ComponentType } from 'react';

import { getSdk } from '@/sdk';
import ContentfulLivePreview from '@sf-modules/cms-contentful/components/live-preview';
import SmarteditLivePreview from '@sf-modules/cms-smartedit/components/live-preview';

interface ConnectCmsPageParams<TProps> {
  getCmsPagePath: (props: TProps) => Promise<string> | string;
}

const componentsByPath: Record<string, ComponentType<any>> = {};

const appLocaleToCmsLocale: Record<string, string> = {
  de: 'de',
  en: 'en',
};

export default function connectCmsPageWithFallback<TProps>(
  PageComponent: ComponentType<TProps>,
  { getCmsPagePath }: ConnectCmsPageParams<TProps>,
) {
  async function CmsPage(props: any) {
    const sdk = getSdk();
    const { params, searchParams } = props;
    const path = await getCmsPagePath(props);
    componentsByPath[path] = PageComponent;
    const cmsLocale = appLocaleToCmsLocale[params.locale];

    // Try primary CMS (Contentful)
    const contentfulPage = await sdk.unifiedContentful
      .getPage(
        { locale: cmsLocale, path },
        prepareConfig({
          headers: {
            'x-preview': `${!!searchParams?.preview}`,
          },
        }),
      )
      .catch(() => null);

    if (contentfulPage) {
      async function rerender(rawPage: Record<string, any>) {
        'use server';
        const PageComponent = componentsByPath[path];
        const page = await getSdk().unifiedContentful.normalizePage({
          page: rawPage,
        });
        return <PageComponent {...props} page={page} cmsSource="contentful" />;
      }

      return (
        <>
          <div id="ssr-content">
            <PageComponent {...props} page={contentfulPage} cmsSource="contentful" />
          </div>
          <ContentfulLivePreview initialData={contentfulPage.$raw} locale={cmsLocale} rerender={rerender} />
        </>
      );
    }

    // Fallback to secondary CMS (SmartEdit)
    const smarteditPage = await sdk.unifiedSmartedit
      .getPage({ locale: cmsLocale, path }, prepareConfig({}))
      .catch(() => null);

    if (smarteditPage) {
      return (
        <>
          <PageComponent {...props} page={smarteditPage} cmsSource="smartedit" />
          <SmarteditLivePreview page={smarteditPage.$raw} />
        </>
      );
    }

    // Neither CMS has the page
    return <PageComponent {...props} page={null} cmsSource={null} />;
  }

  return CmsPage;
}

2

Create your page component

Create app/[locale]/(cms)/[[...slug]]/page.tsx:

import { notFound } from 'next/navigation';
import { Suspense } from 'react';

import { RenderCmsContent as RenderContentful } from '@sf-modules/cms-contentful/components';
import { RenderCmsContent as RenderSmartedit } from '@sf-modules/cms-smartedit/components';
import type { DynamicPage } from '@/types/cms';

import connectCmsPageWithFallback from './connect-cms-page-with-fallback';

interface PageProps {
  params: { slug?: string[]; locale: string };
  page: DynamicPage | null;
  cmsSource: 'contentful' | 'smartedit' | null;
}

async function DynamicPageComponent({ page, cmsSource }: PageProps) {
  if (!page) {
    return notFound();
  }

  const { componentsAboveFold, componentsBelowFold } = page;

  // Select the appropriate RenderCmsContent based on which CMS provided the content
  const RenderCmsContent = cmsSource === 'smartedit' ? RenderSmartedit : RenderContentful;

  return (
    <>
      <RenderCmsContent aboveFold item={componentsAboveFold} />
      <Suspense>
        <RenderCmsContent item={componentsBelowFold} />
      </Suspense>
    </>
  );
}

export default connectCmsPageWithFallback(DynamicPageComponent, {
  getCmsPagePath: ({ params }) => `/${params.slug?.join('/') ?? ''}`,
});

How it works:

  1. The wrapper first attempts to fetch content from Contentful (primary CMS)
  2. If content is not found, it automatically falls back to SmartEdit (secondary CMS)
  3. The page component receives a cmsSource prop indicating which CMS provided the content
  4. The appropriate RenderCmsContent component is used based on the content source

SDK Usage

Each CMS has its own SDK namespace. You can use both in the same file:

import { getSdk } from '@/sdk';

const sdk = await getSdk();

// Fetch from Contentful
const contentfulPage = await sdk.unifiedContentful.getPage({
  path: '/about',
  locale: 'en',
});

// Fetch from SmartEdit
const smarteditPage = await sdk.unifiedSmartedit.getPage({
  path: '/terms',
  locale: 'en',
});

Best Practices

  • Clear separation: Use distinct route paths for each CMS (e.g., /cms-a/* and /cms-b/*)
  • Consistent imports: Always import from the specific CMS module
  • Testing: Test both CMS integrations independently

Common Use Cases

PoC/Evaluation Phase

During evaluation, create parallel pages in both CMS systems:

  • /contentful/homepage - Contentful version
  • /smartedit/homepage - SmartEdit version

This allows stakeholders to compare both systems side-by-side.

Migration Scenario

Gradually migrate pages from old CMS to new CMS:

  • Keep old CMS pages at their original paths
  • Create new CMS pages at new paths
  • Redirect old paths to new paths as content is migrated
  • Remove old CMS once migration is complete

Multi-brand Setup

Use different CMS for different brands:

  • Brand A uses Contentful at /brand-a/*
  • Brand B uses SmartEdit at /brand-b/*