Technology Tales

Notes drawn from experiences in consumer and enterprise technology

TOPIC: READING

Creating modular pages in Grav CMS

15th February 2026

Here is a walkthrough that demonstrates how to create modular pages in Grav CMS. Modular pages allow you to build complex, single-page layouts by stacking multiple content sections together. This approach works particularly well for modern home pages where different content types need to be presented in a specific sequence. The example here stems from building a theme from scratch to ensure sitewide consistency across multiple subsites.

What Are Modular Pages

A modular page is a collection of content sections (called modules) that render together as a unified page. Unlike regular pages that have child pages accessible via separate URL's, modular pages display all their modules on a single URL. Each module is a self-contained content block with its own folder, markdown file and template. The parent page assembles these modules in a specified order to create the final page.

Understanding the Folder Structure

Modular pages use a specific folder structure. The parent folder contains a modular.md file that defines which modules to include and in what order. Module folders sit beneath the parent folder and are identified by an underscore at the start of their names. Numeric prefixes before the underscore control the display order.

Here is an actual folder structure from a travel subsite home page:

/user/pages/01.home/
    01._title/
    02._intro/
    03._call-to-action/
    04._ireland/
    05._england/
    06._scotland/
    07._scandinavia/
    08._wales-isle-of-man/
    09._alps-pyrenees/
    10._american-possibilities/
    11._canada/
    12._dreams-experiences/
    13._practicalities-inspiration/
    14._feature_1/
    15._feature_2/
    16._search/
    modular.md

The numeric prefixes (01, 02, 03) ensure modules appear in the intended sequence. The descriptive names after the prefix (_title, _ireland, _search) make the page structure immediately clear. Each module folder contains its own markdown file and any associated media. The underscore prefix tells Grav these folders contain modules rather than regular pages. Modules are non-routable, meaning visitors cannot access them directly via URLs. They exist only to provide content sections for their parent page.

The Workflow

Step One: Create the Parent Page Folder

Start by creating a folder for your modular page in /user/pages/. For a home page, you might create 01.home. The numeric prefix 01 ensures this page appears first in navigation and sets the display order. If you are using the Grav Admin interface, navigate to Pages and click the Add button, then select "Modular" as the page type.

Create a file named modular.md inside this folder. The filename is important because it tells Grav to use the modular.html.twig template from your active theme. This template handles the assembly and rendering of your modules.

Step Two: Configure the Parent Page

Open modular.md and add YAML front matter to define your modular page settings. Here is an example configuration:

---
title: Home
menu: Home
body_classes: "modular"

content:
    items: '@self.modular'
    order:
        by: default
        dir: asc
        custom:
            - _hero
            - _features
            - _callout
---

The content section is crucial. The items: '@self.modular' instruction tells Grav to collect all modules from the current page. The custom list under order specifies exactly which modules to include and their sequence. This gives you precise control over how sections appear on your page.

Step Three: Create Module Folders

Create folders for each content section you need. Each folder name must begin with an underscore. Add numeric prefixes before the underscore for ordering, such as 01._title, 02._intro, 03._call-to-action. The numeric prefixes control the display sequence. Module names are entirely up to you based on what makes sense for your content.

For multi-word module names, use hyphens to separate words. Examples include 08._wales-isle-of-man, 10._american-possibilities or 13._practicalities-inspiration. This creates readable folder names that clearly indicate each module's purpose. The official documentation shows examples using names like _features and _showcase. It does not matter what names that you choose, as long as you create matching templates. Descriptive names help you understand the page structure when viewing the file system.

Step Four: Add Content to Modules

Inside each module folder, create a markdown file. The filename determines which template Grav uses to render that module. For example, a module using text.md will use the text.html.twig template from your theme's /templates/modular/ folder. You decide on these names when planning your page structure.

Here is an actual example from the 04._ireland module:

---
title: 'Irish Encounters'
content_source: ireland
grid: true
sitemap:
    lastmod: '30-01-2026 00:26'
---
### Irish Encounters {.mb-3}
<p style="text-align:center" class="mt-3"><img class="w-100 rounded" src="https://www.assortedexplorations.com/photo_gallery_images/eire_kerry/small_size/eireCiarraigh_tomiesMountain.jpg" /></p>
From your first arrival to hidden ferry crossings and legendary names that shaped the world, these articles unlock Ireland's layers beyond the obvious tourist trail. Practical wisdom combines with unexpected perspectives to transform a visit into an immersive journey through landscapes, culture and connections you will not find in standard guidebooks.

The front matter contains settings specific to this module. The title appears in the rendered output. The content_source: ireland tells the template which page to link to (as shown in the text.html.twig template example). The grid: true flag signals the modular template to render this module within the grid layout. The sitemap settings control how search engines index the content.

