CMS, PHP

Create a Grav CMS cookie consent plugin using PHP

Grav CMS is my new favourite of lightweight CMS. And it has all the tools to make creating a plugin look easy.

Create a Grav CMS cookie consent plugin using PHP

Grav CMS is such a lightweight and convenient content management system for building your own blog or portfolio page. It offers freedom to beginner and intermediate+ backend engineers to make adjustments according to their needs.

As a flat filesystem based CMS building your own theme or plugin is unsurprisingly simple. In this article I'm gonna share my experiences in building your first Grav CMS plugin.

Prequisites

What you need for building your own Grav CMS plugin, is of course an up-and-running Grav CMS setup. I am fully satisfied with a buildup using docker and docker-compose and a minimalistic docker-compose.yaml configuration file.

---
services:
  grav:
    image: lscr.io/linuxserver/grav:latest
    container_name: grav
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
    volumes:
      - $PWD/grav/config:/config
    ports:
      - 8090:80
    restart: unless-stopped

Running docker-compose up -d exposes the desired services on port 8090 of your host system and the admin panel is accessible via localhost:8090/admin. After completing the basic setup, you are almost good to go.

Grav CMS provides its own package manager, which makes it easier to manage new themes or plugins. You gain access by punching in docker exec -it -w /app/www/public grav bin/gpm.

In future references I won't be displaying the full docker command anymore.

According to the great Grav CMS docs, you'll need the DevTools plugin to start making plugins on your own. Install the DevTools plugin by simply running bin/gpm install devtools.

Create a base plugin

Initialize a basic plugin by executing bin/plugin devtools new-plugin, and entering the required information afterwards.

Enter Plugin Name: CookieConsent v3
Enter Plugin Description: cookieConsentv3
Enter Developer Name: <yourname>
Enter Developer Email: <youremail>

SUCCESS plugin cookieconsent-v3 -> Created Successfully

Path: /www/user/plugins/cookieconsent-v3

Make sure to run `composer update` to initialize the autoloader

Congratulations! Your just created a base plugin.

But pay attention to the last stack of information: You need to run 'composer update' inside your plugin folder for your Grav CMS service to be able to run again without getting an error.

Thereafter access your plugin folder, to get a glimpse of the files created.

cd grav/config/www/user/plugins/cookieconsent-v3 && ls -l

---
-rw-r--r-- 1 root root  949 Aug  9 13:26 blueprints.yaml
-rw-r--r-- 1 root root   64 Aug  9 13:26 CHANGELOG.md
drwxr-xr-x 2 root root 4096 Aug  9 13:26 classes
-rw-r--r-- 1 root root  542 Aug  9 13:26 composer.json
-rw-r--r-- 1 root root  126 Aug  9 13:26 languages.yaml
-rw-r--r-- 1 root root 1076 Aug  9 13:26 LICENSE
-rw-r--r-- 1 root root 2617 Aug  9 13:26 README.md
-rw-r--r-- 1 root root 1483 Aug  9 13:26 cookieconsent-v3.php
-rw-r--r-- 1 root root  109 Aug  9 13:26 cookieconsent-v3.yaml

The files of interest do have the endings *.php and *.yaml.

Preparing your plugin for cookieConsent

To be honest: I didn't want to create a cookieConsent JavaScript library on my own, because it would completely break the mold, while there are great open source options available on the market. My goal was to aim for a library which was easy to setup with a bit of customization options at hand.

I picked the CookieConsent v3 library maintained by orestbida. A big thanks to him 👏 and the other contributors for providing this library!

01. Setting up plugin options

The *.yaml file has the purpose to provide your plugin the necessary options to decide what it should do in different scenarios.

enabled: true # enables the plugin
overwrite_languages: false # allows use of custom texts
cdn:
  enabled: true # enables external cdn links
theme: # allows cookieConsentv3 theming 
  darkmode: false
  layout:
    type: box
    variant: inline
  position:
    x: right
    y: bottom
  layout_variant: inline
links:
  legal: # legal links shown in the first cookieConsentv3 popup
    imprint: /legal/imprint
    privacy: /legal/privacy-policy

Let's have a closer look at the options I set up here. Enabling your plugin is mandatory for your plugin to be processed and called. Setting this to 'false' in this file or via admin panel will deactivate it.

The cdn option gives the user the freedom to decide if to fetch the library locally, or use the official content delivery network link to provide all the external files needed.

02. Injecting the JavaScript library into the grav CMS plugin

