TOPIC: HYPERLINK
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.
Altering table and hyperlink tags for single Grav articles using HTML post-processing
This year, there have been a few entries on here regarding Grav because of my moving parts of my website estate to that content management system, first from Textpattern and latterly from WordPress. Once the second activity was completed, I then added an article on German public holidays elsewhere. That brought me to the topic of this piece: ensuring that some Markdown was rendered as required.
There were two parts to this: the styling of tables and the actions of hyperlinks. Each needs to be performed in a page template when all HTML has been initially rendered. Further processing then makes the required changes. Since this is a page template and not a partial template and not a partial template, you need to import a master template like this:
{% extends 'partials/base.html.twig' %}
Then, you go to the next stage, defining the content block within {% block content %}...{% endblock %} Twig tags:
{% set content = page.content
|replace({'<table>': '<table class="table mt-5 mb-5">'})
|replace({'<a href="http': '<a target="_blank" rel="noopener noreferrer" href="http'})
%}
The above reads in the page content (page.content) and does some text replacement operations. The first of these changes <table> to <table class="table mt-5 mb-5">, while the second replaces <a href="http with <a target="_blank" rel="noopener noreferrer" href="http. While my content was a mix of Markdown and HTML, depending on the article, the latter operation appeared to standardise every link.
Once the text replacement has been completed, the next step is to output the processed HTML like this:
{{ content|raw }}
This last line sits outside the {% block content %}...{% endblock %} block; coming after it, in fact. To send the processed output to the generated web page, you need to ensure that you are referring to the right variable, the local one called content and not page.content. The raw filter also is essential here to ensure that nothing is rendered into HTML when the raw HTML itself is what is needed.
All of this effort ensures that straightforward Markdown can be used in content, while Grav does some extra work in the background to ensure that all is rendered without extra intervention. While there may need to be a certain level of standardisation to make this all work well, I find that it does what is needed, albeit in a different manner from shortcode approach that you find in Hugo.
Building context-sensitive navigation in Grav CMS with Twig
If you are migrating a web portal because a new CMS release broke a site navigation menu plugin, then you are going to make sure that something similar is there in the new version. The initial upset was caused by Textpattern 4.9.0, the move to which resulted in a hasty rollback. A subsequent AI interaction brought Grav CMS into the fray, where menus can be built using the Twig templating language.
Along the way, there was one noticeable difference: a composite menu with hierarchy that drilled down to the pages in a selected section was not quite as possible in Grav. Nevertheless, displaying one menu for pages in a given section along with another showing the other sections is hardly a dealbreaker as far as I am concerned, especially when things are context-sensitive anyway.
This may because Grav derives its navigation directly from the content directory tree using folder naming conventions and front matter flags, all unlike database-driven systems that rely on a dedicated menu editor. After all, you are working with files that expose page state, and not queries of database tables.
The Pages Folder
At the heart of the system is the pages folder. Grav looks at the top level of this directory to determine the primary navigation, and any subfolder that begins with a numeric prefix is treated as a visible page whose position in the menu is set by that number. A structure such as pages/01.home, pages/02.about, pages/03.blog and pages/04.contact immediately yields a working menu in the order you expect. Because this approach is driven by the file system, reordering pages is as simple as renaming the folders, with no additional configuration required.
Visibility can also be controlled without renaming if you prefer to keep folders unnumbered. Each page has a Markdown file containing YAML front matter (commonly named default.md), and adding visible: true to that front matter ensures the page appears in navigation. Setting visible: false hides it. Both approaches work across a site, though the numeric prefix convention remains the most straightforward way to manage ordering and visibility together.
Customising Menu Labels
Menu text defaults to the page title, which suits most cases well. There are times when you want a shorter label in the navigation while keeping a fuller title for the page itself, and the front matter field menu: makes that possible. Writing menu: Blog on the blog page means the menu displays "Blog" even if the page title reads "Company Blog and News". This keeps navigation crisp without sacrificing descriptive titles for search engines and content clarity.
The Main Menu Loop
The primary navigation iterates over pages.children.visible and prints a link for each top-level page. The active and activeChild flags on each page object let you mark the current location: active matches the page the visitor is on, while activeChild is true on any parent whose descendant is currently being viewed. Testing both together means a top-level item is highlighted, whether the visitor is on that page directly or anywhere beneath it:
<ul id="mainmenu" class="section_list">
{% for p in pages.children.visible %}
<li class="{{ (p.active or p.activeChild) ? 'active_class' : '' }}">
<a href="{{ p.url }}">{{ p.menu }}</a>
</li>
{% endfor %}
</ul>
This loop picks up any changes to the page tree automatically, with no further configuration required.
Context-Sensitive Sidebar Headings
Before the navigation blocks, the sidebar can show a contextual heading depending on where the visitor is. On the home page, a page.home check provides one heading, and a route comparison handles a specific page such as /search:
{% if page.home %}
<h4 class="mt-4 mb-4">Fancy Some Exploration?</h4>
{% endif %}
{% if page.route == '/search' %}
<h4 class="mt-4 mb-4">Fancy Some More Exploration?</h4>
{% endif %}
These headings appear independently of the secondary navigation block, so they display even when there is no active section with children to list below them.
The Secondary Menu
When a visitor is inside a section that has visible child pages, a secondary menu listing those children is more useful than a dropdown. The approach is to find the active top-level page, referred to here as the owner, by looping through pages.children.visible and checking the same active and activeChild flags:
{% set owner = null %}
{% for top in pages.children.visible %}
{% if top.active or top.activeChild %}
{% set owner = top %}
{% endif %}
{% endfor %}
Once owner is found, its menu label can be used as a section heading and its visible children rendered as a list. Importantly, each child item should test child.active or child.activeChild rather than child.active alone. Without activeChild, a visitor to a grandchild page would see no item highlighted in the secondary nav at all:
{% if owner and owner.children.visible.count > 0 %}
<h4 class="mt-4 mb-4">{{ owner.menu }}</h4>
<ul id="secondary-nav" class="section_list">
{% for child in owner.children.visible %}
<li class="{{ (child.active or child.activeChild) ? 'active_class' : '' }}">
<a href="{{ child.url }}">{{ child.menu }}</a>
</li>
{% endfor %}
</ul>
<h4 class="mt-4 mb-4">Looking Elsewhere?</h4>
{% endif %}
The entire block is conditional on owner existing and having visible children, so it does not render at all on the home page, the search page or any other top-level page without subsections.
Common Troubleshooting Points
There are a few subtleties worth bearing in mind. The most frequent cause of trouble is looping over the wrong collection: using page.children.visible instead of pages.children.visible in the owner-detection loop places you inside the current page's subtree, so nothing will flag as active or activeChild correctly. A second issue affects secondary nav items specifically: using only child.active means a visitor to a grandchild page sees no item highlighted because none of the listed children is the current page. Adding or child.activeChild to the condition resolves this. Clearing the Grav cache is also a worthwhile step during development because stale output can make correct template changes appear to have no effect.
Closing Remarks
In summary, you have learned how Grav assembles navigation from its page tree, how to detect the active section using active and activeChild flags, how to display a secondary menu only when a section has visible children, and how to show context-sensitive headings for specific pages. The result is a sidebar that maintains itself automatically as pages are added or reorganised, with no manual menu configuration required.
Eliminating Peekaboo content display problems in Internet Explorer
Recently, I changed the engine of my online photo gallery to a speedier PHP/MySQL-based affair from its PHP/Perl/XML-powered predecessor. On the server side, all was well, but a peculiar display issue turned up in Internet Explorer (6, 7 & 8 were afflicted by this behaviour) where photo caption text on the thumbnail gallery pages was being displayed erratically.
As far as I can gather, the trigger for the behaviour was that the thumbnail block was placed within a DIV floated using CSS that touched another DIV that cleared the floating behaviour. I use a table to hold the images and their associated captions in place. Furthermore, each caption was also a hyperlink nested within a set of P tags.
The remedy was to set the CSS Display property for the affected XHTML tag to a value of "inline-block". Within a DIV, TABLE, TR, TD, P and A tag hierarchy, finding the right tag where the CSS property in question has the desired effect took some doing. As it happened, it was the tag set, that for the hyperlink, at the bottom of the stack that needed the fix.
Of course, it's all very fine fixing something for one browser, but it's worthless if it breaks the presentation in other browsers. In that vein, I did some testing in Opera, Firefox, Seamonkey and Safari to check if all was well and it was. There may be older browsers, like versions of IE before 6, where things don't appear as intended, yet I get the impression from my visitor statistics that the newer variants hold sway anyway. All in all, it was a useful lesson learnt, and that's never a bad thing.
LVHA…
On my web design journey, I have learned the wisdom that CSS styles for hyperlinks should be defined like the following:
a:link {...}
a:visited {...}
a:hover {...}
a:active {...}
List out the names of the pseudoselectors, and you'll soon work out where they got LVHA: Link, Visited, Hover and Active. However, I have recently spotted the following being used:
a {...}
a:hover {...}
The trick here is to define your style globally and only define specifics for the relevant pseudoselector, hover in this example. It works well in the likes of Mozilla and Opera, but Internet Explorer is another story. Even IE7 needs the LVHA treatment. I spotted this when I observed unexpected changes in the appearance of link text after visiting the link: visited links starts to change colour. While I know that the likes of Jakob Nielsen frown upon non-changing link colour, I choose to ignore this and keep it constant, so following the LVHA approach is needed to keep things as I would like them.
Wonders of the middle mouse button
My installation of Firefox seems to have stopped listening to the target attribute of hyperlinks. Thankfully, the middle mouse button comes to the rescue. Clicking on a link with the middle button opens it the destination page up in a new window or tab, depending on how you set your defaults. The behaviour goes even further than this: the trusty middle mouse button does the same for bookmarks and the Google search bar; all very useful. And it is not just a Firefox thing, either. IE7 does the same thing for web page hyperlinks and bookmarks while in Opera, it is limited to links on web pages.