So you're building a headless solution in sitecore and you want to create a multi-site solution? How can you do this in a properly headless way? This article explores a simple/light-weight approach to setting this up using NPM Workspaces. We're focusing primarily on the client-side solution here but will briefly run through the Sitecore configuration as well.
We have 3 websites we need to create which will have some shared components and some unique components and different theming/content and our solution is using NextJS.
We're looking to have multiple "heads" to manage each Site/App so that we can deploy each to it's own Vercel instance and have it ONLY contain what each App actually needs. This allows us to deploy each Head/Site independantly and scale it independantly. It's also nice to have the simple OOTB error page handling and rewrites etc all managed per-site without having to change it all to understand which site we care about etc. When we talk about "headless" this is essentially what we are trying to achieve here. The ability to detach things which should be seperated. We might want to have another head be deployed to a Kiosk device or another entirely different channel but still share some componentry/logic for example and this would allow us to achieve this while having all 3 of your sites within one NextJS solution would not.
Below is a simiplified diagram of our 3 sites/heads and we have Sitecore running in Azure App Services.
We're using a pretty standard Sitecore Headless Services SXA Tenant/Sites structure here. Note the "shared" site which we are using to allow for sharing of content and settings and things like that between all the sites. We're going to do something very similar in the Front End Solution Set up.
Our Solution is a typical Helix-centric solution which aligns to this structure as well:
Finally, we've set up our Front End Solution such that we have an entirely seperate NextJS Sitecore JSS application for each Site as well as our "shared" one.
Note: the "shared" site could just as easily be a slimmed down react project however we are considering using the 'shared' site as a potential test/styleguide space as well as being a shared component/code solution which is why we've chosen to run it up as another NextJS Sitecore JSS application.
Everything matches up nicely so far, things are looking good. We can build each site independently and link them up to the Sitecore Backend easily. We've got JUST the Visit site's code in the Visit front end project and we've got JUST the Corporate site's code in the Corporate front end project and so on..
Time to share stuff
There are a bunch of options for sharing code between different projects/apps/packages available. The major players being Lerna, Yarn Workspaces and NPM Workspaces. Each has their pros and cons which are worth consideration however I decided to keep things as light-weight and simple as possible and so we're going to focus on NPM Workspaces.
Following the doco linked above you can pretty quickly get this up and running by adding a packages.json file to the root directory (in our case /JSS ) and add the workspace configuration like so:
We only want to share the 'shared' site to each of the individual sites so the other steps we followed to make this possible was to add a dependency in each site's packages.config file for the "shared" site as shown:
We also made good use use of the
next-transpile-modules library which you can read about here to add our "shared module" to make it available within our application. This is done in each site's
next.config.js file as shown.
Something to note here is that we want to wrap the module exports for the OOTB
plugins (along with any others you might have) with the Transpiler logic too. Thankfully they provide a mechanism for this so we're going to update the module.exports to do this now by updating it to look like this:
Since we now have a NPM Workspace defined and we've configured each project with the new dependency in the
packages.json files we can now run NPM Install in the
/JSS (root directory). What this is going to do is install all the node packages into the main
node_packages folder and keep track of which project needs which dependencies. Pretty neat.
Once you've run
npm i you will see that in the generated
packages-lock.json file that you have an entry for each of your Sites similar to this:
This is nice when deploying your sites as well as the shared site won't need to have it's packages installed if they're the same as each app/site as they'll already exist. It's also easy to track/ensure package versions are the same.
Sharing Components between Sites
Now that all the configuration is sorted, we can look at making the shared components available in each site. A good example which we'll use is the SEO Meta Data components. I want to have these be listed as "components" within the Shared App and just register these to be used within each individual site. So let's look at how to do this. We're going to take a convention based approach here and say that anything which lives in the standard JSS "components" directory in the shared app will be auto-added to each individual App to make life easy. For our example we're going to look at the configuration for just one of the sites (Visit) which will combine components from Visit and from Shared.
In the Shared Site we have a set of components in our Feature layer including our SEO Feature components.
In the Visit Site we have just one Sample component named "VisitContentBlock"
In order for each Site to be able to use the site-specific components as well as the shared components we need them to be available in the
ComponentFactory.ts output when we run a build. By default when you run a build using the Sitecore Starter App it targets just the current site's
/src/components/* items and writes these out to the
ComponentFactory.ts file. which will look like this:
As you might have guessed, we now need to also add the shared site items to make them available. We can achieve this by adding some additional logic into the
Lets look at the changes we can make (left) to the original file (right). The important things to note here are that when this file is loading the files in, the file paths need to be relative to the
generate-component-factory.ts file in order for the files to be found correctly. However, when the final
componentFactory.ts file is generated, the paths to the same files must be relative to the output file's location (remember this script is responsible for generating and writing the contents of another file). This is why you'll see we are replacing the component path value string to make the path correct for the output file AFTER we've loaded all the files in with
comp.path = comp.path.replace('../', '../../../');
Let's take a look at our handiwork... Here we can see that the
componentFactory.ts file contains the VisitContentBlock as well as the shared metadata components. Great success!