The plugin's PHP base of operations is of course the *.php file named after your plugin. That's it! Isn't this great?!

The onPluginsInitialized() parent event hook as part of your plugin class helps us to define which further functions to bootstrap from here. We'll supply two functions to both setup an assets folder and provide the necessary library files.

// ...
class CookieConsentV3Plugin extends Plugin
{

/**
 * Initialize the plugin
 */
public function onPluginsInitialized(): void
{
    if ($this->isAdmin()) {
        return;
    }

    $this->enable([
        'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], // initalize the 'templates' folder
        'onTwigSiteVariables' => ['onTwigSiteVariables', 0]  // inject the external library
    ]); 
}

  // ...
}

We use the onTwigSiteVariables event hook, because we need to access some twig full site variables. A list of all available Grav CMS event hooks can be fetched from here.

To support cdn and locally injected library files as an option, we need to create an assets folder containing the optional local library files.

cd grav/config/www/user/plugins/cookieconsent-v3 && mkdir assets && cd assets

## Following commands will fetch the libraries
## of course you could download them manually and put them in the 'assets' folder
wget cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.0.1/dist/cookieconsent.css
wget cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.0.1/dist/cookieconsent.umd.js

Great, we stored the library files locally. Now we need to tell the plugin to use them if granted.

// ...
class CookieConsentV3Plugin extends Plugin
{

  /**
   * tell the plugin to search in 'templates' folder for additional files
   */
  public function onTwigTemplatePaths()
  {
    $this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
  }

  /**
   * loads library and executes main JavaScript
   */
  public function onTwigSiteVariables()
  {
      $twig = $this->grav['twig'];

      $cdnEnabled = $this->config->get('plugins.cookie-consent-v3.cdn.enabled');
      $useCustomTexts = $this->config->get('plugins.cookie-consent-v3.overwrite_languages');

      if ($cdnEnabled) {
          $this->grav['assets']->addCss("//cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.0.1/dist/cookieconsent.css");
          $this->grav['assets']->addJsModule("//cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.0.1/dist/cookieconsent.umd.js");
      }
      else {
          $this->grav['assets']->addCss("plugin://cookie-consent-v3/assets/cookieconsent.min.css");
          $this->grav['assets']->addJsModule("plugin://cookie-consent-v3/assets/cookieconsent.umd.js");
      }

      /**
       * add CookieConsent v3 executable JavaScript
       */
      $this->grav['assets']->addInlineJsModule($twig->twig->render('partials/umd.js.twig', [
          'content' => $useCustomTexts?$this->config->get('plugins.cookie-consent-v3.content'):[],
          'links' => $this->config->get('plugins.cookie-consent-v3.links'),
          'theme' => $this->config->get('plugins.cookie-consent-v3.theme')
      ]));
  }
}

The plugin interpets the cdn.enabled variable in the cookie-consent-v3.yaml, and either fetches the lib files externally, or alternatively uses the lib files stored in the assets folder.

Finally we create a JavaScript file umd.js.twig which serves as library executioner.

## create an empty umd.js.twig file in templates/partials folder
cd grav/config/www/user/plugins/cookieconsent-v3 && \ 
  mkdir templates && cd templates && \
   mkdir partials && touch partials/umd.js.twig
