How to Manage Multiple Domains with a Headless CMS

Anton Meleshkin - Aug 26 - - Dev Community

Hello, fellow developers! Today, we’re diving into a common challenge: "How can I manage 2+ domains under one CMS space?" Luckily, with modern technologies, this is easier than you might think.

For this guide, we'll use Storyblok as our headless CMS (hCMS) of choice due to its intuitive design and flexible features. However, if you’re using a different hCMS, you can still apply the core principles discussed here, as long as your CMS supports folder-based organization.

What will you learn in this guide? Here's a sneak peek:

  • How to bake a multi-domain Storyblok space
  • Tasty spices, aka small but very useful features
  • Handling international filling

The Scenario: Managing Multiple Domains

Let’s imagine you have three domains that need to be managed under one hCMS workspace:

  • domain-1.com
  • domain-2.com
  • domain-3.com

Step 1: Organizing Content by Domain

Start by creating folders within your CMS for each domain and fill them with the respective content. In Storyblok, this might look something like this:

Three domain Storyblok example

Domain pages example

With your content neatly organized, the necessary preparations on the CMS side is complete. Now, let’s dive into the code.

Step 2: Configuring Data Fetching in Next.js

In this guide, we'll use Next.js along with Vercel for deployment. The key here is to configure data fetching based on the domain, which will allow your application to serve the correct content dynamically.

Example: Fetching Data for a Specific Domain

async function getData({ slug }: { slug: string }) {
  const storyblokApi = getStoryblokApi();
  try {
    let { data } = await storyblokApi.get(
      `cdn/stories/${process.env.NEXT_PUBLIC_BASE_PATH}/${slug}`,
      {
        version: "draft", // Fetches the draft version of the content
      }
    );

    return {
      props: {
        story: data ? data.story : false,
        key: data ? data.story.id : false,
      },
      revalidate: 86400, // Revalidate every 24 hours
    };
  } catch (error) {
    redirect("/404"); // Redirect to 404 page if content is not found
  }
}
Enter fullscreen mode Exit fullscreen mode

Environment Variable Setup

To ensure your application knows which domain it’s serving, set up an environment variable in your .env file:

NEXT_PUBLIC_BASE_PATH=domain-1
Enter fullscreen mode Exit fullscreen mode

Step 3: Deploying on Vercel

When deploying to Vercel, you’ll need to create a separate project for each domain. In each project, configure the environment variables accordingly:

  1. Project 1: NEXT_PUBLIC_BASE_PATH=domain-1
  2. Project 2: NEXT_PUBLIC_BASE_PATH=domain-2
  3. Project 3: NEXT_PUBLIC_BASE_PATH=domain-3

Vercel environment fields image

This ensures that each domain pulls the correct content from your CMS.

Step 4: Handling Links in Your Application

Example: Link Processing Function

export const linkProcessor = (link: IStoryLink) => {
  if (!!process.env.NEXT_PUBLIC_BASE_PATH) {
    let correctUrl = link?.story?.full_slug || link.cached_url || link.url;

    // Remove base path if link is to the homepage
    if (correctUrl === `${process.env.NEXT_PUBLIC_BASE_PATH}/`) {
      correctUrl = correctUrl.replace(`${process.env.NEXT_PUBLIC_BASE_PATH}`, '');
    } else {
      correctUrl = correctUrl.replace(`${process.env.NEXT_PUBLIC_BASE_PATH}/`, '');
    }

    return correctUrl;
  }

  return link?.story?.full_slug || link.cached_url || link.url;
};
Enter fullscreen mode Exit fullscreen mode

This function ensures that when users navigate your site, they won’t see URLs like prod-domain.com/domain-1/ but rather the cleaner prod-domain.com/.

That's all. It's THAT simple. What did you expect, a 10-page guide? Stop reading and go have fun with JavaScript! Happy hacking!

Advanced Considerations

For more sophisticated readers, here are a few other places where you can use this in your project.

Sitemap Generation

First of all, don't forget your roots. In our case, it's a sitemap. The main thing you should do is add this NEXT_PUBLIC_BASE_PATH to .env file.

NEXT_PUBLIC_PRODUCTION_DOMAIN=https://prod-domain.com
Enter fullscreen mode Exit fullscreen mode

In your sitemap generation script (generateSitemap.ts), modify the slug to remove the base path:

const slug = story.full_slug.replace(`${process.env.NEXT_PUBLIC_BASE_PATH}/`, '');

const slugWithoutTrailingSlash = slug
  .split('/')
  .filter(Boolean)
  .join('/');

return `\n<url>\n<loc>${process.env.NEXT_PUBLIC_PRODUCTION_DOMAIN}/${slugWithoutTrailingSlash}</loc>
<lastmod>${publishedAt}</lastmod></url>`;
Enter fullscreen mode Exit fullscreen mode

Configuring Redirects and Rewrites

You can also control redirects, rewrites, or other configurations based on the domain:

switch (process.env.NEXT_PUBLIC_BASE_PATH) {
  case 'domain-1':
    redirects = [...DEFAULT_REDIRECTS, ...DOMAIN_1_REDIRECTS];
    rewrites = DEFAULT_REWRITES;
    break;
  default:
    redirects = DEFAULT_REDIRECTS;
    rewrites = DEFAULT_REWRITES;
    break;
}
Enter fullscreen mode Exit fullscreen mode

Managing Tracking Scripts

Lastly, use the environment variable to manage domain-specific tracking scripts:

<Script
  id="tracking-script"
  strategy="lazyOnload"
  src="https://cdn.tracking_script.com/script/ididid"
  data-domain={
    process.env.NEXT_PUBLIC_BASE_PATH === 'domain-1'
      ? 'first-domain-id'
      : 'domain-2.com'
  }
/>
Enter fullscreen mode Exit fullscreen mode

This ensures that your tracking data is accurately associated with the correct domain.

Multilanguage

You may ask yourself, "What about multilanguage support?" Fear not! Storyblok has you covered. By following the same principles discussed here, you can easily manage multilanguage content across multiple domains. Simply create folders for each language and domain, and you're good to go.

Multilanguage folders example

If you wish for more control over multilanguage settings, you can create a configuration folder with a languages subfolder. In this subfolder, create stories for each language with any settings you can imagine. Just fetch this data like we did in the Step 2 example, and you're all set!

Multilanguage configuration lines example

Another question you might have is, "Hey, what if some of my domains are multilanguage, and others aren't?" No worries! You can mix and match as needed. Storyblok's flexible structure allows you to adapt to various scenarios seamlessly.

For example, you can create configuration folder with domains-settings subfolder which will have stories for each domain with settings like isUsingLocales and defaultHomepageSlug:

Configuration lines with locales example

Also, don't forget about Storyblok built-in conditional fields. They can be a lifesaver when you need to show or hide some fields from content managers.

Configuration lines without locales example

You can fetch this configuration in your application and use it to determine how to handle multilanguage content for each domain. The possibilities are endless!

That's all, folks!

And there you have it! With Storyblok and a bit of JavaScript magic, you can manage multiple domains under one CMS space with ease. So go forth and build amazing projects that span the digital realm. Happy hacking!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .