Measurable Success «10 Checkpoints for Future-Proof Enterprise CMS» White Paper
Measurable Success - White Paper

Setting up Multiple Websites

New in 1.18.0

Setting up Multiple Websites

Scrivito supports the delivery of multiple websites, enabling enterprises to manage the content for their various web outlets in a single Scrivito CMS. Whether you are running country sites, a customer portal, or websites dedicated to knowledge transfer or marketing purposes – not only can all of them be served content-wise by the same CMS, but they can also be edited individually and comfortably using the editing interface.

Technically, Scrivito’s multi-site approach is straightforward as well as flexible in the sense that websites can be added by means of a few lines of code (optionally even by editors), and for working on their individual content, switching between sites is a matter of a single click.

Key features and benefits

  • As indicated above, managing several websites using a single Scrivito CMS frees you from having to duplicate content like images common to two or more sites. Also, the fact that all application code is part of a single application obviates the need to maintain overarching application code separately just to be able to reuse it in the various site-specific apps.
  • Migrating from a single-site to a multi-site setup comes down to extending the configuration of the Scrivito-based app by two callbacks allowing the SDK to determine a site’s ID and site-specific URLs to perform the proper routing.
  • All CMS objects, i.e. all pages and assets, are associated with either exactly one of the sites, or with all sites. This is accomplished by means of a built-in attribute, siteId.
  • All of the operations the web application performs (such as rendering navigations) refer to the currently active site. The active site is derived from the site the currently displayed page belongs to. Permalinks are resolved in the context of the current website, and the same applies to searches. There’s also an API that lets you search across all sites.
  • The editing interface is site-aware. A site switcher is available wherever appropriate, for example on the hierarchy panel. In the Content Browser, non-site-specific, i.e. global content is marked with a label and can thus be clearly distinguished from the content part of the current site. You can link, embed, as well as copy or move content across sites.
  • Workflows support you as needed with letting editors focus on a specific site when authoring or changing content in working copies. This way, inadvertent changes to website content can be avoided.

Basic website setup

As mentioned earlier, with multisite configured, every piece of content in a Scrivito CMS is either global and thus available on all sites, or it is part of exactly one specific site. The website assignment of a CMS object is specified in its siteId attribute. In a single-site setup, all CMS objects belong to the same website, and, consequently, the value of their siteId attribute is default.

By defining two callbacks, siteforUrl and baseUrlForSite, and thereby providing a mapping between a siteId and a URL (and vice versa), a Scrivito-based application becomes multisite aware. These callbacks enable the application to determine the site being displayed and to generate proper URLs. Provide the callbacks in your call to Scrivito.configure, and make sure to provide both of them to not cause an error.

Here is an example of how siteForUrl and baseUrlForSite could be implemented. It is assumed that a dedicated domain exists for each site:

config/scrivito.js  Example for individual domains per site
Scrivito.configure({
  baseUrlForSite(siteId) {
    switch (siteId) {
      case "international":
        return "https://example.com";
      case "german":
        return "https://example.de";
    }
  },

  siteForUrl(url) {
    switch (new URL(url).hostname) {
      case "example.com":
        return {
          siteId: "international",
          baseUrl: "https://example.com",
        };
      case "example.de":
        return {
          siteId: "german",
          baseUrl: "https://example.de",
        };
    }
  },
  // other keys …
});

Scrivito calls baseUrlForSite to determine the starting point of the site with the given ID and must return an absolute URL.

siteForUrl is called to determine the ID of the site to which a given absolute URL belongs. If the site has the domain all to itself (like in the above example), it is sufficient for siteForUrl to return this ID. Otherwise, if two or more sites share a domain (like in the example below), siteForUrl should return the site’s ID as well as its base URL.

config/scrivito.js Example for path-based sites
function baseUrlForSite(siteId) {
  switch (siteId) {
    case "international":
      return `${window.location.origin}/intl`;
    case "german":
      return `${window.location.origin}/de`;
  }
}

