Kirby3 blueprint or hook to show info of which user created / modified a page

Ohh ok, I think this was silently erring - trying the hidden field now like you posted above.

the same approach will also work for a hidden field as date storage:

                type: hidden
                default: "{{ date('Y-m-d h:i')}}"

But note that you still need the hooks for the modification fields.

Edit: Turns out this doesn’t work after all…

1 Like

Cool, thanks for the heads up!

Sorry for all the replies, haha, slowing down now, I’ll focus on just the creation stuff, no hooks.

Here’s what I have:

  type: hidden
  default: "{{ date('D. M j, Y — g:ia')}}"
  type: info
  label: false
  text: Page created by {{ page.createdBy.toUser.username }} on {{ page.createdOn.time.toDate('D. M j, Y — g:ia') }}

The username is pulling in correctly, however, the date is not (it’s empty). I tried just {{ page.createdOn }} but that was also empty / broken.

Because this code is wrong: page.createdOn.time.toDate('D. M j, Y — g:ia')

Should be page.createdOn.toDate('D. M j, Y — g:ia'), without the time bit in between.

And keep in mind that default values only work at page creation, not if the page was already created and you add the fields afterwards.

Thanks! I tried this and it still came up empty / broken… :thinking:

And yes, I deleted the page, refreshed from the main panel and went back in creating a brand new page…

Ok, looks like I was mistaken regarding the hidden field as date.


  • Use a standard disabled date field and hide via custom stylesheet
  • Use a page model to store the date
  • Use a hook to store the date

Edit: I wouldn’t store this format: date('D. M j, Y — g:ia') but keep it basic or only store a timestamp.

1 Like

Page model example:


class NotePage extends Page
    public static function create(array $props): Page
        $props['content']['createdon'] = date('Y-m-d h:i');

        return parent::create($props);

NotePage is the model for a page with the note blueprint, so you would have to change this.

Using a model is useful if you only have one page type that needs this method. If you want to apply this to multiple page types, you either need as many models (or a based model that you extend) or a hook is the better option.

1 Like

There is something strange with your setup, @something-strange. :wink:

The timezone which is shown from your format (here UTC) is the default timezone, when the date.timezone value in your php.ini is NOT set. Please double check this. Note, that you might have two configuration files named php.ini, one for PHP which will be invoked by your webserver and one for PHP which will be invoked on the command line. Take care to define the correct setting in both files.

This is also independent from executing this on your localhost (either via CLI or Webserver) or on a remote server, however the timezone setting could be different in both installations.

The PHP time() function - which is used by my plugin - will ALWAYS return the epoch as an offset from Jan 1st, 1970 0:00 GMT (yes, but we can consider this as equivalent to UTC here), regardless of the timezone setting in php.ini. Thus you will have an universally valid timestamp, which then will be rendered for the timezone which is set for PHP.

All of this however will always be server side, independently from where a client will look onto it. In order to show the same date/time converted to the timezone of the clients, you will need to do some client-side conversions via JavaScript or any other client-side scripting in your website. But for this it makes sense to have a universal time, or lets say “UTC”(!), on the server side as a source, which will lead us back to the epoch, this universally valid timestamp which is written by the time() function.

To illustrate this further run this script (in comments the output on my machine):


