List, Detail and Beyond

The list and detail pattern is very common on the web. A basic implementation of this pattern is to display a list of items and by clicking on an item you display the item details (often on a separate page).

This pattern is commonly used on Perch sites too. If you have set up a blog with a listing page and a post detail page before, you have implemented a list/detail pattern.

When it comes to Perch and Runway, there are multiple ways of doing this.

Core (no add-ons) - these can be anything such as services, case studies, employees, articles, etc:

With the use of official Perch apps such as:

In this post I’ll go through:

Notes:

Firstly, this is a long post. It doesn’t mean we’re implementing things in a complex way; there’s just a lot to go over.

While there’s no complex implementation, you may find this post most helpful if you have used Perch and implemented a list/detail pattern before as I don’t explain every concept in depth.

There are some basic examples in the docs:


Creating the list

Any list and detail approach needs at least 2 templates:

item.html is just a generic name. It can be named after what the item represents.

For instance if you are creating a Collection of events you may want to name it event.html. If you check the official Blog app it is named post.html, and in the official Shop app it is named product.html.

These 2 templates are the minimum you need.

Separating the edit form template from the display template

One of Perch’s features is that you can use an edit form template as a display template too. This is a great feature, but sometimes it makes sense to separate your edit form templates from the display templates especially when you have long and complex edit forms.

So if we take the Blog app’s edit form template post.html as an example, it can be as short as 7 lines:

<perch:blog id="postTitle" type="text" label="Title" required="true" size="xl autowidth" />
<perch:blog id="image" type="image" width="320" height="240" crop="true" label="Image" />
<perch:blog id="excerpt" type="textarea" label="Excerpt" markdown="true" size="s" />
<perch:blog id="postDescHTML" type="textarea" label="Post" editor="markitup" markdown="true" size="xxl autowidth" required="true" />
<!--* Publishing *-->
<perch:blog id="postDateTime" type="date" label="Date" time="true" format="Y-m-d H:i:s" divider-before="Publishing" />
<perch:categories id="categories" set="blog" label="Categories" display-as="checkboxes" />

And then you can have a separate display template named something like post_detail.html or post_frontend.html which would include your HTML, author details, and category details. The display template can be as complex as you need.

You don’t have to do this, but I find it easier to maintain longer templates this way and it also allows me to freely order/group the fields in the edit form to acheive a nicer editing experience (I am aware of the order attribute, but I only find it helpful in shorter templates).

So we now have 3 templates:

  1. item.html - edit form template
  2. item_detail.html - item display template
  3. list.html - item listing template

We actually need 2 more (we’ll discuss these later):

  1. meta_head.html or seo.html
  2. item_in_sitemap.html

Keep it organised

A maintainable codebase needs to be organised. Perch by default has an organised structure that is very easy to follow as each app’s templates are placed in different folders.

If you ever used Perch Shop, you’ll notice how the templates are organised in sub-folders. For instance you have a folder for products and another folder for orders. Each folder practically represents a collection of items. Products can be displayed with a list/detail approach, and the same applies to orders.

When you create a new Collection or a mulit-item region, the templates need to be saved in /templates/content. Instead of saving the template straight in templates/content, save each collection’s template in templates/content/collection. This separates the collection/multi-item region templates from your general content templates and keeps your template folders organised.

For example, if you have a Collection called services, you would save its templates in templates/content/services.

So if you have multiple Collections and/or multi-item regions on a single site, your template folder can be like this:

templates/
  └ content/
    ├ case_studies/
      ├ item.html
      ├ item_detail.html
      ├ item_in_sitemap.html
      ├ seo.html
      └ list.html

    ├ team/
      ├ item.html
      ├ item_detail.html
      ├ item_in_sitemap.html
      ├ seo.html
      └ list.html

    └ services/
      ├ item.html
      ├ item_detail.html
      ├ item_in_sitemap.html
      ├ seo.html
      └ list.html

