Skip to content

Kirby 3.9.8

Creating pages from frontend

You will learn how you can use Kirby's API to create pages based on user input entered into a form on the frontend. A typical use case for this is an event or newsletter registration form.

The content structure

Let's first have a look at the content structure: We have an events page with the events stored as subpages and a success page which we use later to display a success message.

  • content
    • 1_events
      • 1_event-a
        • event.txt
      • 2_event-b
        • event.txt
      • 3_event-c
        • event.txt
      • events.txt
    • success
      • success.txt
    • ...

The registration form snippet

First, we need the event registration form, which we will save in a snippet called registration-form.php.

/site/snippets/registration-form.php
<form class="registration-form" action="<?= $page->url() ?>" method="POST">

  <div class="form-element">
    <label for="name">Name <abbr title="required">*</abbr></label>
    <input type="text" id="name" name="name" value="<?= $data['name'] ?? null ?>" required/>
  </div>

  <div class="form-element">
    <label for="company">Company</label>
    <input type="text" id="company" name="company" value="<?= $data['company'] ?? null ?>"/>
  </div>

  <div class="form-element">
    <label for="email">Email <abbr title="required">*</abbr></label>
    <input type="email" name="email" id="email" value="<?= $data['email'] ?? null ?>" required/>
  </div>

  <div class="form-element">
    <label for="message">Message</label>
    <textarea name="message" id="message"><?= $data['message'] ?? null ?></textarea>
  </div>

  <div class="honey">
     <label for="website">If you are a human, leave this field empty</label>
     <input type="website" name="website" id="website" value="<?= isset($data['website']) ? esc($data['website']) : null ?>"/>
  </div>

  <input class="registration-button" type="submit" name="register" value="Register" />

</form>

The form contains some form fields (name, company, email and message) and a honeypot field to ensure a minimum level of spam bot protection.

The honeypot field needs to be positioned off the screen via CSS. Therefore add these styles to your stylesheet (you can change the class and styling).

.honey {
  position: absolute;
  left: -9999px;
}

When we submit the form, the action attribute calls the URL of the current page. This allows us to process the input data in the event.php controller. We will get to this in a bit.

The form snippet is included the event.php template because we want to render it on every event page that uses this template.

The event.php template

The template renders the content of each event page. We want to add the registration form snippet below.

Additionally, we will display alerts if the user filled in the form incorrectly or the registration failed.

/site/templates/event.php
<?php snippet('header') ?>

<main>
  <article class="event">
    <header class="event-header intro">
      <h1><?= $page->title() ?></h1>
      <time class="event-date"><?= $page->date()->toDate('d F Y') ?></time>
    </header>

    <div class="registration">
      <header class="note-header">
        <h2>Register for this event</h2>
      </header>

      <?php
      // if the form input is not valid, show a list of alerts
      if ($alert) : ?>
      <div class="alert">
        <ul>
          <?php foreach ($alert as $message): ?>
          <li><?= kirbytext($message) ?></li>
          <?php endforeach ?>
        </ul>
      </div>
      <?php endif ?>
      <?php snippet('registration-form', compact('data')); ?>
    </div>
  </article>
</main>

<?php snippet('footer') ?>

The event.php controller

/site/controllers/event.php
<?php

