TOPIC: COMMENT
Structuring a Grav Microblog using Twig and YAML
This year has seen me move to using Grav on multiple websites. The first one has two sections on Grav: Travel Jottings and Varied Surroundings, both based around single articles with the sort of HTML output post-processing that I have described in another entry on here. In this one, I move onto how to display things in a blog structure. This approach is in use on the parts of this website that are hosted using Grav: AI & Data Science, Coding Notebook and Collected Snippets. HTML output is post-processing here too, even if the story is much bigger than all that.
Building a Paginated Blog Template with Twig
First, we work through the blog page template that shows all the entries with pagination to ensure that not everything appears on the same page. In this file, the first step is to refer to a base template:
{% extends 'partials/base.html.twig' %}
Then, you need to define the content block, which encases the rest of the code:
{% block content %}...{% endblock %}
The above overrides any matching block of the same name in the parent template, meaning that everything between these tags replaces whatever the base template had in its content block. That includes the page collection declaration that is defined next:
{% set collection = page.collection %}
This fetches the page's collection, defined in the page's front matter. In other words, this is the YAML at the top of the Markdown file, and we will examine that later, which should make what is happening here clearer. For a fuller discussion, I have another entry describing how to set up an RSS feed that describes collections in even more detail. For added organisation, I enclosed everything with section HML tags:
<section class="blog-collation">...</section>
Before displaying the individual microblog entries, I also added a section with a hyperlinked title that allows a visitor to easily reach the website section front page from anywhere within that part of the site. Here is the code for that:
<article class="the-post">
<h2><a href="{{ base_url }}" title="Go to first page">{{ page.title }}</a></h2>
</article>
After that, there is a loop to display a number of posts for each page with some conditional logic to handle when there are no Markdown files available in the expected directory:
{% for item in collection %}...{% else %}<p>No entries found.</p>{% endfor %}
When there are Markdown files in the required directory, this is the code that adjusts links in the HTML rendered for each Markdown file so they open securely in a new browser tab:
{% set content = item.content
|replace({'<a href="http': '<a target="_blank" rel="noopener noreferrer" href="http'})
%}
The above reads in the page content (item.content) because this is a loop with `item` as the iterator. Then, it replaces <a href="http with <a target="_blank" rel="noopener noreferrer" href="http. The input was solely Markdown in this case, which meant that the rendering and text replacement was streamlined as expected. Beyond, the main article display code follows:
<article class="the-post">
<p class="mt-3 mb-4"><strong>
{% if item.date %}
{{ item.date|date("H:i, j") }}<sup>{{ item.date|date("S") }}</sup> {{ item.date|date("F Y") }}
{% endif %}
</strong></p>
<div class="entry-body">
{{ content|raw }}
</div>
</article>
The mix of HTML tags above should not surprise anyone who has worked with web content production before. What really needs more comment are these lines from the above:
{% if item.date %}
{{ item.date|date("H:i, j") }}<sup>{{ item.date|date("S") }}</sup> {{ item.date|date("F Y") }}
{% endif %}
In a block guarded by {% if item.date %} to ensure nothing is output if a page has no date set, the publishing time and date for an entry is rendered in three separate filter calls, which combine to produce a single formatted output that looks like this: 14:30, 3rd April 2026
| Filter | Output | Example | |
|---|---|---|---|
date("H:i, j") |
24-hour time and day number | 14:30, 3 | |
date("S") |
Ordinal suffix, in <sup> tags |
rd | |
date("F Y") |
Full month and year | April 2026 |
Enclosed within the nested tags, it is {{ content|raw }} that displays the paragraph output underneath the date and time for each entry. At the bottom of the page, there is an extra code block needed to add pagination and Previous/Next buttons for navigation:
{% if config.plugins.pagination.enabled and collection.params.pagination %}
{% include 'partials/pagination.html.twig' with {'base_url': page.url, 'pagination': collection.params.pagination} %}
{% endif %}
The above is positioned outside the <section class="blog-collation">...</section> block but within the first part of the {% for item in collection %}...{% else %}<p>No entries found.</p>{% endfor %} construct. The {% if config.plugins.pagination.enabled and collection.params.pagination %} condition ensures that noting is displayed unless the Pagination plugin has been installed and enabled.
Decoding the Associated YAML Front Matter
All of the above does make as much sense unless you also look at the front matter YAML on the Markdown file for the main microblog page, blog.md, itself consistent with the name of its template, which is the one we worked through above. This is what now follows.
---
title: 'AI & Data Science Jottings'
content:
items: '@self.children'
order:
by: date
dir: desc
pagination: true
limit: 10
---
In the above, the title field defines the name of the microblog, one of three on this website. Within the content section, the items page collection is defined as all child pages of the current page. In this case, these would be sub-folders sitting inside this page's folder, each containing a single Markdown file called default.md, another name consistent with that of its associated template. The order of appearance of the microblog entries follows next, with the latest posts appearing first because of the descending date order. Pagination gets switched on with a limit of ten entries per page and navigation buttons to go between them.
Tying the Blog Template and Front Matter Together
Here, we have walked through the two components that make a paginated Grav microblog work: the Twig template that renders the collection and the YAML front matter that defines it. The template handles the display logic, from extending the base file through to looping over each item, adjusting links and formatting the date and time output. The front matter tells Grav which pages belong to the collection, how they should be ordered, and how many should appear per page.
Taken together, this approach has proved flexible enough to serve all three of the Grav-hosted sections on this website. Each relies on the same underlying template and front matter structure, with only the content and folder names differing between them.
In short, anyone setting up a similar microblog structure in Grav should find that these building blocks cover most of what is needed: the base template, the content block, the collection loop and the pagination section. Naturally, there remains plenty of room to adapt the HTML markup and filters to suit a different site's requirements.
Rendering Markdown in WordPress without plugins by using Parsedown
Much of what is generated using GenAI as articles is output as Markdown, meaning that you need to convert the content when using it in a WordPress website. Naturally, this kind of thing should be done with care to ensure that you are the creator and that it is not all the work of a machine; orchestration is fine, regurgitation does that add that much. Naturally, fact checking is another need as well.
Writing plain Markdown has secured its own following as well, with WordPress plugins switching over the editor to facilitate such a mode of editing. When I tried Markup Markdown, I found it restrictive when it came to working with images within the text, and it needed a workaround for getting links to open in new browser tabs as well. Thus, I got rid of it to realise that it had not converted any Markdown as I expected, only to provide rendering at post or page display time. Rather than attempting to update the affected text, I decided to see if another solution could be found.
This took me to Parsedown, which proved to be handy for accomplishing what I needed once I had everything set in place. First, that meant cloning its GitHub repo onto the web server. Next, I created a directory called includes under that of my theme. Into there, I copied Parsedown.php to that location. When all was done, I ensured that file and directory ownership were assigned to www-data to avoid execution issues.
Then, I could set to updating the functions.php file. The first line to get added there included the parser file:
require_once get_template_directory() . '/includes/Parsedown.php';
After that, I found that I needed to disable the WordPress rendering machinery because that got in the way of Markdown rendering:
remove_filter('the_content', 'wpautop');
remove_filter('the_content', 'wptexturize');
The last step was to add a filter that parsed the Markdown and passed its output to WordPress rendering to do the rest as usual. This was a simple affair until I needed to deal with code snippets in pre and code tags. Hopefully, the included comments tell you much of what is happening. A possible exception is $matches[0]which itself is an array of entire <pre>...</pre> blocks including the containing tags, with $i => $block doing a $key (not the same variable as in the code, by the way) => $value lookup of the values in the array nesting.
add_filter('the_content', function($content) {
// Prepare a store for placeholders
$placeholders = [];
// 1. Extract pre blocks (including nested code) and replace with safe placeholders
preg_match_all('//si', $content, $pre_matches);
foreach ($pre_matches[0] as $i => $block) {
$key = "§PREBLOCK{$i}§";
$placeholders[$key] = $block;
$content = str_replace($block, $key, $content);
}
// 2. Extract standalone code blocks (not inside pre)
preg_match_all('/).*?<\/code>/si', $content, $code_matches);
foreach ($code_matches[0] as $i => $block) {
$key = "§CODEBLOCK{$i}§";
$placeholders[$key] = $block;
$content = str_replace($block, $key, $content);
}
// 3. Run Parsedown on the remaining content
$Parsedown = new Parsedown();
$content = $Parsedown->text($content);
// 4. Restore both pre and code placeholders
foreach ($placeholders as $key => $block) {
$content = str_replace($key, $block, $content);
}
// 5. Apply paragraph formatting
return wpautop($content);
}, 12);
All of this avoided dealing with extra plugins to produce the required result. Handily, I still use the Classic Editor, which makes this work a lot more easily. There still is a Markdown import plugin that I am tempted to remove as well to streamline things. That can wait, though. It best not add any more of them any way, not least avoid clashes between them and what is now in the theme.
Using multi-line commenting in Perl to inactivate blocks of code during testing
Recently, I needed to inactivate blocks of code in a Perl script while doing some testing. Since this is something that I often do in other computing languages, I sought the same in Perl. To accomplish that, I need to use the POD methodology. This meant enclosing the code as follows.
=start
<< Code to be inactivated by inclusion in a comment >>
=cut
While the =start line could use any word after the equality sign, it seems that =cut is required to close the multi-line comment. If this was actual programming documentation, then the comment block should include some meaningful text for use with perldoc. However, that was not a concern here because the commenting statements would be removed afterwards anyway. It also is good practice not to leave commented code in a production script or program to avoid any later confusion.
In my case, this facility allowed me to isolate the code that I had to alter and test before putting everything back as needed. It also saved time since I did not need to individually comment out every executable line because multiple lines could be inactivated at a time.
Overriding replacement of double or triple hyphenation in WordPress
On here, I have posts with example commands that include double hyphens, and they have been displayed merged together, something that has resulted in a comment posted by a visitor to this part of the web. All the while, I have been blaming the fonts that I have been using, only for it to be the fault of WordPress itself.
Changing multiple dashes to something else has been a feature of Word autocorrect, but I never expected to see WordPress aping that behaviour, and it has been doing so for a few years now. The culprit is wptexturize and that cannot be disabled, for it does many other useful things.
What happens is that the wptexturize filter changes '--' (double hyphens) to '–' (– in web entity encoding) and '---' (triple hyphens) to '—' (— in web entity encoding). The solution is to add another filter to the content that changes these back to the way they were, and the following code does this:
add_filter( 'the_content' , 'mh_un_en_dash' , 50 );
function mh_un_en_dash( $content ) {
$content = str_replace( '–' , '--' , $content );
$content = str_replace( '—' , '---' , $content );
return $content;
}
The first line of the segment adds in the new filter that uses the function defined below it. The third and fourth lines above do the required substitution before the function returns the post content for display in the web page. The whole code block can be used to create a plugin or placed in the theme's functions.php file. Either way, things appear without the substitution, confusing your readers. It makes me wonder if a bug report has been created for this because the behaviour looks odd to me.
Tidying dynamic URL’s
A few years back, I came across a very nice article discussing how you would make a dynamic URL more palatable to a search engine, and I made good use of its content for my online photo gallery. The premise was that URL's that look like that below are no help to search engines indexing a website. Though this is received wisdom in some quarters, it doesn't seem to have done much to stall the rise of WordPress as a blogging platform.
http://www.mywebsite.com/serversidescript.php?id=394
That said, WordPress does offer a friendlier URL display option too, which you can see in use on this blog; they look a little like the example URL that you see below, and the approach is equally valid for both Perl and PHP. Since I have been using the same approach for the Perl scripts powering my online phone gallery, now want to apply the same thinking to a gallery written in PHP:
http://www.mywebsite.com/serversidescript.pl/id/394
The way that both expressions work is that a web server will chop pieces from a URL until it reaches a physical file. For a query URL, the extra information after the question mark is retained in its QUERY_STRING variable, while extraneous directory path information is passed in the variable PATH_INFO. For both Perl and PHP, these are extracted from the entries in an array; for Perl, this array is called is $ENV and $_SERVER is the PHP equivalent. Thus, $ENV{QUERY_STRING} and $_SERVER{'QUERY_STRING'} traps what comes after the ? while $ENV{PATH_INFO} and $_SERVER{'PATH_INFO'} picks up the extra information following the file name (/id/394/ in the example). From there on, the usual rules apply regarding cleaning of any input but changing from one to another should be too arduous.