The above is really easy to follow and maintain. Even if you hand the project over to a different developer, they shouldn’t have a problem following your templates.


Pages

You can implement a list/detail pattern in a single page or two pages. On this post I’m only discussing the two-page approach.

So we’ll need 2 pages:

  1. One for the listing view
  2. One for the item detail view

Your master pages can be structured like this:

templates/
  └ pages/
    ├ collection/
      ├ index.php
      └ detail.php

So if you have a Collection called Services:

templates/
  └ pages/
    ├ services/
      ├ index.php
      └ detail.php

Instead of index.php you can use services.php, and instead of detail.php you can use service.php. This is just a matter of preference.

Displaying the list

On your services/index.php, you don’t have to do things differently from other pages. It can be as simple as this:

<?php
  perch_layout('global/top');

  perch_pages_breadcrumbs();

  perch_collection('Services', [
      'template' => 'services/list.html'
  ]);

  perch_layout('global/footer');
?>

In case you are unfamiliar with layouts, they are a common way of including repeated sections in Perch websites.

In the above example we are using the layout global/top.php to add what we need at the top of each page. It can be something like this:

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

<?php
        PerchSystem::set_var('page_title', perch_pages_title(true));
        perch_page_attributes(['template' => 'seo.html']);    
?>

    <link rel="stylesheet" type="text/css" href="/assets/css/styles.min.css">
</head>
<body>
    <nav>
        <?php perch_pages_navigation(); ?>
    </nav>

Above we are setting a variable with PerchSystem::set_var() to pass the page’s title into our page attributes template templates/pages/attributes/seo.html so we can display it among our other meta tags:

<title><perch:pages id="page_title" type="hidden" /> - Site Name</title>

Displaying the item

On your services/detail.php, we change our code to display the item instead of the listing:

<?php
  perch_layout('global/header');

  perch_pages_breadcrumbs();

  perch_collection('Services', [
    'template' => 'services/item_detail.html',
    'filter' => 'slug',
    'match' => 'eq',
    'value' => perch_get('s'),
  ]);

  perch_layout('global/footer');
?>

This is a dynamic page. We need to get the item’s slug from the URL to know which item to display.

If you are not familiar with perch_get(), you can check this blog post.

If you are not familiar with filtering, check the documentation as it shows some basic examples.

Now the item detail page is displayed, but we’re not quite done yet.

Meta and SEO

The meta tags in our <head> are being added by perch_page_attributes() in our global/top.php layout.

If we keep things as is, the template will output the exact same tags for all items on our item detail page. All items will have the same <title> and same <meta> tags. Why? Because we’re working on a single page and the perch_pages_attributes() outputs the page’s attributes not the item’s.

This is not we want. Here we want the item’s attributes not the page’s.

Layout variables to the rescue:

perch_layout('global/top', [
    'meta' => 'service',
]);

Above we are passing a layout variable called meta to the global/top.php layout in pages/services/detail.php. Now we need to modify the layout to make use of this variable.

We will add a switch statement to the layout. A switch statement works like this:

switch(expression) {
    case value1:
        // code executed if expression = value1
        break;

    case value2:
        // code executred if expression = value2
        break;

    default:
        // code executed if expression doesn't match any case
}

In our case the expression is the value of the layout variable meta and the default case is for our regular pages attributes. If the layout variable meta isn’t present or does not match any of our case values, the default statement will be executed.

switch(perch_layout_var('meta', true)) {
    default:
        PerchSystem::set_var('page_title', perch_pages_title(true));
        perch_page_attributes(['template' => 'seo.html']);
}

To add the services meta tags:

switch(perch_layout_var('meta', true)) {
    case 'service':
        perch_collection('Services', [
            'template'   => 'services/seo.html',
            'filter' => 'slug',
            'match' => 'eq',
            'value' => perch_get('s'),
        ]);
        break;

    default:
        PerchSystem::set_var('page_title', perch_pages_title(true));
        perch_page_attributes(['template' => 'seo.html']);
}

