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:
- Basic Concepts of flexbox at MDN
- A Complete Guide to Flexbox at CSS Tricks
- FlexboxGame by Webflow
- Flexbox Froggy by Codepip
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:
- Bulmaswatch tempaltes
- Bulma-Extensions with divider, badge, slider, icon picker, switch, accordion, carousel, etc. styles.
- BulmaJS library so you don't have to roll your own Javascript like I did.
- And other stuff, listed at the Bulma GitHub
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 usingwith
.
The main Grav additions are:
- Theme Variables - e.g.
{{ base_dir }}
- Twig Filters & Functions
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><meta http-equiv="Content-Type" content="text/html; charset=utf-8">
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) {
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:
- Vectr - no login required!
- DrawIo - offline too
- Vecteezy - Chrome only
- Method Draw
- GravIt Designer
But who does that? Just find good open-source / free images and note the terms of use. I used the first two resources only:
- unDraw by Katerina Limpitsouni - a vast and beautiful collection!
- Feather icons by colebemis - elegant simplicity!
- Open Iconic
- Ionicons by the Ionic Framework team
- Eva Icons
- Icofont
- Linea outline iconset
- Mobirise
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:
- First, enable Markdown Extra on the page header, by adding:
markdown: extra: true
- Now, wrap the table content with a
<div>
with the appropriate style classes and most importantly, withmarkdown="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 Markdown | Color | Border | Background | Color | New Markdown |
---|---|---|---|---|---|
>>>>>> |
green | #5CB85C | #F1F9F1 | #3d8b3d | !!!! |
>>>>> |
cyan | #5BC0DE | #F4F8FA | #28a1c5 | !!! |
>>>> |
red | #D9534F | #FDF7F7 | #b52b27 | !! |
>>> |
yellow | #F0AD4E | #FCF8F2 | #df8a13 | ! |
> |
gray | #F0F2F4 | none | #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 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:
- All Pre Blocks - which defaults any blocks without a language to
txt
- Line Numbers - the Line Numbers plugin
- Command Line - the Command Line plugin
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:
-
Disable the actual downloaded plugins in the dashboard. I keep the original plugin around, so I am notified of updates.
-
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).
-
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"
-
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
-
Edit the file "cy-prism-highlight.php" and change the class name:
//class PrismHighlightPlugin extends Plugin class XPrismHighlightPlugin extends Plugin
-
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:
- Do I use CSS that breaks in...
- Can I use...
- Browserling - live interactive cross-browser testing
- If you create a page using...
- JSHint
- CSS Lint
- IE8-11 and Edge Virtual Machines
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.
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!