3 Multi-Site Implementation Pains (And How dotCMS Solves Them)

3 Multi-Site Implementation Pains (And How dotCMS Solves Them)
Author image

Ian Cooper

Manger, Professional Services

Share this article on:

I’m one of the lucky ones at dotCMS. Every day I get to talk to our customers implementing their sites and apps and see the novel ways those developers are using our platform. That’s often a big reason why developers come to us at the dot, because the challenges they’re trying to solve are not a matter of downloading some stock theme (insert joke about WordPress here), but require some real thought on how to accomplish in a user friendly, scalable, fashion. 

So in avoiding sharing some tired cliché on how every customer has different challenges (biting my tongue, I promise), I think it’s probably best to share what is similar for the good folks implementing multiple sites and apps on our platform. After all, that’s probably what’s on your mind too!  

1. Multi-Site Taxonomy

Even our smallest customers these days don’t come in with a vision of a single site or web app. In fact, they rarely stop the use of a CMS at just one department, business unit, or user group who needs to make updates! So how does a developer in dotCMS build for the here and now, while also giving them flexibility for the future?

Let’s start at the Core:

dotCMS uses a concept of hosts where you can create and store content and code. While these sites can be (and most often are) used to serve content to a dotCMS native or custom front end, For maintainable  multi-site and multi-channel implementations we suggest creating a Core host. 

This Core host functions not as a property that is DNS-addressable, but as a central place for you to store your core codebase so it can be easily accessible across all your dotCMS hosts. This means if you have many different properties, but still rely on similar core code, it can easily be leveraged across multiple hosts for ease of maintenance and consistency in what normally would be extremely complicated multi-site implementations. 

We often use our good friend, the Velocity parser for this:

#dotParse(String filename[, Integer ttl])

With a good Core host setup and liberal use of #dotParse, we are well on our way towards simplifying what sometimes end up as nightmares of siloed code and content across our digital ecosystem.

For those that are wondering how the Core host can fit into your CI/CD pipeline, check out our docs on the dotCLI tool. Developers who want a click glance on how to set it up, don’t miss the cheat sheet to quick start!

dotCLI cheat sheet preview.png

2. Content Reuse (WTH Example) 

Create once, use everywhere! While I shared above one way to do that with code, dotCMS gives the developer and content architect flexibility from a content reuse perspective as well! 

Since dotCMS stores content on a host, not on a page, you can easily reuse it just by leveraging basic Velocity viewtools or even simply by adding it to another page

Some of our customers however, wanted to go further. World Travel Holdings, for example, has a B2B business model where they support thousands of travel agents’ sites on a single dotCMS instance. While each of those sites has large sections of bespoke content, there are many pages and content items that need to be uniform across all of those sites.

Rather than maintaining those pages across thousands of properties, the development team was able to create these shared pages on their Core host, then utilize a web interceptor OSGi Plugin to share those pages and content to each of their agents properties (as a treat, here’s a preview of some of the base code leveraged for this use case):

public class ReusablePageRenderInterceptor implements WebInterceptor {
    private static final String API_CALL = Config.getStringProperty("API_CALL_PATH", "/some-path");
    private static final String PAGE_ID  = Config.getStringProperty("PAGE_ID", "ID");
    private static final String HOST_ID  = Config.getStringProperty("HOST_ID", "ID");
    private static final String HEADER_KEY  = Config.getStringProperty("HEADER_KEY", "dotheader");
    private static final String FOOTER_KEY  = Config.getStringProperty("FOOTER_KEY", "dotfooter");

    @Override
    public String[] getFilters() {
        return new String[] {
                API_CALL
        };
    }

