Skip to main content

How to Multi Domain/Tenant/Site with a headless CMS

Learn to manage multi-domain, multi-site and multi-tenant setups in Storyblok and Next.js from content organization to domain-specific configurations.

How to Multi Domain/Tenant/Site with a headless CMS

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:

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
}
}

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

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

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;
};

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

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>`;

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;
}

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'
}
/>

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.

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!

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:

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.

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!

CONTACT US TODAY

Don't want to fill out the form? Then contact us by email hi@focusreactive.com