The content below the front matter appears when the module renders. You can use standard Markdown syntax for formatting. You can also include HTML directly in markdown files for precise control over layout and styling. The example above uses HTML for image positioning and Bootstrap classes for responsive sizing.

Module templates can access front matter variables through page.header and use them to generate dynamic content. This provides flexibility in how modules appear and behave without requiring separate module types for every variation.

Step Five: Create Module Templates

Your theme needs templates in /templates/modular/ that correspond to your module markdown files. When building a theme from scratch, you create these templates yourself. A module using hero.md requires a hero.html.twig template. A module using features.md requires a features.html.twig template. The template name must match the markdown filename.

Each template defines how that module's content renders. Templates use standard Twig syntax to structure the HTML output. Here is an example text.html.twig template that demonstrates accessing page context and generating dynamic links:

{{ content|raw }}
{% set raw = page.header.content_source ?? '' %}
{% set route = raw ? '/' ~ raw|trim('/') : page.parent.route %}
{% set context = grav.pages.find(route) %}
{% if context %}
    <p>
        <a href="{{ context.url }}" class="btn btn-secondary mt-3 mb-3 shadow-none stretch">
            {% set count = context.children.visible|length + 1 %}
            Go and Have a Look: {{ count }} {{ count == 1 ? 'Article' : 'Articles' }} to Savour
        </a>
    </p>
{% endif %}

First, this template outputs the module's content. It then checks for a content_source variable in the module's front matter, which specifies a route to another page. If no source is specified, it defaults to the parent page's route. The template finds that page using grav.pages.find() and generates a button linking to it. The button text includes a count of visible child pages, with proper singular/plural handling.

The Grav themes documentation provides guidance on template creation. You have complete control over the design and functionality of each module through its template. Module templates can access the page object, front matter variables, site configuration and any other Grav functionality.

Step Six: Clear Cache and View Your Page

After creating your modular page and modules, clear the Grav cache. Use the command bin/grav clear-cache from your Grav installation directory, or click the Clear Cache button in the Admin interface. The CLI documentation details other cache management options.

Navigate to your modular page in a browser. You should see all modules rendered in sequence as a single page. If modules do not appear, verify the underscore prefix on module folders, check that template files exist for your markdown filenames and confirm your parent modular.md lists the correct module names.

Working with the Admin Interface

The Grav Admin plugin streamlines modular page creation. Navigate to the Pages section in the admin interface and click Add. Select "Add Modular" from the options. Fill in the title and select your parent page. Choose the module template from the dropdown.

The admin interface automatically creates the module folder with the correct underscore prefix. You can then add content, configure settings and upload media through the visual editor. This approach reduces the chance of naming errors and provides a more intuitive workflow for content editors.

Practical Tips for Modular Pages

Use descriptive folder names that indicate the module's purpose. Names like 01._title, 02._intro, 03._call-to-action make the page structure immediately clear. Hyphens work well for multi-word module names such as 08._wales-isle-of-man or 13._practicalities-inspiration. This naming clarity helps when you return to edit the page later or when collaborating with others. The specific names you choose are entirely up to you as the theme developer.

Keep module-specific settings in each module's markdown front matter. Place page-wide settings like taxonomy and routing in the parent modular.md file. This separation maintains clean organisation and prevents configuration conflicts.

Use front matter variables to control module rendering behaviour. For example, adding grid: true to a module's front matter can signal your modular template to render that module within a grid layout rather than full-width. Similarly, flags like search: true or fullwidth: true allow your template to apply different rendering logic to different modules. This keeps layout control flexible without requiring separate module types for every layout variation.

Test your modular page after adding each new module. This incremental approach helps identify issues quickly. If a module fails to appear, check the folder name starts with an underscore, verify the template exists and confirm the parent configuration includes the module in its custom order list.

For development work, make cache clearing a regular habit. Grav caches page collections and compiled templates, which can mask recent changes. Running bin/grav clear-cache after modifications ensures you see current content rather than cached versions.

Understanding Module Rendering

The parent page's modular.html.twig template controls how modules assemble. Whilst many themes use a simple loop structure, you can implement more sophisticated rendering logic when building a theme from scratch. Here is an example that demonstrates conditional rendering based on module front matter:

