Custom snippets for specific page types

If you are like me and you make heavy use of snippets the following Component might prevent you from some headaches.

Most of my Kirby sites use many different content types, the visual representation however is often quite similar. I have a Drupal background and somehow like that you have the change to overwrite a template based on the maschine name of a content type. For example to customize the markup for a custom post type article you can – in Drupal – overwrite the default template (node.tpl.php) by copying the file and renaming it to node--article.tpl.php. I wanted to have something similar in Kirby and since version 2.3 it is possible to implement that behavior with a custom Snippet Component.

// site/plugins/theme/components/snippet.php

namespace Weidner\Theme\Component;

use A;
use PageAbstract;
use Str;
use Tpl;
use Traversable;

/**
 * Custom Snippet Builder Component
 *
 * @author  Daniel Weidner <hallo@danielweidner.de>
 * @package Weidner\Theme
 * @since   1.0
 */
class Snippet extends \Kirby\Component\Snippet {

  /**
   * Renders the snippet with the given data.
   *
   * @param  string  $name
   * @param  array   $data
   * @param  boolean $return
   * @return string
   */
  public function render($name, $data = [], $return = false) {

    // Split snippet information
    $file = basename($name);
    $dir = str::contains($name, '/') ? dirname($name) . DS : '';

    // Collect file suggestions based on properties of the page object
    $suggestions = [];

    if ($item = $this->item($data)) {

      $id = $item->id();
      $type = $item->intendedTemplate();

      $suggestions[] = $dir . $file . '--' . str::slug($id);
      $suggestions[] = $dir . $file . '--' . $type;
      $suggestions   = array_filter(array_unique($suggestions));

    }

    // Ensure the default snippet file is always the last candidate in the list
    $suggestions[] = $name;

    // Load the first candidate that actually exists in the site’s snippet folder
    foreach ($suggestions as $suggestion) {
      if ($file = $this->kirby->registry->get('snippet', $suggestion)) {
        return tpl::load($file, $data, $return);
      }
    }

    // Snippet file not found
    return '';
  }

  /**
   * Try to find a page object in the variable scope of the snippet.
   *
   * @param array $data Collection of snippet variables.
   * @return Page|boolean
   */
  protected function item($data) {

    // Test whether the given item is a page object
    $item = a::get($data, 'item');

    if ($item instanceof PageAbstract) {
      return $item;
    }

    // Test whether one of the given items is a page object
    $items = a::get($data, 'items');

    if (is_array($items) || $items instanceof Traversable) {
      foreach ($items as $item) {
        if ($item instanceof PageAbstract) {
          return $item;
        }
      }
    }

    return false;

  }
}

In order to use the new component you have to register this new Component somewhere in a custom plugin:

// site/plugins/theme/theme.php

load([
  'weidner\\theme\\component\\snippet' => __DIR__ . DS . 'components' . DS . 'snippet.php',
]);

$kirby->set('component', 'snippet', 'Weidner\Theme\Component\Snippet');

Did you know, you can pass the snippet function a page object and it will allow your template file to access it via a variable called $item? The proposed component uses the same convention. Every time you pass a page object as item or a page collection as items the component tries to identify the type of your page objects ($page->indentedTemplate()) and tries to load a custom snippet for you if it exists. If it does not, it uses the default snippet name as fallback.

Now you can write the following in your template:

// site/templates/default.php

<?php snippet('header'); ?>

<main>
  <?php snippet('page', [ 'item' => $page]); // or simply snippet('page', $page) ?>
</main>

<?php snippet('footer'); ?>

If the template specified for your page object is book and the uri is books/moby-dick, Kirby tries to load one of the following snippets in the given order:

  • site/snippets/page--books-moby-dick.php
  • site/snippets/page--book.php
  • site/snippets/page.php

Now my different content types can share the same snippet file, but I can decide for each type individually whether I want to use the default or some custom markup . It would be easy to extend the component and include custom field values like a format field in the hierarchy as well:

// site/plugins/theme/components/snippet.php
// ...
  if ($item->format()->exists() && $item->format()->isNotEmpty()) {
    $suggestions[] = $suggestions[] = $dir . $file . '--' . str::slug($item->format()->value());
  }
// ..

Hope this might be useful for someone else. I would appreciate your feedback.

5 Likes

Is that plugin available on GitHub? If so, you may want to publish a link to it here and change the category to β€œplugin”.

No, not yet. This is part of a plugin I use for theme development. It ships with a few custom template function. I was not sure, if anyone else might find this useful.

I you feel like sharing there’s definitely a lot of interest in what you built! Looks promising!

Yes, I think it might be useful for other people as well. If you provide a GitHub repo for it, the plugin can be listed at http://getkirby-plugins.com and in @jenstornell’s plugin repo. Makes more sense than listing the files here, I think.

1 Like

I will think a bit more about it and ship the component together with some other methods and extensions I find useful during theme development. Suggestions are always welcome.