“The page slug is required”

:compass: Context

I’m building a directory using Kirby CMS in combination with Illuminate/Database (as ORM for a MySQL database).

I’m using Kirby ^4.0 and have integrated Eloquent via Composer.

:toolbox: Composer Dependencies

{
  "require": {
    "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
    "getkirby/cms": "^4.0",
    "getkirby/staticache": "^1.0",
    "arnoson/kirby-vite": "^5.3",
    "ext-zend-opcache": "*",
    "ext-gd": "*",
    "illuminate/database": "^12.19"
  }
}

:brick: Relevant Project Structure

  • site/models/Company.php: Model for DB interaction (Eloquent)
  • site/config/hooks.php: Used to sync Kirby content to database
  • site/blueprints/pages/company.yml: Company content type
  • site/config/database.php: DB configuration

:white_check_mark: Goal

Each time an Entry page is created, updated, or deleted in the Panel, the data should be synced to a MySQL table listings via Eloquent.

:warning: The Problem

When editing or updating or deleting an listing page in the Kirby Panel (triggering page.update:after), I get this error:

“The page slug is required”

Even though:

  • slug is in the database
  • slug is used in the model or $fillable
  • slug is used in the hook
  • the page has a valid title

:magnifying_glass_tilted_left: Hook Snippet – page.update:after

'page.update:after' => function (Kirby\Cms\Page $newPage, Kirby\Cms\Page $oldPage) {
        if ($newPage->intendedTemplate()->name() !== 'listing') {
            return;
        }
        Listing::query()->updateOrCreate(
            ['uuid' => $oldPage->uuid()->id()],
            [
                'name' => $newPage->title()->value(),
                'slug' => $newPage->slug(),
                'street' => $newPage->street()->value(),
                'house_number' => $newPage->house_number()->value(),
                'zipcode' => $newPage->zipcode()->value(),
                'city' => $newPage->city()->value(),
                'email' => $newPage->email()->value(),
                'phone' => $newPage->phone()->value(),
                'fax' => $newPage->fax()->value(),
                'website' => $newPage->website()->value(),
                'description' => $newPage->description()->value(),
                'tags' => explode(',', $newPage->tags()->value() ?? ''),
                'logo' => $newPage->logo()->toFile()?->url(),
                'status' => $newPage->status()
            ]
        );
    }

:receipt: Blueprint Snippet – listing.yml

title: Entry
icon: 📢

fields:
  street:
    width: 1/3
    label: Straße
    type: text
  house_number:
    width: 1/3
    label: Hausnummer
    type: text
  zipcode:
    width: 1/3
    label: PLZ
    type: text
  city:
    width: 1/3
    label: Ort
    type: text
  email:
    width: 1/3
    label: E-Mail
    type: email
  phone:
    width: 1/3
    label: Telefon
    type: tel
  fax:
    width: 1/3
    label: Fax
    type: tel
  website:
    width: 1/3
    label: Website
    type: url
  description:
    width: 1/3
    label: Beschreibung
    type: writer
  tags:
    width: 1/3
    label: Tags
    type: tags
  logo:
    width: 1/3
    label: Logo
    type: files
    layout: cards
    template: image

Databse Schema:

Capsule::schema()->create('listings', function ($table) {
    $table->id();
    $table->uuid('uuid')->unique();
    $table->string('name')->unique();
    $table->string('slug')->nullable();
    $table->string('street')->nullable();
    $table->string('house_number')->nullable();
    $table->string('zipcode')->nullable();
    $table->string('city')->nullable();
    $table->string('email')->nullable()->unique();
    $table->string('phone')->nullable();
    $table->string('fax')->nullable();
    $table->string('website')->nullable()->unique();
    $table->text('description')->nullable();
    $table->string('logo')->nullable();
    $table->json('tags')->nullable();
    $table->enum('status', ['draft', 'unlisted', 'listed']);
    $table->timestamps();
});

Model for Listing

use Illuminate\Database\Eloquent\Model;
use Kirby\Cms\Pages;

class Listing extends Model
{
    protected $table = 'listings';

    protected $fillable = [
        'uuid',
        'name',
        'slug',
        'street',
        'house_number',
        'zipcode',
        'city',
        'email',
        'phone',
        'fax',
        'website',
        'description',
        'logo',
        'status',
        'tags',
    ];

    protected $casts = [
        'tags' => 'array',
    ];

    public static function listed()
    {
        return Listing::all()->where('status', 'listed');
    }
}

class ListingPage extends Kirby\Cms\Page
{
    public function children(): Pages
    {
        if ($this->children instanceof Pages) {
            return $this->children;
        }

        return $this->children = Pages::factory((array)Listing::all(), $this);
    }
}

:white_check_mark: What works:

  • page.create:after: syncs to DB correctly including slug Column

:cross_mark: What fails:

Saving Updates or Delete Page:

“The page slug is required”

Even though I use or don’t use slug anywhere in my database or PHP logic.

:speech_balloon: Question

What am I missing?

  • Why is Kirby enforcing a required slug on update even though the page exists and has a valid title?
  • Could this be caused by title()->value() being temporarily empty?
  • Do I need to manually handle the slug in the update hook even if I don’t store it?

Any advice or ideas are greatly appreciated :folded_hands:

What does this updateOrCreate method do? What does it expect as params?

Listing::query()->updateOrCreate(
            ['uuid' => $oldPage->uuid()->id()],
            [                
                'name' => $newPage->title()->value(),
                'slug' => $newPage->slug(), // tried also 'page_slug' => $newPage->slug(),
                'street' => $newPage->street()->value(),
                'house_number' => $newPage->house_number()->value(),
                'zipcode' => $newPage->zipcode()->value(),
                'city' => $newPage->city()->value(),
                'email' => $newPage->email()->value(),
                'phone' => $newPage->phone()->value(),
                'fax' => $newPage->fax()->value(),
                'website' => $newPage->website()->value(),
                'description' => $newPage->description()->value(),
                'tags' => explode(',', $newPage->tags()->value() ?? ''),
                'logo' => $newPage->logo()->toFile()?->url(),
                'status' => $newPage->status()
            ]
        );

i have already tried to narrow down the error with a different column name, but the panel always gives me the same text. so i rule out the column name. even removing the hook did not bring any new insights.

What I don’t understand at all is why you have entry page and listing pages, if the purpose of the listings is only to sync local entry pages to the database. Something seems to be missing here for my understanding.Or are these supposed to be virtual pages?

Yes, they should be virtual pages. Creating the entries also works and everything is output in the frontend as planned. Only changing/deleting the entries/pages causes problems.

The database entries will later only be used for Typesense. I would like to be able to create and edit the entries via the panel. The project itself is only a frontend page with the search function using Typesense.

If you create an instance of a page, the slug is needed. I don’t know where you are passing this to the

Pages::factory((array)Listing::all(), $this);

as the code that defines Listing::all() is nowhere to be seen…

class ListingPage extends Kirby\Cms\Page
{
    public function children(): Pages
    {
        if ($this->children instanceof Pages) {
            return $this->children;
        }
        // return $this->children = Pages::factory((array)Listing::all(), $this); => ERROR
        return $this->children = Pages::factory(Listing::all()->toArray(), $this); => IT WORKS NOW
    }
}