Skip to content

How to Build a Grav Plugin: Part 8

Making improvements after a review

  • code
  • tutorial
  • series

This was actually written in May of 2022, but I had just started a new position and never got around to publishing. Same goes for the JS and canvas rendition of the Malevich painting

One of the great benefits of open-source work and the surrounding communities is the opportunity to learn from people who are better at things than you. GitHub user @pamtbaau, a moderator and very active user of the Grav discourse forum, gave me some good suggestions after my pull request.

This post covers responding to and implementing those suggestions. (Don’t forget to update the documentation: There will a couple of changes to make in after each change to the plugin logic, not covered in the post. Similarly, don’t forget to update the CHANGELOG and the plugin metadata.)

Use shortcode with parameters

Back in the second post in this series, I elected to give each language its own shortcode because I liked the brevity of the approach. @pamtbaau’s reasons for using different approach are:

  • Only load shortcode for requested language(s), instead of 349. It saves a bit of performance.
  • No pollution of the shortcodes namespace with 349 extra names.
  • No need to search for 349 languages in Markdown while they are not used anyway.

Yep. (I didn’t experience any performance issues, but that’s immaterial. Good points all around.) I’ll use a hybrid of my original sketch, and @pamtbaau’s suggested syntax, preferring my hl shortcode with their code parameter (instead of my valid, but probably too opaque, @) in the inline form, yielding:

This has the benefit of simplifying the code, too. In HighlightPhpShortcode.php, the render method stays as-is. Updates to the init method are pretty straightforward:

With the above, and using a ternary instead of the if block, we end up with the following:

    public function init()
        $rawHandlers = $this->shortcode->getRawHandlers();

        $rawHandlers->add('hl', function (ShortcodeInterface $sc) {
            $lang = $sc->getBbCode();
            $content = $sc->getContent();
            $isInline = is_null($content);
            $code = $isInline ? $sc->getParameter('code') : $content;
            $code = trim($code);
            return $this->render($lang, $code, $isInline);

Use a fixed location for custom styles

I don’t see a real added value for a customisable location.

Now that it’s been pointed out, neither do I!

My suggestion would be to use folder: /user/data/highlight-php/. I prefer the /user/data folder to not pollute the user folder. Drop the folder property from the config.

Good point. What’s more, /user/data is the recommended location for plugins to store data. I missed that, and took my cue from the shortcode-core documentation. Whoops.I submitted pull requests on the grav-plugin-shortcode-core and grav-plugin-file-content repositories to reflect this. There’s even a dedicated stream for that folder, user-data://; I’ll use that.

The changes here are in the highlight-php.php and highlight-php.yaml files. In the latter, it’s just a matter of deleting a line.

 enabled: true
 theme: default
-custom_styles: highlight-php-styles

In the PHP, there’s scarcely more to it: one deletion, and updating the path in onPluginsInitialized:

-$customStylesDirName = $this->config->get('plugins.highlight-php.custom_styles');
 $locator = Grav::instance()['locator'];
-$userCustomDirPath = $locator->findResource('user://') . '/' . 'custom' . '/' . $customStylesDirName; 
+$userCustomDirPath = $locator->findResource('user-data://') . '/' . 'highlight-php';
 if (!($locator->findResource($userCustomDirPath))) {

There’s more related to this change to come in the new handling for custom styles in the next section.

Add a property in a page to enable/disable the plugin

I presume a page using snippets will be the exceptions instead of the rule. Test for it during onPageInitialized.

In my case, I’d just as soon enable it globally and be done with it. Per-page configuration is a good idea, though, so I built it.

This necessitated some different event subscriptions and a careful review of the Grav lifecycle. I also elected to add a new blueprint to the plugin (plugin://blueprints/default.yaml) that extends the default to allow for toggling the plugin for a given page via the admin panel. I added a new section to the bottom of the default ‘Options’ tab, along with some explanatory text; I elected not to allow for per-page syntax highlighting options. (I.e., in my plugin, the user selects a syntax highlighting colour scheme for the site, and can only enable/disable syntax highlighting per page.)

Use two config properties for styles

style for build-in styles and customStyle for a custom style. Add a None value to both.

  • User can now
    • Select a build-in style or None
    • Select a custom style or None
    • Select both build-in and custom style to override the build-in style.
  • Load each style (if value !== ‘None’) as separate asset with customStyle as last.

This was the biggest change, requiring edits throughout the plugin. blueprints.yaml and highlight.php both get substantial changes. Here’s a high-level summary of the changes.If you’ve made it this far and are making your own plugin, I assume you’ll be able to use git and/or GitHub well enough to look at the details yourself. Here’s a direct link to the commit where this took place.


Rejigged fields to handle the displaying the builtin and custom themes as options in dropdown fields. Each dropdown uses a data-options@ function call; this function in turn necessitated some refactoring of highlight-php.php. Also added a new field to handle the site-wide default asset loading strategy for the plugin.


Here’s the meat of the revision. Added some abstraction to facilitate the two different directory listings in the dropdown fields. Some additional logic was required to handle the four possibilities with per-page settings enabled:

Wrapping up

That’s a final wrap on this plugin (aside from vendor updates or bugfixes, although none have been reported). I’m grateful to the user who suggested the improvements described here. For me, the experience of developing and releasing this plugin has been a treat: taking part in a globally distributed, like-minded community is fun. I’ve learned some things along the way, and I gather from the handful of stars on the GitHub repository that this is useful to at least a couple of other people. That’s nice, too.

Next steps for me: