My custom Grav theme

Posted

Updated

24 Jun 2019

I re-designed my blog template using a new CSS framework called Bluma, re-developed all JavaScript without jQuery and customized some plugins. This was quite a journey, so this post is going to be quite long too!

Bulma CSS Framework

I wanted to find a modern CSS framework based on Flexbox introduced with CSS3. I’ve been using Bootstrap 3 for many years, and it was time for a change. I was looking for something simpler and lighter, without jQuery.

CSS3 Flexbox has been around since about 2009, and all modern browsers support it. Some cool learning tools:

I settled on Bulma. It’s pure CSS with minimal JavaScript for elements like the NavBar.

Rather than trying to build customized Bulma CSS, I used the Bulma Customizer tool. Very cool.

There are also pre-built extensions and templates for Bulma, though I didn’t use any of these:

Designing the Grav Template

In building my custom template, I referenced Grav’s Skeleton blog Quark theme, which uses Spectre.css.

I decided on this overall structure to my theme, wherein I collapsed some files to make life easier:

  • templates/partials/base.twig.html (used by all other templates below)

    • includes partials/metadata.twig.html
    • includes partials/navigation.twig.html
    • includes partials/footer.twig.html
    • includes partials/svg.twig.html (more on this later)
  • default.twig.html
    • extends partials/base.twig.html
    • includes partials/hero.twig.html
  • blog.twig.html
    • extends partials/base.twig.html
    • includes partials/hero.twig.html
    • includes partials/archives.twig.html
    • includes partials/taxonomlylist.twig.html
    • includes partials/simplesearch_searchbox.html.twig
    • includes partials/blog-list-item.twig.html
    • includes partials/pagination.twig.html
  • item.twig.html
    • extends partials/base.twig.html
    • includes partials/blog-item.twig.html
    • includes partials/simplesearch_searchbox.html.twig
    • includes partials/relatedpages.twig.html (default)
    • includes partials/taxonomlylist.twig.html
  • error.twig.html

    • extends partials/base.twig.html
    • includes partials/hero.twig.html
  • modular.twig.html (uses subfolders of templates to include)
    • extends partials/base.twig.html
    • may include modular/hero.twig.html
      • includes partials/hero.twig.html
    • may include modular/items.twig.html
      • includes partials/blog-list-item.twig.html
    • may include modular/text.twig.html

Twig bits