    @Override
    public Result intercept(final HttpServletRequest request, final HttpServletResponse response) {
        final User user = WebAPILocator.getUserWebAPI().getLoggedInUser(request);
        final PageMode mode = PageMode.get(request);

        try {
            final Host host = APILocator.getHostAPI().find(HOST_ID, APILocator.systemUser(), false);
            final Identifier identifier = APILocator.getIdentifierAPI().find(PAGE_ID);
            final VersionInfo versionInfo = APILocator.getVersionableAPI().getVersionInfo(identifier.getId());
            final HTMLPageAsset page = (HTMLPageAsset) APILocator.getHTMLPageAssetAPI().findPage(versionInfo.getLiveInode(), user, mode.respectAnonPerms);

            final String pageHTML = new HTMLPageAssetRenderedBuilder()
                    .setHtmlPageAsset(page)
                    .setUser(user)
                    .setRequest(request)
                    .setResponse(response)
                    .setSite(host)
                    .setLive(true)
                    .getPageHTML(mode);

            final String header = this.getHeader (request, response, user, mode);
            final String footer = this.getFooter (request, response, user, mode);

            Logger.info(this, "HEADER: " + header);
            // Logger.info(this, "Page HTML: " + pageHTML);

            response.getWriter().write(this.interpolateHeaderAndFooter(pageHTML, header, footer));

            return Result.SKIP_NO_CHAIN;
        } catch (Exception e) {

            Logger.error(this, "ERROR: "+e.getMessage(), e);
            // return 404
        }

        return Result.NEXT;
    }

    private String interpolateHeaderAndFooter(final String pageHTML, final String header, final String footer) {

        final Map<String, Object> parametersMap = new HashMap<>();
        parametersMap.put(HEADER_KEY, header);
        parametersMap.put(FOOTER_KEY, footer);
        return StringUtils.interpolate(pageHTML, parametersMap);
    }

    private String getHeader(final HttpServletRequest request, final HttpServletResponse response,
                             final User user, final PageMode mode) {
        try {
            final String urlString = "http://" +  request.getServerName() + ":8082";
            URL url = new URL(urlString);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");

            // Step 2: Read the response HTML
            BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String inputLine;
            StringBuilder content = new StringBuilder();
            while ((inputLine = in.readLine()) != null) {
                content.append(inputLine);
            }
            in.close();
            connection.disconnect();

            Logger.info(this, "URL: "+urlString);

            // Step 3: Use JSoup to parse the HTML and extract the Header section
            Document doc = Jsoup.parse(content.toString());
            String headContent = doc.select(".template-hd").html();
            // Logger.info(this, "CONTENT: "+headContent);

            return headContent;
        } catch (Exception e) {
            Logger.error(this, "ERROR PARSING HTML: "+e.getMessage(), e);
        }

        return null;

Perhaps the coolest thing here is that in addition to enabling reuse of pages from a Core host to as many sites as desired, the plugin also maintains the appropriate URLs and folder paths, all the while being processed server side to help avoid negative SEO impact. 

3. Locales and Languages

Whether you’ve enabled it server-side, or your visitors trigger it through their web client, you can bet your app/websites are not being read in just one language. If maintaining those language versions is critical for your company though, it can become a huge effort to maintain, particularly if you have multiple sites. 

dotCMS makes this easier though with the notion of language variants. Forget about creating another site just for a different language. Instead, create your language versions so your audience can view the same content, on the same site, just in their native tongue. 

While there are a few ways to trigger this in a user session, when dealing with scale the best approach is to leverage dotCMS Rules. For example, if I wanted to trigger a redirect from French to English, I could create a rule like the below:

language redirect rule.png

dotCMS Rules essentially provide “virtual folders” to the site in this regard. While your core taxonomy may just have one language as its default, through the use of language variants and redirects through Rules, what in the past could have been many sites, are now simplified, and content is similarly further reused.

We’re Just Scratching the Surface

Like I mentioned at the outset, the best part of my job is seeing how many different ways our customers implement the dotCMS system. Because of that, while I shared a few basic challenges and how to approach them in dotCMS, this is perhaps as brief an introduction that can be done to the myriads of approaches to be taken when implementing multi-site architectures in dotCMS. So while I encourage you to take inspiration from our product and from your peers that have pushed it, I’ll be sitting in the background, looking forward to the next wrinkle and edge case that is assuredly sitting around the next corner.