🚚 How to migrate to Kirby Tips & tutorials
Skip to content

Placeholders

Placeholders are a powerful way to include repeatedly used text snippets like company names, email addresses or phone numbers, but also dynamic stuff like galleries into your text fields. When you want to change this text snippet, you can do that in one place and it will automatically update in all places where the placeholder is used. It's also useful if these text snippets need a special styling or markup. Placeholders thus help you to keep information consistent and maintainable.

Adding placeholders in text fields

By default, such placeholders are wrapped in double curly braces like this:

Text:

{{ company }} was founded in 2005 with the intention of making a lot of money quickly.

Today, {{ company }} rules the world. Contact us at {{ phone }}.

If you want to use different start and end characters instead of the curly braces, you can control this using the $start and $end parameters of the Str::template() method.

Replacing placeholders in templates

In your templates, you can replace such placeholders on the fly using the Str::template() method:

<?= Str::template($page->text()->kt(), [
    'company' => 'Great Company',
    'phone'   => '+01 123 1234567'
]) ?>

This works great if you only need a few placeholders in a few limited places.

However, if you want to use placeholders all over the place, you don't want to repeat each replacement in every template. So how can we make this more versatile?

Defining the replacements in config.php

If you know in advance what text snippets you need and what they should be replaced with, you can define them in your configuration file:

/site/config/config.php
<?php

return [
  'placeholders' => [
    'company' => 'Great company',
    'phone'   => '+01 123 1234567',
  ],
];

With this in place, we can pass this array to the Str::template() method:

<?= Str::template($page->text()->kt(), option('placeholders')) ?>

Now, if we want to add additional placeholders, we can do that in the config.

But hey, this is still quite limited.

Let the user define the placeholders

Maybe your editors would prefer to have control over the types of placeholders they need and what they should be replaced with. A structure field lends itself well to this purpose, and the site.yml blueprint is a good place to define site-wide placeholders.

/site/blueprints/site.yml
placeholders:
  label: Text replacements
  type: structure
  fields:
    key:
      label: Placeholder key
      type: text
      help: >
        The key is what you use in your textfields, e.g. {{ company }}
    value:
      label: Replacement text
      type: textarea
      help: >
        The replacement text is the text that will replace your keyholders.

To get an array of options from this field, we create a toOptions() field method in a plugin:

/site/plugins/placeholders/index.php
<?php

Kirby::plugin('cookbook/placeholders', [
    'fieldMethods' => [
        'toOptions' => function($field) {
            $result = [];
            foreach ($field->toStructure() as $option) {
                if ($option->key()->isNotEmpty() && $option->value()->isNotEmpty()) {
                   $result[$option->key()->value()] = $option->value()->html();
                }
            }
            return $result;
        }
    ]
]);

We can now get the replacement array from the field above like this:

<?= Str::template($page->text()->kt(), $site->placeholders()->toOptions()) ?>

Custom field method

Wouldn't it be nice to create our own field method and write it like this?:

<?= $page->text()->kt()->replace([
    'company' => 'Great company',
    'phone'   => '+01 123 1234567',
]) ?>

… or connected to our structure field:

<?= $page->text()->kt()->replace($site->placeholders()->toOptions()) ?>

Let's put this in our plugin:

/site/plugins/placeholders/index.php
<?php

Kirby::plugin('cookbook/placeholders', [
    'fieldMethods' => [
        'replace' => function ($field, array $placeholders = []) {

            // replace the field value
            $field->value = Str::template($field->value, $placeholders);

            return $field;
        }
    ]
]);

Wrapping the replacement code in a hook

We can automate it even more, though. A hook replaces all the placeholders automatically, so that we don't even have to call our method in templates anymore:

/site/plugins/placeholders/index.php
<?php

Kirby::plugin('cookbook/placeholders', [
    'hooks' => [
        'kirbytext:before' => function ($text) {
            return Str::template($text, site()->placeholders()->toOptions());
        }
    ]
]);

For cases that don't use kirbytext() (layout field, blocks field, writer field etc-), you can apply a page.render:after hook instead that replaces your placeholders all over your site in the rendered HTML, independent of the type of field where the placeholder is used:

/site/plugins/placeholders/index.php
<?php

Kirby::plugin('cookbook/placeholders', [
    'hooks' => [
        'page.render:after' => function (string $contentType, string $html) {
            if ($contentType !== 'html') {
                return $html;
            }

            return Str::template($html, site()->placeholders()->toOptions());

        },
    ]
]);

The replace() field method is no longer needed when we use this hook.

The final plugin

For the final plugin, we want to get rid of some of the hardcoded stuff and leave it to the developer to decide where the replacements are defined, in the config or by the editor in a structure field, or even allow overriding/extending the replacements defined in config.php.

Config options for plugins should always follow the naming convention: yourName.pluginName.optionName

/site/plugins/placeholders/index.php
<?php

Kirby::plugin('cookbook/placeholders', [
    'fieldMethods' => [
        'replace' => function ($field, array $placeholders = []) {

            // replace the field value
            $field->value = Str::template($field->value, $placeholders);

            return $field;
        },
        'toOptions' => function($field) {
            $result = [];
            foreach ($field->toStructure() as $option) {
                // use only key/value pairs that are not empty
                if ($option->key()->isNotEmpty() && $option->value()->isNotEmpty()) {
                    // check if the block field is set to true and parse KirbyTags
                    if ($option->block()->toBool() === true) {
                        $result[$option->key()->value()] = kirbytags($option->value());
                    } else {
                        $result[$option->key()->value()] = $option->value()->html();
                    }
                }
            }
            return $result;
        }
    ],
    // Don't use both hooks but only one as required, remove or comment the one you don't need
    'hooks' => [
        'kirbytags:before' => function ($text) {

            // set the options from the config as defaults
            $defaults = option('cookbook.placeholders.replacements', []);
            $options  = [];

            // if options from fields are set to true, get options from field
            if (option('cookbook.placeholders.field')) {
                $field   = option('cookbook.placeholders.field', 'placeholders');
                $options = site()->{$field}()->toOptions();
            }

            // merge defaults and options
            $options = array_merge($defaults, $options);

            return Str::template($text, $options);
        },
       'page.render:after' => function (string $contentType, string $html) {
            if ($contentType !== 'html') {
                return $html;
            }

            return Str::template($html, site()->placeholders()->toOptions());
        },
    ]
]);

Now, we can control the behavior of the plugin through additional config options:

/site/config/config.php
<?php
return [
    'cookbook.placeholders.replacements' => [
        'company' => 'Great company',
        'phone'   => '+01 123 1234567',
    ],
    'cookbook.placeholders.field' => 'placeholders'
];

We can set some defaults in the configuration file and optionally replace or extend them with options from the placeholders field.

We can now even allow block level replacements and control them via an additional block toggle field within the structure field definition:

placeholders:
  label: Text replacements
  type: structure
  fields:
    key:
      label: Placeholder key
      type: text
      help: >
        The key is what you use in your textfields, e.g. {{ company }}
    block:
      type: toggle
      text: Block type replacement?
    value:
      label: Replacement text
      type: textarea
      help: >
        The replacement text is the text that will replace your keyholders.

If you rename the fields within the structure field or don't want to put the field into the site options, make sure to adapt the plugin code accordingly.

Author