Lauri Liimatta

Blog Home | Portfolio

How to create a comments system for KirbyCMS

I recently needed to create a comments section for a KirbyCMS website and I'm sharing it here because I think other people could find it useful as well. This cookbook covers a lot of the same functionality but I think there's enough differences to make my version worth sharing. You could easily tweak this code a bit and turn it into some other type of user generated content. Without further ado let's get into it!

Note: I'm using the Kirby Starterkit for this tutorial and the comments section will be added for each individual project page.

1) Create the comment form snippet

<h3>Leave a comment</h3>
<form class="comment-form" action="<?= $page->url() ?>/#comment" method="post">
    <ul>
        <li>
            <label for="name">Name:</label>
            <input type="text" id="name" name="name" value="<?= isset($data['name']) ? $data['name'] : '' ?>">
        </li>
        <li>
            <label for="email">Email:</label>
            <input type="email" id="email" name="email" value="<?= isset($data['email']) ? $data['email'] : '' ?>">
        </li>
        <li>
            <label for="comment">Comment:</label>
            <textarea name="comment" id="comment" rows="10"><?= isset($data['comment']) ? $data['comment'] : '' ?></textarea>
        </li>
        <li class="honey">
            <label for="website">If you are a human, leave this field empty</label>
            <input type="website" name="website" id="website" placeholder="http://example.com" value="<?= isset($data['website']) ? $data['website'] : '' ?>"/>
        </li>
        <li>
            <input type="submit" id="submit-comment" name="submit-comment" value="Submit comment">
        </li>
    </ul>
</form>

The above snippet should go into site/snippets and you should save it as commentform.php. The fourth field serves as a basic spam bot catcher.

2) Include the snippet in project.php

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

Include this snippet just before the closing of the div that has the classes text and wrap (line 27).

3) Create a project controller

Inside of the controllers folder create a new file called project.php and drop the following code in it:

<?php
return function($site, $pages, $page) {

    $alert = null;

    if(r::is('post') && get('submit-comment')) {
        if(!empty(get('website'))) {
            go($page->url());
            exit;
        }

        $data = array(
            'name' => esc(get('name')),
            'email' => esc(get('email')),
            'comment' => esc(get('comment')),
            'page' => esc($page->uid()),
            'timestamp' => time()
        );

        $rules = array(
            'name' => array('required'),
            'comment' => array('required'),
            'email' => array('required', 'email'),
        );

        $messages = array(
            'name' => 'Please enter your name',
            'comment' => 'Please write your comment',
            'email' => 'Please enter a valid email address',
        );

        if($invalid = invalid($data, $rules, $messages)) {
            $alert = $invalid;
        } else {
            try {
                $newComment = $page->create('comments/'. date('Ymd', time()) . '-' . str::slug($data['name'] . '-' . time()) , 'comment', $data);

                $success = 'Your comment was successfully posted';
                $data = array();

            } catch(Exception $e) {
                echo 'Your comment failed: ' . $e->getMessage();
            }
        }
    }

    return compact('alert', 'data', 'success');
};

So what's going on here? First, I'm checking if the request is a POST request that's coming from the comment form. Following that I check whether or not the website field has been filled. In this example the website field is used to catch spam bots because it won't be visible for humans. If a spam bot fills in that field I make them reload the page.

If you plan on adding a comments section to a website with a lot of traffic I recommend adding more a sophisticated way to combat spam. Many bots are smart enough to avoid this kind of "honey trap".

Next I'm creating an array with all the data from the form, the page uid and a timestamp. The rules and messages arrays are for the invalid function which checks if all the required fields were filled. In the case of the email field I've also made it a requirement that the provided address is an actual email address.

If the data passes my check then I'll create a new folder inside of comments folder and pass a success message back to the template.

4) Hide the website/bot cather field

Add the following class to your css file to hide the spam bot catcher field:

.honey {
    display: none;
}

5) Create the comments folder

In this example I decided to make all the comments go into a single folder called comments in the root of the content folder. Another option would be to create a comments folder for each project individually and place the comments in there.

6) Connect the controller and comment form snippet

In step 2 I included the comment form snippet. Now that the controller is in place we need to make sure it's connected to our snippet. Make sure your snippet looks as follows:

<?php snippet('commentform', compact('data')); ?>

This ensures that data is flowing between the project page and the comment form.

If you now submit the form on one of project pages it will save your comment but neither a success message or your comment is visible anywhere yet. Let's add those next.

7) Add the success message

<?php if(isset($success)): ?>
<div class="success">
    <p><?php echo $success; ?></p>
</div>
<?php endif; ?>

I placed this just above the comment form snippet but if you wish to show it in a different spot you're free to place it anywhere on the project.php template.

8) Add the error messages

<?php if($alert): ?>
<div class="alert">
    <ul>
      <?php foreach($alert as $message): ?>
        <li><?php echo html($message) ?></li>
      <?php endforeach ?>
    </ul>
</div>
<?php endif; ?>

As with the success message I also placed the error messages above comment form snippet.

9) Make the comments visible

<div class="comments">
    <?php
    $comments = page('comments')->children()->visible()->filterBy('page', $page->uid())->sortBy('timestamp', 'desc');
    if($comments->count() > 0):
    ?>
    <ul>
        <?php foreach($comments as $comment): ?>
        <li>
            <div class="comment-meta">
                <span class="comment-author"><?php echo $comment->name() ?></span>
                <time><?php echo date('d.m.Y H:i', $comment->timestamp()->int()) ?></time>
            </div>
            <div class="comment-body">
                <?php echo $comment->comment()->kirbytext() ?>
            </div>
        </li>
        <?php endforeach; ?>
    </ul>
    <?php else: ?>
    <p>No comments yet.</p>
    <?php endif; ?>
</div>

Once again this code needs to go into the project.php template and I placed it above the comment form and its messages. What's happening here is that I'm looking into the comments folder and checking if I can find any comments where the page variable matches the uid of the current page.

And there it is, a functioning comment system for your Kirby site! Now go and spice it up with some css.

Optional extra moderation

If you're looking for a way to add some moderation functionality you could adjust the project controller in the following way:

try {
    $prevApproved = page('comments')->children()->visible()->filterBy('email', $data['email']);

    if($prevApproved->count() > 0) {
        $newComment = $page->create('comments/'. date('Ymd', time()) . '-' . str::slug($data['name'] . '-' . time()) , 'comment', $data);

        $success = 'Your comment was successfully posted';
    } else {
        $newComment = $page->create('comments/'. str::slug($data['name'] . '-' . time()) , 'comment', $data);

        $success = 'Your comment was successfully posted but it is waiting for approval.';
    }

    $data = array();

}

What I do here is that I check if I can find any existing visible (approved) comments by the same email address. If there are matches then I publish the comment immediately and otherwise I add the comment as an invisible comment which I can later decide to approve and publish (or not).