Skip to content

Cookie consent across subdomains with Material for MkDocs

This post looks at why you'd want shared cookie consent across your subdomains, then outlines the key features to be aware of when setting this up with Material for MkDocs, and provides a code sample to help you get started.

The scenario

A common pattern for company websites is to have multiple subdomains, all built as different sites, but existing for the user as a more or less continuous experience. For example:

  • mydomain.com: the main product website. Built with anything from a static site generator to WordPress to . . .
  • docs.mydomain.com: the documentation site. Built with Material for MkDocs (of course)
  • community.mydomain.com: the product forum. Perhaps powered by something like Disqus.
  • app.mydomain.com: the product itself. Again, could be built with anything.

Ideally, these should have a fairly unified feel. One way to support this is to ensure users only have to accept cookies once. In other words, if a user clicks Accept (or Decline) on the main website, they shouldn't get prompted again when they navigate to the docs.

However, behind the scenes, the main website and the docs site are entirely different sites: different tooling, code bases, deployments, and so on. This can make setting up shared cookie consent tricky.

I was recently given cookie code from the main website, and told to add it to the docs. The main website stored user consent in a cookie named companyname-consent. This seemed simple enough: I just needed to set and read that same cookie on the docs site. In practice, there were a couple of gotchas.

This was the first thing that caught me out when investigating this. Material remembers user choice in local storage, in an object named __consent. So instead of reading or setting a cookie, you need to do:

1
2
3
4
__md_get("__consent");
// Set consent by providing the cookie name
// true = user consented to this cookie
__md_set("__consent", {"cookieName": true})

Gotcha 2: All Material for MkDocs code runs before any extra JavaScript

MkDocs supports users adding their own JavaScript. You can set extra_js in your mkdocs.yml to tell MkDocs where to find your code (more info in the MkDocs documentation). In my first attempt, I used this to add my functionality.

However, the theme's JavaScript runs before the extra JS. This means that Material for MkDocs has already checked __consent and decided whether or not to display the docs cookie banner, before the extra JS has a chance to run and check for other cookies. This led to the banner still displaying when users had already accepted cookies on the main website.

Luckily, MkDocs also supports a system of theme overrides, and Material for MkDocs is carefully designed with this type of theme extension in mind.

So, instead of using extra JS, I overrode the theme's consent JavaScript:

  1. Set up an overrides directory (more info).
  2. Add partials/javascripts/consent.html
  3. Copy in the contents of Material consent.html.
  4. Modify as needed, ensuring my script preceded the theme's.

Thanks to Squidfunk for pointing out this was the approach I needed.

The code

You'll probably need to modify the following code to suit your own setup. It assumes cookie consent is stored in a cookie on the main website. This is just an example to get you started.

Add the following at the start of your partials/javascripts/consent.html file in your overrides directory:

<!-- START CUSTOM CODE -->
<!-- 
Company Name's cookie consent handling
Ensures cookie consent is shared between docs
 and other .companyname.io properties
-->
<script>
// If the user accepts cookies in the docs, set the companyname-consent cookie
// This means if they then go to the main website, they won't be prompted again
let docsConsent = __md_get("__consent")
let d = new Date();
d.setTime(d.getTime() + 5 * 24 * 60 * 60 * 1000);
let companyNameCookie = {'consent': true};
// When user clicks Accept on the consent form, page reloads and this sets
// DEBUG TIP: if it breaks, first thing to do is check the page reload is still happening
if (docsConsent && docsConsent.analytics === true) {
  document.cookie = `companyname-consent=${JSON.stringify(companyNameCookie)};expires=${d.toUTCString()};path=/;domain=.companyname.io`;
}

// If the user already has the companyname-consent cookie, accept cookies in docs as well
let getCompanyNameCookie = getCookie("companyname-consent");
if(getCompanyNameCookie) {
  var parsedCompanyNameCookie = JSON.parse(getCompanyNameCookie);
}
if(parsedCompanyNameCookie && parsedCompanyNameCookie.consent === true) {
    // Note that Material uses local storage to remember cookie settings
    __md_set("__consent", {"analytics": true});
}

// Function to help with extracting cookies by name
// From https://www.w3schools.com/js/js_cookies.asp
function getCookie(cname) {
  let name = cname + "=";
  let decodedCookie = decodeURIComponent(document.cookie);
  let ca = decodedCookie.split(';');
  for(let i = 0; i <ca.length; i++) {
    let c = ca[i];
    while (c.charAt(0) == ' ') {
      c = c.substring(1);
    }
    if (c.indexOf(name) == 0) {
      return c.substring(name.length, c.length);
    }
  }
  return "";
}
</script>
<!-- END CUSTOM CODE -->

Wrap up

As Material's creator tactfully noted, this is a rather "exotic" case. It's one of the joys of MkDocs, and the Material theme, that it's so easily extendable and overrideable when you need to do something that isn't supported by default.

Hopefully this blog post will help you get started with this use case (or at least save me some time next time I forget the script execution order 🙄).