How to filter + reorder + move pages between categories from the Panel in Kirby 5?

Hi everyone,

I’m building a portfolio site with Kirby 5 and I need to organize projects into categories (current, archive) and subcategories (dynamic, managed via a structure field in a Settings page).

My requirements are:

  • Projects filtered by category/subcategory in the Panel
  • Ability to reorder projects via drag and drop within each category
  • Draft projects visible in the Panel
  • Ability to move a project from one subcategory to another from the Panel

What I’ve tried:

  1. Flat structure + select fields + page.update:after hook to call changeParent() and physically move the page to the right folder. The hook fires correctly and logs the right values, but changeParent() silently does nothing. I’m using kirby()->impersonate(‘kirby’) before the call. This seems related to the known Kirby 5 bug #6869.

  2. Real folder hierarchy (projects/current/subcategory/project) with native Panel sections. Sorting and drafts work, but moving a project between subcategories requires manual filesystem operations — not acceptable for a non-technical client.

  3. Flat structure + query in site.yml using filterBy(). Filtering works, but sortable: true has no effect with custom queries, and drafts are not shown even with status: all.

Is there a recommended approach in Kirby 5 to achieve all four requirements together? Is changeParent() expected to work inside page.update:after in Kirby 5, or is there a workaround?

Thanks!

Why would moving a project require filesystem operations. The Panel offers a move option, so this should work out fine, provided the receiving parent page accepts the current page.

Can you please support with a hint on how to proceed?
because I wasn’t able to move the projects inside the folders through the Panel (the folder structure was unmodified).

This is the hook I was using inside config.php:
hooks’ => [
‘page.save:after’ => function (Kirby\Cms\Page $newPage, Kirby\Cms\Page $oldPage) {

  // Controlli di sicurezza
  if (!$newPage) return;
  if (!$newPage->template()) return;
  if ($newPage->intendedTemplate()->name() !== 'project') return;

  file_put_contents(kirby()->root('site') . '/debug.log', "hook eseguito\n", FILE_APPEND);
  file_put_contents(kirby()->root('site') . '/debug.log', "template: " . $newPage->intendedTemplate()->name() . "\n", FILE_APPEND);

  $category = $newPage->category()->value();
  $subcategory = $newPage->subcategory()->value();

  file_put_contents(kirby()->root('site') . '/debug.log', "category: $category\n", FILE_APPEND);
  file_put_contents(kirby()->root('site') . '/debug.log', "subcategory: $subcategory\n", FILE_APPEND);

  if ($category === 'current' && !empty($subcategory)) {
    $parent = page('projects/current/' . $subcategory);
  } elseif ($category === 'archive') {
    $parent = page('projects/archive');
  } else {
    return;
  }

  if ($parent && $newPage->parent()->id() !== $parent->id()) {
      file_put_contents(kirby()->root('site') . '/debug.log', "sposto in: " . $parent->id() . "\n", FILE_APPEND);
      kirby()->impersonate('kirby');
      $newPage->changeParent($parent);
      kirby()->impersonate(null);
      file_put_contents(kirby()->root('site') . '/debug.log', "spostato!\n", FILE_APPEND);
  } else {
      file_put_contents(kirby()->root('site') . '/debug.log', "parent già corretto o non trovato\n", FILE_APPEND);
      file_put_contents(kirby()->root('site') . '/debug.log', "parent id: " . ($parent ? $parent->id() : 'NULL') . "\n", FILE_APPEND);
      file_put_contents(kirby()->root('site') . '/debug.log', "current parent: " . $newPage->parent()->id() . "\n", FILE_APPEND);
  }
}
]

The log was showing the right values but nothing changed in the file structure.
What am I doing wrong?

A hook is not a good idea if you want to move pages around, as this always results in a Panel error that the page cannot be found.

The Panel has a Move Page functionality in the dropdown:

It is available if the permission is not denied and if the receiving page allows to have subpages with the same blueprint.