Custom fields on a page type are worth nothing until something puts them on a page. Building the block that lists them meant handing every block view the page it is rendered on, which turned out to be a one-line change to the renderer and the most useful thing in the release.
A page type can declare a price, a card image and an excerpt. Then what? A client fills them in, and unless something on the site reads them back, they are three inputs writing to a column nobody queries.
That is the block half of v0.5.0.
Two things it needed
A query. Pages of one type, published only, in some order, optionally hand-picked.
A card. Markup that knows what the type called its fields, which the package cannot know, because the whole point of a page type is that the developer names its fields.
So the type names the partial:
public static function cardView(): string
{
return 'services.card';
}
The block finds the pages and includes that view per item. The package ships a fallback card of title and link, so a type that declares none renders something rather than nothing, because a block that renders blank reads as broken.
The card is the one file that knows the vocabulary:
<a href="{{ $page->url($locale) }}">
<h3>{{ $page->title }}</h3>
<p>{{ $page->data('excerpt', $locale) }}</p>
<p>From AED {{ number_format((float) $page->data('starting_price')) }}</p>
</a>
The missing variable
A block view received $attributes, $shared, $children, $id, $node, $locale and $editing. Not the page. For six releases nothing needed it, because a block rendered its own attributes and nothing else.
A listing needs it twice over. To exclude the current page from its own list, and to answer "the pages under this one" by slug path. And a pricing table needs it more than that: on a service page it reads that service's own tiers, so the block has no pricing fields at all.
public function render(array $tree, string $locale, bool $editing = false, ?Page $page = null): string
Optional parameter, two call sites updated, done in ten minutes. It is the change in this release I expect to matter most in a year, because it turns a block from a thing that renders its own settings into a thing that can render its context.
The demo that proves it
The example app has a pricing table block with no pricing fields:
$tiers = collect($page?->data('tiers') ?? [])->filter(...);
Edit a tier's price under Services in the panel and three places follow: the pricing table on the service's page, the "from" line on its card everywhere it is listed, and the options in the booking form below it. One field, one place to edit, three consumers. That is what the whole feature is for.
On a page with no tiers, the same block renders nothing at all rather than an empty table, so a client can drop it anywhere without breaking a layout.
Hand-picking, and the trap inside it
The first version listed everything published. Then came the obvious follow-up: choose which four services appear on the homepage, in a specific order.
A multi-select gives you the set and not the order. A repeater of selects gives you both, with drag handles, in about the same amount of code:
Repeater::make('pages')
->simple(Select::make('page')->options(...)->searchable())
->reorderable()
The options closure has to read the type chosen in the select above the repeater, from inside a repeater item, which is a relative path two levels up. Get that path wrong and nothing errors: the options resolve to nothing and the picker is silently empty.
So the fallback is not an empty list. With no type resolvable it offers every typed page with its type in the label, which is worse UX and never a dead end. Then a test locks the happy path by asserting the absence of that fallback label:
expect($html)->toContain('Web design')
->not->toContain('Web design (Service)');
A test that asserts what should not be on the page, because the failure mode is silence.
Published only, in the editor too
One rule worth stating: the listing shows published pages in the editor preview as well as on the live site. A preview that lists half-finished drafts is a preview of a site that does not exist, and the whole argument for this editor is that what you see is the page.
Where it stands
Shipped in v0.5.0. A Collection block that lists a type, everything published or hand-picked in a dragged order or the children of the current page, each rendered through the type's own card.
The renderer change is the part I would keep if I could only keep one line of this release.