CookieConsent.run({

    categories: {
        necessary: {
            enabled: true,  // this category is enabled by default
            readOnly: true  // this category cannot be disabled
        },
        analytics: {
            readOnly: false,
            autoClear: {
                cookies: [
                    {
                        name: /^(_ga)/      //regex
                    },
                    {
                        name: '_gid'        //string
                    },
                    {
                        name: 'blockMatomo'        //string
                    }
                ]
            }
        }
    },

    guiOptions: {
        consentModal: {
            layout: '{% if theme.layout.type %}{{ theme.layout.type }}{% else %}box{% endif %} {% if theme.layout.variant %}{{ theme.layout.variant }}{% else %}inline{% endif %}',
            position: '{% if theme.position.y %}{{ theme.position.y }}{% else %}bottom{% endif %} {% if theme.position.x %}{{ theme.position.x }}{% else %}right{% endif %}',
            flipButtons: false,
            equalWeightButtons: true
        }
    },

    language: {
        default: 'en',
        translations: {
            en: {
                consentModal: {
                    title: '{% if content.title %}{{ content.title }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.CONTENT_TITLE'|t }}{% endif %}',
                    description: '{% if content.msg %}{{ content.msg }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.CONTENT_MSG'|t }}{% endif %}',
                    acceptAllBtn: '{% if content.accept %}{{ content.accept }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.CONTENT_ACCEPT_ALL'|t }}{% endif %}',
                    acceptNecessaryBtn: '{% if content.reject %}{{ content.reject }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.CONTENT_REJECT_ALL'|t }}{% endif %}',
                    showPreferencesBtn: '{% if content.customize %}{{ content.customize }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.CONTENT_CUSTOMIZE'|t }}{% endif %}',
                    footer: `
                        <a href="{% if links.legal.imprint|t %}{{ grav.language.active }}{{ links.legal.imprint }}{% else %}/yourcustomimprintlink{% endif %}">
                            {% if content.imprint_text %}{{ content.imprint_text }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.IMPRINT_TITLE'|t }}{% endif %}
                        </a>
                        <a href="{% if links.legal.privacy %}{{ grav.language.active }}{{ links.legal.privacy }}{% else %}/yourcustomprivacypolicylink{% endif %}">
                            {% if content.privacy_text %}{{ content.privacy_text }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.PRIVACY_TITLE'|t }}{% endif %}
                        </a>
                    `
                },
                preferencesModal: {
                    title: '{% if content.manage_preferences_title %}{{ content.manage_preferences_title }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.MANAGE_PREFERENCES_TITLE'|t }}{% endif %}',
                    acceptAllBtn: '{% if content.accept %}{{ content.accept }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.CONTENT_ACCEPT_ALL'|t }}{% endif %}',
                    acceptNecessaryBtn: '{% if content.accept_necessary %}{{ content.accept_necessary }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.CONTENT_ACCEPT_NECESSARY'|t }}{% endif %}',
                    savePreferencesBtn: '{% if content.accept_selection %}{{ content.accept_selection }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.CONTENT_ACCEPT_SELECTION'|t }}{% endif %}',
                    closeIconLabel: 'Close modal',
                    sections: [
                        {
                            title: '{% if content.manage_preferences_subtitle %}{{ content.manage_preferences_subtitle }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.MANAGE_PREFERENCES_SUBTITLE'|t }}{% endif %}',
                            description: '{% if content.manage_preferences_msg %}{{ content.manage_preferences_msg }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.MANAGE_PREFERENCES_MSG'|t }}{% endif %}',
                        },
                        {
                            title: '{% if content.manage_preferences_strictly_necessary %}{{ content.manage_preferences_strictly_necessary }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.MANAGE_PREFERENCES_STRICTLY_NECESSARY'|t }}{% endif %}',
                            description: '{% if content.manage_preferences_strictly_necessary_desc %}{{ content.manage_preferences_strictly_necessary_desc }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.MANAGE_PREFERENCES_STRICTLY_NECESSARY_DESC'|t }}{% endif %}',

                            //this field will generate a toggle linked to the 'necessary' category
                            linkedCategory: 'necessary'
                        },
                        {
                            title: '{% if content.manage_preferences_custom %}{{ content.manage_preferences_custom }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.MANAGE_PREFERENCES_CUSTOM'|t }}{% endif %}',
                            description: '{% if content.manage_preferences_custom_desc %}{{ content.manage_preferences_custom_desc }}{% else %}{{ 'PLUGINS.COOKIE_CONSENT_V3.MANAGE_PREFERENCES_CUSTOM_DESC'|t }}{% endif %}',
                            linkedCategory: 'analytics'
                        }
                    ]
                }
            }
        }
    }
});

{% if theme.darkmode %}
(function(){
    document.documentElement.classList.add('cc--darkmode');
})();
{% endif %}

The JavaScript CookieConsent library offers multilanguage support, which is crucial for this type of plugin.

But we'd rather use the Grav CMS features to enable multilanguage support instead of wrapping up different languages in the Javascript object. The languages.yaml can be filled with the necessary internationalization information, and in my case provides support for two languages at this very moment.