return function ($kirby, $page) {

    // if the form has been submitted…
    if ($kirby->request()->is('POST') && get('register')) {

        // check the honeypot and exit if is has been filled in
        if(empty(get('website')) === false) {
            go($page->url());
            exit;
        }

        $data = [
            'name'    => get('name'),
            'company' => get('company'),
            'email'   => get('email'),
            'message' => get('message')
        ];

        $rules = [
            'name'  => ['required'],
            'email' => ['required', 'email'],
        ];

        $messages = [
            'name'  => 'Please enter your <a href="#name">name</a>',
            'email' => 'Please enter a valid <a href="#email">email address</a>',
        ];

        // some of the data is invalid
        if ($invalid = invalid($data, $rules, $messages)) {
            $alert = $invalid;

        } else {

            // authenticate as almighty
            $kirby->impersonate('kirby');

            // everything is ok, let's try to create a new registration
            try {
                // we store registrations as subpages of the current page
                $registration = $page->createChild([
                    'slug'     => md5(str::slug($data['name'] . microtime())),
                    'template' => 'registration',
                    'content'  => $data
                ]);

                if ($registration) {
                    // store referer and name in session
                    $kirby->session()->set([
                        'referer' => $page->uri(),
                        'regName'  => esc($data['name'])
                    ]);
                    go('success');
                }

            } catch (Exception $e) {
                $alert = ['Your registration failed: ' . $e->getMessage()];
            }
        }
    }

    // return data to template
    return [
        'alert' => $alert ?? null,
        'data'  => $data ?? false,
    ];
};

Since this is a lot of stuff, let's go through this one step at a time:

The form evaluation starts once we receive a POST request. First, we check if a bot was trapped in our honeypot. If this is the case, we send him back to the page and stop script execution.

We then fetch the values of each form field from the POST data into the $data variable. We can use the get() helper to do so:

$data = [
    'name'    => get('name'),
    'company' => get('company'),
    'email'   => get('email'),
    'message' => get('message')
];

Next, we check if all form fields have been filled according to our validation rules using the invalid() helper:

All fields are required and must be filled out.

  • The name field is required.
  • The email field is required and must contain a valid email address.
$rules = [
    'name'  => ['required'],
    'email' => ['required', 'email'],
];

We also want to tell the user what is wrong if the validation fails. We create an array of messages for each validated field:

$messages = [
    'name'  => 'Please enter your <a href="#name">name</a>',
    'email' => 'Please enter a valid <a href="#email">email address</a>',
];

You can change these rules based on the type of data you want to obtain and use Kirby's validators or your own custom validators to make sure you get the desired data.

To prevent garbage data, you may want to use validators on the other fields as well, for example by limiting input to a given character set (using regex patterns).

If all went well, we authenticate using the almighty kirby user and try to create a new subpage within a try/catch block, which allows us to react on possible errors. We store all registrations as subpages of the current event page. If the registration was successfully created, two things happen:

  1. We store the current page URI and the name in the session.
  2. We redirect to the success page.
// authenticate as almighty
$kirby->impersonate('kirby');

// everything is ok, let's try to create a new registration
try {
    // we store registrations as subpages of the current page
    $registration = $page->createChild([
        'slug'     => md5(str::slug($data['name'] . microtime())),
        'template' => 'registration',
        'content'  => $data
    ]);

    if ($registration) {
        // store referer and name in session
        $kirby->session()->set([
            'referer' => $page->uri(),
            'regName'  => esc($data['name'])
        ]);
        go('success');
    }

} catch (Exception $e) {
    $alert = ['Your registration failed: ' . $e->getMessage()];
}

If the registration fails, we add the error message to our alerts variable.

The success page and a plugin

If the registration was successful, the user is redirected to the success page.

/content/success/success.txt
Title: Success

----
Text:

Hello {{ name }},
Thank your for registering for **{{ event }}**.

You will receive confirmation and further information soon via email to the address you provided.

If you want to register for another event, go back to the <a href="https://v39.getkirby.com/events">Events overview page</a>.

In the text field, we include a placeholder for the event title to customize it a little. To actually display the title, we use a kirbyTags:after hook in a plugin.

The success page is rendered using the default.php template.

A little plugin to customize the message

In the plugin, we replace the name and event placeholders in the text with the data we stored in the session.

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

Kirby::plugin('eventkit/event', [
    'hooks' => [
        'kirbytags:after' => function ($text, $data, $options) {
            $session = kirby()->session();

            if ($location = $session->get('referer')) {
                if ($page = page(urldecode($location))) {
                    $title = $page->title();
                }
            }

            return Str::template($text, [
                'event' => $title ?? '',
                'name'  => $session->get('regName') ?? ''
            ]);
        }
    ],
]);

Download example

For a working example, download the demo "Eventkit".