function siteForUrl(url) {
  let siteId;
  switch (new URL(url).pathname.split("/")[1]) {
    case "intl":
      siteId = "international";
      return { siteId, baseUrl: baseUrlForSite(siteId) };
    case "de":
      siteId = "german";
      return { siteId, baseUrl: baseUrlForSite(siteId) };
  }
}

Scrivito.configure({
  siteForUrl,
  baseUrlForSite,
  // other keys …
});

In case the callbacks cannot handle the incoming siteId or url, they may return undefined to indicate this. If baseUrlForSite returns undefined, Scrivito takes this as “website not found”, whereas undefined as a result of calling siteForUrl causes Scrivito to not take any further action since the passed-in URL obviously doesn’t belong to any of the websites.

Mapping sites and URLs dynamically

In the above examples, the site IDs and base URLs are hard coded, which is a straightforward approach to configuring multisite for just a couple of sites. However, in cases where sites need to be defined spontaneously, having to change the configuration and to redeploy the app is not an option. Instead, you can include the data needed to determine site IDs and base URLs in your content by means of an attribute like hostname, subdomain, or pathPrefix, for example.

In the following snippet, it is assumed that all CMS objects of the Homepage class are equipped with a hostname attribute whose respective value represents a unique site.

config/scrivito.js Example for dynamic site creation
function baseUrlForSite(siteId) {
  const siteRoot = Scrivito.onSite(siteId).root();
  const hostname = siteRoot?.get("hostname");
  if (hostname) return `https://${hostname}`;
}

Scrivito.configure({
  baseUrlForSite,
  siteForUrl(url) {
    const siteId = Scrivito.Obj.onAllSites()
      .where("hostname", "equals", new URL(url).hostname)
      .first()
      ?.siteId();

    if (siteId) {
      return { siteId, baseUrl: baseUrlForSite(siteId) };
    }
  },
  // other keys …
});

You might have noticed from the example above that the mapping functions have access to CMS content. However, in them, no APIs must be used that depend on the mapping to determine the current site or the current page. Scrivito.Obj.where, for example, searches the current site, and calling this method in the mapping would therefore cause a recursion. Using Scrivito.Obj.onAllSites().where instead is the way to go.

Combined with a wildcard DNS entry (and a corresponding SSL certificate), the above approach would allow editors to bring new websites online without requiring assistance from a developer or an administrator. With just a single domain at your disposal, you could let editors specify a path prefix instead of a hostname, and adjust the callbacks accordingly.

Providing the homepages for your sites

Assuming that your website infrastructure is static, i.e. no domain names, URL paths, etc., specified by editors need to be taken into account, you can create the sites supported by your callbacks with a single command in the browser console:

Scrivito.getClass("Homepage").create({_siteId: "international", _path: "/", title: "International Homepage"})

Provided that a suitable working copy has been selected, the above command creates a CMS object of the “Homepage” class, sets its path to / and its title provisionally to what the homepage is meant to cover. Note that all homepages must have / as their path for editors to be able to create subpages, i.e. build and work with the site’s page hierarchy. Also, the Obj.root method identifies the homepage of a website by this path.

After executing this command for the “International Homepage” and the “German Homepage”, the hierarchy panel on the sidebar includes these sites.

Note that adding sites in the course of migrating from a single-site Scrivito CMS to a multi-site setup doesn’t change the default site assignment of your existing content. If you would like to continue to use this content, you can either adapt your callbacks accordingly, or change the siteId of the CMS objects concerned, or both. As an example, in order to shift all your content living on the default site to the english site, execute the following in the browser console (again, after selecting the working copy to use):

objs = await Scrivito.load(() => Scrivito.Obj.onSite("default").all().toArray())
objs.forEach(obj => obj.update({ _siteId: "english" }))

What’s left?

  • As you might have guessed, any piece of data the mapping callbacks have access to can be used to determine a site ID or a URL. You could, for example, change the hostname or path prefix depending on the value of a specific environment variable.
  • Note that the routingBasePath and origin configuration parameters of Scrivito.configure are intended for single-site applications only. They are not needed if a site mapping is provided (specifying the site mapping as well as origin or routingBasePath is considered an error).