Now whenever we add a new item detail page all we need to do is change the value of the meta variable and add another case statement.

If you are working on a large site that has multiple item detail pages for blog posts, shop products and multiple collections, your global/top.php layout may look like this:

switch(perch_layout_var('meta', true)) {
  case 'blog-post':
    perch_blog_post_meta(perch_get('s'));
    break;

  case 'product':
    perch_shop_product(perch_get('s'), [
        'template' => 'products/seo',
    ]);
    break;

  case 'service':
    perch_collection('Services', [
        'template'   => 'services/seo',
        'filter' => 'slug',
        'match' => 'eq',
        'value' => perch_get('s'),
    ]);
    break;

  case 'case-study':
    perch_collection('Case Studies', [
        'template'   => 'services/seo',
        'filter' => 'slug',
        'match' => 'eq',
        'value' => perch_get('s'),
    ]);
    break;

  default:
    PerchSystem::set_var('page_title', perch_pages_title(true));

    perch_page_attributes([
      'template' => 'seo.html'
    ]);
    break;
}

I think this is easier to read and maintain than multiple if and elseif statements. Plus we only use one layout variable meta instead of using a different one for each page.

Similarly, if you keep the breadcrumbs as is all items will share the same output. It will display something like this for all items:

Home > Services > Service

We want to display the item title like this:

Home > Services > Printing

First you should go to the page settings and check the box “Hide from main navigation”. Now your breadcrumbs template outputs only:

Home > Services

Now we need to pass the item title to the breadcrumbs template. Let’s hard code it first:

$page_title = 'Printing';
PerchSystem::set_var('current_page_title', $page_title);

perch_pages_breadcrumbs();

The default breadcrumbs template looks like this:

<perch:before>
<ul class="breadcrumbs">
</perch:before>

    <li>
        <perch:if exists="perch_item_last">
            <perch:pages id="pageNavText" />
        <perch:else />
            <a href="<perch:pages id="pagePath" />"><perch:pages id="pageNavText" /></a> >
        </perch:if>
    </li>

<perch:after>
</ul>
</perch:after>

Add the following inside perch:after just before the closing </ul> tag:

<perch:if exists="current_page_title">
    <li>
      > <perch:pages id="current_page_title" />
    </li>
</perch:if>

Now whenever the variable current_page_title is set, the breadcrumbs template will output its value as the current page title.

So on each item detail page you need to get the item’s title. How you get the title will be slightly different whether the item is a Collection item, a blog post or a shop product.

For a blog post you can get it like this:

$page_title = perch_blog_post_field(perch_get('s'), 'postTitle', true);
PerchSystem::set_var('current_page_title', $page_title);

perch_pages_breadcrumbs();

For a Collection item, you can get it like this:

$service = perch_collection('Services', [
    'skip-template' => true,
    'filter' => 'slug',
    'match' => 'eq',
    'value' => perch_get('s'),
]);

$page_title = $service[0]['title'];
PerchSystem::set_var('current_page_title', $page_title);

perch_pages_breadcrumbs();

Unmatched slugs

If a user visits the page /services/some-service-that-doesnt-exist, they won’t be automatically be directed to a 404 page or your services listing page. This is because Perch gives you complete control on how you want your website to behave so it is up to you to implement what you want.

In order to redirect the user to a 404 page we need to check whether the item with the slug some-service-that-doesnt-exist actually exists. We need to do this before we output any HTML.

$service = perch_collection('Services', [
    'skip-template' => true,
    'filter' => 'slug',
    'match' => 'eq',
    'value' => perch_get('s'),
]);

if($services) {
    // exists - let's grab the title
    $page_title = $service[0]['title'];
    PerchSystem::set_var('current_page_title', $page_title);
} else {
    // doesn't exist - use error page
    PerchSystem::use_error_page(404);
    exit;
}

perch_layout('global/top', [
    'meta' => 'service',
]);

perch_pages_breadcrumbs();

Note that you need to add your 404 page.