{% extends 'partials/home.html.twig' %}
{% block content %}
  {% set modules = page.collection({'items':'@self.modular'}) %}
  {# Full-width modules first (search, main/outro), then grid #}
  {% for m in modules %}
    {% if not (m.header.grid ?? false) %}
      {% if not (m.header.search ?? false) %}
            {{ m.content|raw }}
      {% endif %}
    {% endif %}
  {% endfor %}
  <div class="row g-4 mt-3">
    {% for m in modules %}
      {% if (m.header.grid ?? false) %}
        <div class="col-12 col-md-6 col-xl-4">
          <div class="h-100">
            {{ m.content|raw }}
          </div>
        </div>
      {% endif %}
    {% endfor %}
  </div>
  {# Move search box to end of page  #}
  {% for m in modules %}
    {% if (m.header.search ?? false) %}
          {{ m.content|raw }}
    {% endif %}
  {% endfor %}
{% endblock %}

This template demonstrates several useful techniques. First, it retrieves the module collection using page.collection({'items':'@self.modular'}). The template then processes modules in three separate passes. The first pass renders full-width modules that are neither grid items nor search boxes. The second pass renders grid modules within a Bootstrap grid layout, wrapping each in responsive column classes. The third pass renders the search module at the end of the page.

Modules specify their rendering behaviour through front matter variables. A module with grid: true in its front matter renders within the grid layout. A module with search: true renders at the page bottom. Modules without these flags render full-width at the top. This approach provides precise control over the layout whilst keeping content organised in separate module files.

Each module still uses its own template (like text.html.twig or feature.html.twig) to define its specific HTML structure. The modular template simply determines where on the page that content appears and whether it gets wrapped in grid columns. This separation between positioning logic and content structure keeps the system flexible and maintainable.

When to Use Modular Pages

Modular pages work well for landing pages, home pages and single-page sites where content sections need to appear in a specific order. They excel at creating modern, scrollable pages with distinct content blocks like hero sections, feature grids, testimonials and call-to-action areas.

For traditional multipage sites with hierarchical navigation, regular pages prove more suitable because modular pages do not support child pages in the conventional sense. Instead, choose modular pages when you need a unified, single-URL presentation of multiple content sections.

Related Reading

For further information, consult the Grav Modular Pages Documentation, Grav Themes and Templates guide and the Grav Admin Plugin documentation.

Understanding Bootstrap's twelve column grid for clean layouts

8th February 2026

When it comes to designing your page structure using Bootstrap, you need to work within a twelve column grid. For uneven column widths, you need to make everything add up, while it is perhaps simpler when every column width is the same.

For instance, encountering a case of the latter when porting a website landing page as part of a migration from Textpattern to Grav meant having evenly sized columns, with one block for each section. To make everything up to twelve, two featured article blocks were added. What follows is a little about why that choice was made.

How the Twelve-Column System Works

Bootstrap's grid divides into twelve columns The number twelve was chosen because it has more divisors than any number before it or after it, up to sixty, making it exceptionally flexible for layout work.

<!-- Two across -->
<div class="col-12 col-md-6">...</div>

<!-- Three across -->
<div class="col-12 col-md-6 col-xl-4">...</div>

<!-- Classic blog layout: sidebar and main content -->
<div class="col-md-3">Sidebar</div>
<div class="col-md-9">Main content</div>

The column classes work by specifying how many of the twelve columns each element should span. For example, a col-6 class spans six columns (half the width), whilst col-4 spans four columns (one third). In the classic blog layout, this means using col-3 for a sidebar (one quarter width) and col-9 for main content (three quarters width). Other common combinations include col-8 and col-4 (two thirds and one third), or col-2 and col-10 (one sixth and five sixths).

For consistent column widths, certain numbers divide cleanly: col-12 (full width, one across), col-6 (half width, two across), col-4 (one third width, three across), col-3 (one quarter width, four across), col-2 (one sixth width, six across) and col-1 (one twelfth width, twelve across). When using 2, 3, 4, 6 or 12 blocks with these classes, the grid divides evenly. However, with other numbers, challenges emerge. For instance, eleven blocks in three columns leaves two orphans, whilst seven blocks in two columns creates uneven rows.

Bootstrap Breakpoints Explained

Bootstrap 5 defines six responsive breakpoints for different device categories:

Breakpoint Screen Width Class Modifier
Extra small Below 576px None (just col-*)
Small 576px and above col-sm-*
Medium 768px and above col-md-*
Large 992px and above col-lg-*
Extra large 1200px and above col-xl-*
Extra extra large 1400px and above col-xxl-*

These breakpoints cascade upwards. A class like col-md-6 applies from 768 pixels and continues to apply at all larger breakpoints unless overridden by a more specific class like col-xl-4. This cascading behaviour allows responsive layouts to be built with minimal markup, where each breakpoint only needs to specify what changes, rather than repeating the entire layout definition.

Putting It Into Practice

When column widths are equal, the implementation uses Bootstrap grid classes with a three-tier responsive system so that each block receives consistent treatment, with padding, borders and hover effects. Here is some boilerplate code showing how this can be accomplished:

<div class="row g-4 mt-3">
  <div class="col-12 col-md-6 col-xl-4">
    <div class="h-100">
      <h3 class="mb-3">Irish Encounters</h3>
      <p style="text-align:center" class="mt-3">
        <img class="w-100 rounded" src="...">
      </p>
      <p>From your first arrival to hidden ferry crossings...</p>
      <p>
        <a href="/travel/ireland" class="btn btn-secondary mt-3 mb-3 shadow-none stretch">
          Go and Have a Look: 12 Articles to Savour
        </a>
      </p>
    </div>
  </div>
  <!-- Repeat for 9 more destination blocks -->
  <!-- Then 2 featured article blocks -->
</div>

Two Bootstrap utility classes proved particularly useful here. Firstly, the h-100 class sets the height to 100% of the parent container, ensuring all blocks in a row have equal height regardless of content length. Meanwhile, the w-100 class sets the width to 100% of the parent container, making images fill their containers whilst maintaining aspect ratio when combined with responsive image techniques. Together, these help create visual consistency across the grid.

The responsive behaviour works as follows for twelve blocks:

Screen Width Class Used Number of Columns Number of Rows
Below 768px col-12 1 12
768px and above col-md-6 2 6
1200px and above col-xl-4 3 4

The g-4 class adds consistent guttering between blocks across all breakpoints and is part of Bootstrap's spacing utilities, where the number (4) corresponds to a spacing value from Bootstrap's spacer scale. To accomplish this, the class applies gap spacing both horizontally and vertically between grid items, creating visual separation without needing to add margins to individual elements. This ensures blocks do not sit flush against each other whilst maintaining consistent spacing throughout the layout.

Taking Stock

Bootstrap's twelve-column grid works cleanly for certain block counts (1, 2, 3, 4, 6 and 12). In contrast, other numbers create visual imbalance in multi-column layouts. For this reason, the grid system should inform content decisions early in the planning process. Ultimately, planning block counts around the grid creates more harmonious layouts than forcing arbitrary numbers into place.

In this case, twelve blocks divided cleanly into the three-column grid, where other numbers would have created orphans. Beyond solving the layout challenge, featured articles provided value by drawing attention to important content whilst resolving the constraints of the grid system. The key takeaway is that content planning and grid design work together rather than in opposition.

Related Reading

For further exploration of these concepts, the Bootstrap Grid System documentation provides comprehensive coverage of the twelve-column system and its responsive capabilities. The Flexbox utilities documentation covers alignment and spacing options that complement the grid system.

A little thing with Outlook

24th July 2010

When you start working somewhere new like I have done, various software settings that you have had at your old place of work don't automatically come with you, leaving you to scratch your head as to how you had things working like that in the first place. That's how it was with the Outlook set up on my new work PC. It was setting messages as read the first time that I selected them, and I was left wondering to set things up as I wanted them.

From the menus, it was a matter of going to Tools > Options and poking around the dialogue box that was summoned. What was then needed was to go to the Other Tab and Click on the Reading Pane Button. That action produced another dialogue box with a few check-boxes on there. My next step was to clear the one with this label: Mark item as read when selection changes. While there's another tick box that I left unchanged: Mark items as read when viewed in Reading Pane; that's inactive by default anyway.

From my limited poking around, these points are as relevant to Outlook 2007 as they are to the version that I have at work, Outlook 2003. Going further back, it might have been the same with Outlook 2000 and Outlook XP too. While I have yet to what Outlook 2010, the settings should be in there too, though the Ribbon interface might have placed them somewhere different. It might be interesting to see if a big wide screen like what I now use at home would be as useful to the latest version as it is to its immediate predecessor.

  • The content, images, and materials on this website are protected by copyright law and may not be reproduced, distributed, transmitted, displayed, or published in any form without the prior written permission of the copyright holder. All trademarks, logos, and brand names mentioned on this website are the property of their respective owners. Unauthorised use or duplication of these materials may violate copyright, trademark and other applicable laws, and could result in criminal or civil penalties.

  • All comments on this website are moderated and should contribute meaningfully to the discussion. We welcome diverse viewpoints expressed respectfully, but reserve the right to remove any comments containing hate speech, profanity, personal attacks, spam, promotional content or other inappropriate material without notice. Please note that comment moderation may take up to 24 hours, and that repeatedly violating these guidelines may result in being banned from future participation.

  • By submitting a comment, you grant us the right to publish and edit it as needed, whilst retaining your ownership of the content. Your email address will never be published or shared, though it is required for moderation purposes.