Auto-generated Projects Page

I wanted a page that would list all of my projects, but I didn't want to have the additional effort of maintaining it and I didn't want to risk it going out of date. Fortunately, with Twig, it's prety easy to have a page be automatically generated based on it's children.

Now, all I need to do is create child page within my "Projects" folder, give it a short summary to describe what the project is, which is useful anyway, and then the entry will be automatically added to the main Projects page.

At some point, I might make this a bit more complex, with sections for different types of projects (eg. Python, Grav, Raspberry Pi) but for now this suits my needs.

Setup

First, you'll need to create a page and make sure that you enable Twig processing in the header.

process:
    twig: true

Simple example

The Grav Documentation provides some basic code to generate a list of projects from a set of child pages, so this is where I took my inspiration from.

# All Projects
<ul>
{% for p in page.find('/projects').children if p != page %}
<li><a href="{{p.url}}">{{ p.title }}</a></li>
{% endfor %}
</ul>

The above code starts a unordered list, and then for each child page, prints a list item consisting of the hyperlinked page title. The key parts of the code are:

  • "p" is a temporary variable used to refer to the child pages within this for loop.
  • page is the current page.
  • .find('/projects') looks for a page named "projects".
  • .children selects the children of the found page.
  • if p != page removes the current page from the selection.
  • p.url is simply the URL of the child page "p" and similarly p.title is the title of that page.

Improved example

My aim is to create a list of child pages with summaries, so firstly, I can change page.find('/projects').children to page.children as I'm interested only in the children of the page that I put that code on. Since the children on the current page doesn't contain the current page, the original if-statement is redundant, but I actually do care about whether the pages have been published or not, so this gets changed to p.published.

I also want to do a little bit of reformatting of my summaries, since I want them to display at level 2 (h2) on the summary, but still be level 1 (h1) on the child page itself.

{% for p in page.children if p.published %}
---
{% set headertag ="<h2 class='project-summary'><a href=%s>"|format(p.url) %}
{{p.summary | replace({'<h1>': headertag, '</h1>': '</a></h2>', '<p>': '<p class="project-summary">'})}}
{% endfor %}
  • page.children is the set of children of the current page (i.e. the page the code is on).
  • if p.published checks that "published: true" appears in the header of the child page. Unpublished pages are then skipped.
  • "...<a href='%s'>"|format(p.url) substitutes the child page URL into the string, replacing %s.
    • set is used to set a variable named "headertag" to this.
  • p.summary is the summary of the child page.
  • |replace({'<h1>': headertag, ...}) replaces all instances the <h1> tag with the string given by headertag, and so on through the list.

Note: CSS for the "project-summary" class for both p and h2 should be set in your user/themes/<theme-name>/css/custom.css (or equivalent) file if you don't want the default styling.

Structure of the child pages

The child page doesn't need to be particularly special. It has the usual frontmatter, starts with a header (#) and I've used a summary delimiter === so that I can have an explicitly defined summary at the start of my project page. You could rely simply on your default summary settings as long as you don't have any headings or other things in the summary - these would make the project page look really weird if they got pulled through!

---
[Your usual header content]
---

# Page title

Summary text

===

Rest of the page