Front-End User Input (Counter) without Reloading Page


I am stuck trying to create some kind of “Like-Button” – a button which any visitor can click on and which increases a counter, which is then saved to its page’s content, so the raised value is saved permanently until re-raised again. Unlike the familiar Like-button the one I am aiming for is not limited to “+1 / user” but can be raised repeatedly and endlessly.
Very important would be, that the value increase is displayed and saved immediately without page-reloading.

I looked at the following instructions, but could not figure out how to do it:

I am a beginner (especially when it comes to php functions and kirby controllers) so I would be really happy about any kind of lofi solution.


Without page reloading always mean a JavaScript solution.


  • user clicks button
  • your JS calls a route
  • in your route, you update the page ($page->update()) and return the new value
  • your script inserts the new value in DOM

Can get tricky if the user hits the button a thousand times very fast, I guess and many users do that at the same time.

Thank you for your quick response, I will try out your suggestion!

Thanks again, I’m super excited, it seems to be working now! :partying_face:

And this is what the relevant parts looks like so far:

Blueprint article.yml

        label: Likes
        type: number
        default: 0
        min: 0
        step: 1
        before: ♥

Snippet like-button.php

<div class="like-button" data-slug="<?= $data->slug() ?>">
    <span class="like-icon">&heartsuit;</span>
    <span class="like-value"><?= $data->likes()->html() ?></span>

config.php // updated with $page->increment()

'routes' => [
    'pattern' => 'articles/(:any)/like',
    'action'  => function ($slug) {
      if ($page = page('articles')->find($slug)) {
        $counter = $page->increment('likes',1)->content()->get('likes');
        $page->update([ 'likes' => $counter ]);
        return strval( $counter );


        var myVal = parseInt( $(this).find('.like-value').first().html() );
        var mySlug = $(this).data('slug');

        $(this).find('.like-value').first().load("/articles/" + mySlug + "/like", function(responseTxt, statusTxt, xhr){
            if(statusTxt == "success")
                console.log("External content loaded successfully!");
            if(statusTxt == "error")
                alert("Error: " + xhr.status + ": " + xhr.statusText);

If anyone sees any room for improvement I would be happy to here about it! :slight_smile:

1 Like

Let me introduce you to the magic increment() method

1 Like

Thank you for your suggestion! Apparently I still have to “get” the field value after incrementing it, otherwise the variable $counter seems to contain the page’s slug value.

$counter = $page->increment('likes',1)->content()->get('likes');

Could you try like that:

$counter = $page->increment('likes')->likes()->toInt();

One more addition:
It is necessary to use ->impersonate() so any visitor can trigger the ->increment() and ->update() from the front-end.

So the route in config.php looks like this now:

     'pattern' => 'articles/(:any)/like',
     'action'  => function ($slug) {
       if ( $page = page('articles')->find($slug) ) {

         if ( $page->likes()->exists() ) {

           $kirby = kirby();

           //$counter = $page->increment('likes',1)->content()->get('likes');
           $counter = $page->increment('likes')->likes()->toInt();
           $page->update([ 'likes' => $counter ]);
           return strval( $counter );

         } else {
           return 'Error';

1 Like

Yes, you need impersonate() to page update (increment method works with update too)
An alternative solution, you can give permission to specific or all roles in article.yml like that:

title: Article
    update: true
1 Like

That doesn’t help if you do stuff programmatically. You still need to authenticate.

1 Like

Guest is role that included *: true?

1 Like

What is the guest role? Not sure I get what you are asking?