en:
  PLUGINS:
    COOKIE_CONSENT_V3:
      IMPRINT_TITLE: 'Imprint'
      PRIVACY_TITLE: 'Privacy policy'
      CONTENT_TITLE: 'We value your privacy'
      CONTENT_MSG: 'We use cookies to enhance your browsing experience, serve personalized ads or content, and analyze our traffic. By clicking "Accept all", you consent to our use of cookies.'
      CONTENT_ACCEPT_ALL: 'Accept all'
      CONTENT_ACCEPT_NECESSARY: 'Accept necessary'
      CONTENT_ACCEPT_SELECTION: 'Accept current selection'
      CONTENT_REJECT_ALL: 'Reject all'
      CONTENT_CUSTOMIZE: 'Customize'
      MANAGE_PREFERENCES_TITLE: 'Manage cookie preferences'
      MANAGE_PREFERENCES_SUBTITLE: 'Manage your cookies'
      MANAGE_PREFERENCES_MSG: 'We use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below. The cookies that are categorized as "Necessary" are stored on your browser as they are essential for enabling the basic functionalities of the site. We also use third-party cookies that help us analyze how you use this website, store your preferences, and provide the content and advertisements that are relevant to you. These cookies will only be stored in your browser with your prior consent. You can choose to enable or disable some or all of these cookies but disabling some of them may affect your browsing experience.'
      MANAGE_PREFERENCES_STRICTLY_NECESSARY: 'Strictly necessary cookies'
      MANAGE_PREFERENCES_STRICTLY_NECESSARY_DESC: 'Necessary cookies are required to enable the basic features of this site, such as providing secure log-in or adjusting your consent preferences. These cookies do not store any personally identifiable data.'
      MANAGE_PREFERENCES_CUSTOM: 'Performance & Analytics'
      MANAGE_PREFERENCES_CUSTOM_DESC: 'Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics such as the number of visitors, bounce rate, traffic source, etc.'
de:
  PLUGINS:
    COOKIE_CONSENT_V3:
      IMPRINT_TITLE: 'Impressum'
      PRIVACY_TITLE: 'Datenschutzerklärung'
      CONTENT_TITLE: 'Wir schätzen Ihre Privatsphäre'
      CONTENT_MSG: 'Wir verwenden Cookies, um Ihr Browsing-Erlebnis zu verbessern, personalisierte Werbung oder Inhalte bereitzustellen und unseren Datenverkehr zu analysieren. Wenn Sie auf "Alle akzeptieren" klicken, stimmen Sie unserer Verwendung von Cookies zu.'
      CONTENT_ACCEPT_ALL: 'Alle akzeptieren'
      CONTENT_ACCEPT_NECESSARY: 'Notwendige akzeptieren'
      CONTENT_ACCEPT_SELECTION: 'Auswahl akzeptieren'
      CONTENT_REJECT_ALL: 'Alle ablehnen'
      CONTENT_CUSTOMIZE: 'Anpassen'
      MANAGE_PREFERENCES_TITLE: 'Cookie-Einstellungen verwalten'
      MANAGE_PREFERENCES_SUBTITLE: 'Verwalten Sie Ihre Cookies'
      MANAGE_PREFERENCES_MSG: 'Wir verwenden Cookies, um Ihnen die Navigation zu erleichtern und bestimmte Funktionen auszuführen. Detaillierte Informationen über alle Cookies finden Sie unter jeder Einwilligungskategorie unten. Die Cookies, die als "notwendig" eingestuft sind, werden in Ihrem Browser gespeichert, da sie für die grundlegenden Funktionen der Website unerlässlich sind. Wir verwenden auch Cookies von Drittanbietern, die uns dabei helfen, zu analysieren, wie Sie diese Website nutzen, Ihre Präferenzen zu speichern und Ihnen die für Sie relevanten Inhalte und Anzeigen zu liefern. Diese Cookies werden nur mit Ihrer vorherigen Zustimmung in Ihrem Browser gespeichert. Sie haben die Möglichkeit, einige oder alle dieser Cookies zu aktivieren oder zu deaktivieren, aber die Deaktivierung einiger dieser Cookies kann Ihr Surferlebnis beeinträchtigen.'
      MANAGE_PREFERENCES_STRICTLY_NECESSARY: 'Streng notwendige Cookies'
      MANAGE_PREFERENCES_STRICTLY_NECESSARY_DESC: 'Notwendige Cookies sind erforderlich, um die grundlegenden Funktionen dieser Website zu ermöglichen, wie z. B. das sichere Einloggen oder die Anpassung Ihrer Einwilligungseinstellungen. Diese Cookies speichern keine persönlich identifizierbaren Daten.'
      MANAGE_PREFERENCES_CUSTOM: 'Leistung und Analytik'
      MANAGE_PREFERENCES_CUSTOM_DESC: 'Analytische Cookies werden verwendet, um zu verstehen, wie Besucher mit der Website interagieren. Diese Cookies helfen dabei, Informationen über Metriken wie die Anzahl der Besucher, Absprungrate, Verkehrsquelle usw. zu liefern.'

To be continued...