Performance

Let’s look at our service detail page now:

$service = perch_collection('Services', [
    'skip-template' => true,
    'filter' => 'slug',
    'match' => 'eq',
    'value' => perch_get('s'),
]);

if($service) {
    // exists - let's grab the title
    $page_title = $service[0]['title'];
    PerchSystem::set_var('current_page_title', $page_title);
} else {
    // doesn't exist - use error page
    PerchSystem::use_error_page(404);
    exit;
}

perch_layout('global/top', [
    'meta' => 'service',
]);

perch_pages_breadcrumbs();

perch_collection('Services', [
    'template' => 'services/item_detail.html',
    'filter' => 'slug',
    'match' => 'eq',
    'value' => perch_get('s'),
]);

perch_layout('global/footer');

You see we’re calling perch_collection() twice. We can modify our code so we only call it once for a better performance. We’ll make use of the skip-template and return-html options:

$service = perch_collection('Services', [
    'filter' => 'slug',
    'match' => 'eq',
    'value' => perch_get('s'),
    'template' => 'events/details',
    'skip-template' => true,
    'return-html' => true,
]);

if(isset($service[0])) {
    // exists - let's grab the title
    $page_title = $service[0]['title'];
    PerchSystem::set_var('current_page_title', $page_title);
} else {
    // doesn't exist - use error page
    PerchSystem::use_error_page(404);
    exit;
}

perch_layout('global/top', [
    'meta' => 'service',
]);

perch_pages_breadcrumbs();

// output the item
echo $service['html'];

perch_layout('global/footer');

I often don’t see a significant difference in performance, but any small performance improvement is a win on the web. This may be more relevant if you have complex pages with lots of database queries.


Sitemap

Most things in Perch are not done for you - including the sitemap. As Heydon Pickering puts it in his (2012) blog post A Perch XML Sitemap:

In Perch, you have to be a real developer and do it mostly from scratch.

If you never added a sitemap to a Perch website before, check Clive Walker’s post Creating Google sitemaps with Perch.

You can use perch_pages_navigation() to add your pages to the sitemap. However, you should know by now that not all Perch content lives on pages. You don’t need to add a page for every single item. Instead, you set up dynamic pages that can display different items based on the URL.

perch_pages_navigation() is not able to output the item URLs from your dynamic pages. It can output the actual pages such as /post.php, /event.php and /product.php. But you want it to output the links of your blog posts, your events and products.

First you should hide these dynamic pages from main navigation (done via the control panel) so perch_pages_navigation() won’t output them at all. Then add each collection of items separatly.

A basic sitemap look like this:

<?php
header ("Content-Type: application/xml");
include('perch/runtime.php'); // you don't need this line if you are using Runway
?>
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<?php
    /* Pages */
    perch_pages_navigation([
        'template' => 'page_in_sitemap.html',
        'add-trailing-slash' => true,
        'flat' => true,
        'hide-extensions' => true
    ]);
    /* Blog */
    perch_blog_custom([
        'template' => 'post_in_sitemap.html',
        'sort'=>'postDateTime',
        'sort-order'=>'DESC',
        'count' => 3000,
    ]);
echo '</urlset>';
?>

You can add more items such as Collection items:

perch_collection('Services', [
        'template' => 'item_in_sitemap.html',
]);

If you remember our 5 essential templates to a list/detail pattern, item_in_sitemap.html is one of them. For a Collection the item_in_sitemap template can be something like this:

<url>
  <loc>https://www.example.com/services/<perch:content id="slug" /></loc>
  <changefreq>monthly</changefreq>
  <priority>0.5</priority>
</url>

Last words

While I mainly used Collections and Blog posts in the code examples, you can apply what you learned here for any list/detail pattern whether it is a multi-item region, Shop products or Gallery albums.

Remember Perch does not mess with your website code in any way and it doesn’t add any extra code by default. That’s why you have to do a little more than just displaying your items. You have a plain canvas. If you want it, implement it.

link