Grav uses the Twig templating engine, which itself is based on PHP. The main bit of code that allows the hierarchy of template files above is either:

  • {% extends "partials/base.html" %} or
  • {% includes "partials/hero.html" with { 'title': 'hi' %} - which can pass variables to the included partials using with.

The main Grav additions are:

Just a couple of thins I discovered:

First, I thought that inline JS blocks could be appended with addInlineJS(). Sadly the last of this block clears all prior, so this would not work:

{% block javascripts %}
  {% set script %}
    alert('hi');
  {% endset %}
  {% do assets.addInlineJs(script, {group:'bottom'}) %}
{% endblock %}

Second, speaking of {{ base_url }} - this variable is blank if Grav is the root of your web site. So using just this variable in a link to go “home” would not work, use something like {{ base_url|default('/') }} instead.

Third, and this is really weird. On a “bad” browser - that is to say, on IE8-11 - the Twig engine treats modular pages as .txt.twig instead of .html.twig. I have no idea why! Firefox, Safari, Chome do not exhibit this inconsistently! Anyhoo after a lot of testing and thinking it was a mime type problem (it’s not), the solution is to add template_format: html to your frontmatter (headers) in all modular pages.

Finally, some advice - turn off Grav caching, and even then, if you encounter any Twig errors, it’s a good idea to immediately delete your entire cache anyway - it’ll save you much grief!

HTML bits

I tried to follow HTML5’s Semantic Web structure, which better describes the content, so that a page is not just a bunch of <div> tags. These tags define page landmarks:

  • <nav> for navigation menu, tabs, etc.
  • <header> for the page header
  • <aside> for sidebars
  • <main> for the one-and-only main page content, which may contain:
    • <artcile>
    • <section>
  • <footer> for the page footer

Honestly, my use of <section> and <article> elements is rather arbitrary, so you should look at recommendations at:

HTML is not minified by Grav, so add the plugin Minify Html by Jimblue.

Javascript bits

I didn’t want to use any Javascript library, because I felt the latest version of JavaScript, ECMAScript 2015, would suffice. No fallbacks in may cases, so visitors need a reasonably up-to-date browser.

Some things I learnt:

I use the fetch API to load external HTML content into my modal dialog box. I use this in Disclaimer message in the footer. Without JS the standard hyperlink will navigate to a Terms & Conditions page, but with JS, a modal dialog box will popup instead.

Also, I rolled out my own lightbox with a simple swipe function I got from this discussion on SleepWalker’s GitHub swipe.js.

On fallbacks, I usually set actions as anchors as the fallback e.g. opening a modal dialog box, opening the lightbox, and navigating tabs.

Also, IE11 does not support forEach on the NodeList, so... here is the polyfill polyfill-ie11-nodelist-foreach.js by BobLee

if ('NodeList' in window && !NodeList.prototype.forEach) {
  console.info('polyfill for IE11');
  NodeList.prototype.forEach = function (callback, thisArg) {
    thisArg = thisArg || window;
    for (var i = 0; i < this.length; i++) {
      callback.call(thisArg, this[i], i, this);
    }
  };
}

And finally, I don’t bother to minify, since Grav does that for me!

SVG graphics

I have been using JPEG/PNG images and Font Awesome True Type font for “icons.” I wanted to use vector graphics for all icons instead, since that will scale perfectly irrespective of resolution and device. You can overdose on info at CSS-Tricks’ Everything You Need To Know About SVG but the key points to me are fill styles and hover effects both of which I use in my search bar (as summarized in this blog).

I chuck all my SVGs into a file, called svg.twig.html, which I can then use to insert the ones I want. My implementation is actually more complex - I use headers to load SVGs into pages that I refer to with xlink:href mentioned later.

You can create SVGs with tools like:

But who does that? Just find good open-source / free images and note the terms of use. I used the first two resources only:

There are also various SVG “compression” tools, but for me, I opted to optimize by hand. Nuts, eg?

Fixes

Tables

Grav’s Markdown processor automatically creates tables but does not insert the requisite class="table" for Bulma. So one option is to re-define styles for a table without this class..

But the way I chose to fix this is is to simply wrap the markdown in a <div>. On its own this does not work because Markdown assumes whatever is contained within the <div> is not markdown, so to override this:

  1. First, enable Markdown Extra on the page header, by adding:
    markdown:
    extra: true
  2. Now, wrap the table content with a <div> with the appropriate style classes and most importantly, with markdown="1":
    <div class="table is-hoverable is-striped" markdown="1">
    # | Title  | Description
    --|--------|-------------
    1 | 12345  | caught a fish alive
    2 | 678910 | let it go again
    </div>

You could also enable Markdown Extra globally, edit the system configuration file user/config/system.yaml and add:

pages:
  markdown:
    extra: true

Or just, do what I do, override CSS:

table:not(.table) th  {
    background: #f7f7f7;
    padding: .2rem .4rem .2rem .4rem;
    vertical-align: middle;
    border: 1px solid #eaeaea;
}
table:not(.table) td {
    padding: .2rem .4rem .2rem .4rem;
    vertical-align: middle;
    border: 1px solid #eaeaea;
}

Old notice blocks

Grav now has a plugin for Markdown Notices. I previously used a template with an odd use of angled brackets, so these needed to be replaced.

Old MarkdownColorBorderBackgroundColorNew Markdown
>>>>>>green#5CB85C#F1F9F1#3d8b3d!!!!
>>>>>cyan#5BC0DE#F4F8FA#28a1c5!!!
>>>>red#D9534F#FDF7F7#b52b27!!
>>>yellow#F0AD4E#FCF8F2#df8a13!
>gray#F0F2F4none#999>

Plugins

External Links Changes

I use the External Links Plugin for Grav to mark external websites as such, and open these links in a new window.

I elected to override/replace the CSS style, with the initial intention of using an embedded SVG (in this case, from Open Iconic) like so:

a.external {
    background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 8 8"><path fill="%233273DC" d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z" /></svg>') right center no-repeat;
    padding-right: 0.9rem;
    position: relative;
}

Then I figured that many embedded SVGs would decrease performance. I could’ve included the SVGs somewhere in all my HTML pages and then used xlink:href to refer to the embedded version:

<div style="display:none">
  <svg xmlns="http://www.w3.org/2000/svg" id="externallink" width="9" height="9" viewBox="0 0 8 8">
    <path fill="%233273DC" d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z" />
  </svg>
</div>
a.external {
    background: url('data:image/svg+xml,<svg><use xlink:href="#externallink" /></svg>') right center no-repeat;
    padding-right: 0.9rem;
    position: relative;
}

But in the end, I decided to avoid yet another dependency and go with a Unicode symbol that should exist with most fonts on most devices. I got the idea from this StackOverflow question “New Window Icon (for web accessibility)”.

Add in a cool transition and voilà!

a.external {
    content: "\29C9";
    display: inline-block;
    position: relative;
    top: -5px;
    margin-left: 2px;
    transform: rotate(90deg);
    transition: transform 0.4s;
}
a.external.icon:hover:after {
    content: "\29C9";
    display: inline-block;
    position: relative;
    top: -5px;
    margin-left: 2px;
    transform: rotate(110deg);
    transition: transform 0.6s;
}

Prism-Highlighter Fixes

I wanted to use PrismJS for syntax highlighting. Sigh, the Prism highlighter plugin assumes jQuery! The code injects JavaScript to every page only if you enable the following options:

The fix is to edit user/plugins/prism-highlight/prism-highlight.php (more on this next)

Anyway, just change the lines I have commented out to the next line:

if ($this->config->get('plugins.prism-highlight.all-pre-blocks')) {
  // $inline .= "$('pre:not([class*=\'language-\'])').addClass('language-txt');\n";
  $inline .= "document.querySelectorAll('pre:not([class*=\'language-\'])').forEach(c=>{c.classList.add('language-txt')});\n";
}
  if ($this->config->get('plugins.prism-highlight.plugins.line-numbers')) {
  //$inline .= "$('pre').addClass('line-numbers');\n";
  $inline .= "document.querySelectorAll('pre').forEach(c=>{c.classList.add('line-numbers')});\n";
}
  if ($this->config->get('plugins.prism-highlight.plugins.command-line')) {
  //$inline .= "$('pre').addClass('command-line');\n";
  $inline .= "document.querySelectorAll('pre').forEach(c=>{c.classList.add('command-line')});\n";
}

Also, be careful of lines that reference the configuration, you’ll need to rename those too or figure out if pointing to the original plugin is ok, e.g.

if ($this->config->get('plugins.x-prism-highlight.enabled')) {
...
$theme = $this->config->get('x-plugins.prism-highlight.theme') ?: 'prism-default.css';

Next, I realized Prism’s class names conflict with Bulam’s tag, number and label classes! You can either use the Custom Class plugin and rename all the CSS, or do what I do...

Simply clear any Bulma CSS definitions in favor of Prism’s when it comes to content within a <pre><code> block. Something like this:

pre  {
  background-color: #002b36;
}
pre code {
  background: unset;
  color: unset;
}
pre code .tag, pre code .number, pre code .label {
  display: inline;
  padding: inherit;
  font-size: inherit;
  line-height: inherit;
  text-align: inherit;
  vertical-align: inherit;
  border-radius: inherit;
  font-weight: inherit;
  white-space: inherit;
  background: inherit;
  margin: inherit;
}

Afer facing all this, I gave up using the plugin and instead include what I need directly in my template! Two last tips with Prism:

Here are the languages Prism supports.

And, remember to save the query string from the download URL so you can re-build when needed, e.g.

https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript+apacheconf+csharp+bash+batch+markup-templating+docker+ini+json+markdown+sql+powershell+properties+twig+swift+yaml+regex&plugins=line-numbers

SimpleSearch

Without JavaScript, simplesearch will not work. So, the workaround is the explicity define a form action, and create a php file to re-direct to the SEO friendly search page.

For simplesearch_searchbox.html.twig:

<form id="cy-search" action="/simplesearch.php" ...>
  <input name="searchfield" ...></input>
  ...
</form>

And create the corresponding simplesearch.php file in your Grav root:

<?php
$search = $_GET["searchfield"];
if (strlen($search) == 0)
  header('Location: /', true, 301);
else
  header('Location: /search/query:' . $search, true, 303);
exit();

Other Plugins

I modified other Grav plugins with my own “enhancements”:

  • For Count Views, I wanted the output file name to incldue the month and year (i.e. a new file every month).
  • For Archives, I stored archives_all_data and exposed this variable to the calling Twig template (i.e. before cutting off (array_slice-d) at the count limit).

Since it’s a bad idea to directly edit the plugins, here is what I did:

  1. Disable the actual downloaded plugins in the dashboard. I keep the original plugin around, so I am notified of updates.

  2. Create a copy of the whole plugin folder, call it "x-prism-highlight" (or whatever, but note the naming conventions must align across all files and classes).

  3. Rename the main PHP and YAML files to follow the same name as the folder above, "x-prism-highlight.php" and "x-prism-highlight.yaml"

  4. Edit "blueprints.yaml" with a unique name. I took out all the URLs, author name, etc. so as not to get confused with the original plugin.

    name: X Prism Highlighter
  5. Edit the file "cy-prism-highlight.php" and change the class name:

    //class PrismHighlightPlugin extends Plugin
    class XPrismHighlightPlugin extends Plugin
  6. Finally, I created my own Prism.JS to include the languages I wanted and placed that into the js folder.

Almost there... final checks

To check everything, I used tools like the following:

And after all that optimization, compared to my previous template’s PageSpeed Insights score, I now get scores above 90 across the board. Source: Google Lighthouse / Web.dev and Google PageSpeed Insights.

Google PageSpeed Insights score for re-designed pageGoogle Lighthouse / web.dev score for re-designed page

And one last tip, to spell check the entire page in Firefox, open the Web Developer console, and enter:

document.body.contentEditable='true';
document.designMode='on';

When done, revert with:

document.body.contentEditable='false';
document.designMode='off';

Conclusion

Even with all this effort I’m certain to have bugs and issues with my template. Not difficult, just a lot of effort... and tweaking things exactly as you want them is not so easy and worse is testing all browsers, even for something as simple as changing the CSS framework and dropping jQuery! Kudos to those who manage to publish and share “real” templates!

Updated 22 May: Lots of IE specific fixes - modal positioning, forEach polyfill, Twig template_format for modular pages, and reverted back to ECMAScript 5 (except for promise.finally()). Added testing tools.

Updated 24 June: Only updated the Page Insights and Web.dev screenshots to use HTTPS, increases scores!