function showTime() {
  $myTZ = date_default_timezone_get();
  echo "Currently active timezone: $myTZ\n";
  $Now = time();
  echo "Now it is $Now seconds from epoch, which is ",date('D, j M Y H:i:s T',$Now)," for humans.\n";

$TZ = ini_get('date.timezone');
echo "Default timezone (from ini): $TZ\n";
/* Default timezone (from ini): Europe/Berlin */

/* Currently active timezone: Europe/Berlin
Now it is 1595665226 seconds from epoch, which is Sat, 25 Jul 2020 10:20:26 CEST for humans. */


/* Currently active timezone: UTC
Now it is 1595665226 seconds from epoch, which is Sat, 25 Jul 2020 08:20:26 UTC for humans. */


/* Currently active timezone: America/Denver
Now it is 1595665226 seconds from epoch, which is Sat, 25 Jul 2020 02:20:26 MDT for humans. */
1 Like

Hey @texnixe, @pixelijn and @Adspectus - thank you all for these examples and detailed notes!

I’m going to play around with a bit of all of these, didn’t realize there were so many options, haha. So, I’m not sure which comment to mark ‘Solved’ but I think it’s safe to say by now this overall topic is solved. :sweat_smile:

@Adspectus ‘something strange’ with my setup - haha I see what you did there… :ghost: I think this will resolve on my server, I have some weird stuff running locally with Kirby / Nuxt php servers and stuff (non-MAMP). Keep you posted on this!

I’ll share my code snippet(s) this afternoon once I decide which options to go with.

Thanks again!!

Hey all,

Here’s a 99% complete solution I came up with, consists of 3 parts!

(1) Plugin - based off of @Adspectus’s very nice Date Extended Plugin (I modified plugin name / simplified it a bit for my own understanding of how plugins work - as it’s my first time with all of this).



Kirby::plugin('ssk3-page-log/page-info', [
  'fieldMethods' => [
    'time2date' => function ($field) {
      $format = 'D. M j, Y — g:ia';
      return date($format, $field->value());
  'hooks' => [
    'page.create:after' => function ($page) {
        'createdBy' => kirby()->user(),
        'dateCreatedTime' => time()
    'page.update:after' => function ($newPage, $oldPage) {
        'updatedText' => 'Page was updated by',
        'updatedUser' => kirby()->user(),
        'dateModifiedTime' => time()
    'page.changeTitle:after' => function ($newPage, $oldPage) {
        'updatedText' => 'Page title was changed by',
        'updatedUser' => kirby()->user(),
        'dateModifiedTime' => time()

(2) Blueprint - here are the fields I set up - using hooks / no empty or hidden fields :sunglasses:


  type: fields
      type: headline
      label: Page Info
      numbered: false
      type: info
      label: false
      text: "{{ page.createdBy.toUser.avatar }} Created by {{ page.createdBy.toUser.username }} on {{ page.dateCreatedTime.time2date }}"
      type: info
      label: false
      text: "{{ page.updatedUser.toUser.avatar }} {{ page.updatedText }} {{ page.updatedUser.toUser.username }} on {{ page.dateModifiedTime.time2date }}"

(3) CSS - here’s some custom css, entirely optional, shows avatar next to info text:


.k-section-name-pageInfoColumn .k-box[data-theme=info] {
  border: 0;
  padding: 0;
  border: none;
  background: none;
  font-style: italic;
  opacity: 1;

.k-section-name-pageInfoColumn .k-headline:not(.k-headline-field) {
  display: none;

.k-section-name-pageInfoColumn .k-box[data-theme=info] .k-text {
  width: 100%;
  line-height: 1.25em;
  flex-wrap: nowrap;
  align-items: center;
  display: flex;

.k-section-name-pageInfoColumn .k-box[data-theme=info] .k-text img {
  width: 2em;
  height: 2em;
  min-width: 2em;
  margin-right: 10px;
  background-color: #000;
  border-radius: 50%;
  overflow: hidden;
  display: block;


  • Now for my final question (I hope haha)… Page created and page updated updates the user name, but it isn’t updating the {{ page.updatedText }} in the blueprint for the page.changeTitle:after hook. So, after a user changes the title (and after a page refresh) the value just says ‘Page was updated by…’ which is the page.update:after hook. It’s like page update stomps out / overrides the page changeTitle hook… yet the user and time update perfectly on both hooks.

Any thoughts?

As always, thank you all for your time and efforts!!

That is not really surprising, since your changeTtitle hook updates the page and and the update action triggers the update hook again. Better use save instead of update in the page.changeTitle hook.

On a side note it would nevertheless make sense to define those fields in the blueprint.

1 Like

Sweet! Thanks, using save did the trick! Applied this to page.changeSlug:after and page.changeStatus:after as well, so now the {{ page.updatedText }} updates accordingly.

I was only imaging showing one row being the user that created the page and then one row of the latest update / change by whichever user, what did you mean by:

Just to be on the safe side, in case non-defined fields get deleted in the future (there are discussions about this), or that you have a way to clean up unused fields yourself.

Ah very cool, thanks for sharing!

@Adspectus - I did a push to a server and confirmed my php.ini is working correctly with date.timezone = "America/Denver" It was just a localhost issue, but your example is still very helpful!

Again, not sure which comment(s) to mark ‘Solved’ haha, but I think this thread is solved in 9 different ways if you want to mark this done @texnixe :sweat_smile:

Very simple solution here if somebody is interested: Simple changelog hook for Kirby