v0.40.0

Latest release

3 November 2026

Sourced pages replace virtual pages and Page::sync(), and a page stores only the fields its view declares.

Added

  • PageSource: a page's view-bound class implements record($name) to build its children from an API, a database or any other source. child(), App::resolve(), relation and pages fields and the admin find a child through it. See Sourced pages.
  • Page::isSourced() says whether a page's parent is a PageSource. A sourced page is never in the registry.
  • The admin opens and saves a sourced page that has no folder yet. The first save makes its folder, and drafts start from then on.
  • Record::base() is the one layer read under a record's _data.json: an addon mount root's shipped data, or a sourced page's record.
  • The API reference documents interfaces.

Changed

  • A page stores only what its view owns. save() and autosave() throw a ValidationException for a key that is neither a field the view declares nor a structural key. A save that sets view is checked against that view.
  • A draft holds the view's fields, and the admin preview reads it over the saved content rather than in its place.
  • save() on a record that doesn't exist throws NotFoundException.
  • The second argument of App::page($uri, $index) is the page's _index.json, for a caller that already holds it. It no longer builds a page from content.

Removed

  • Page::sync() and _source.json. Write pages in bulk with new() and save() inside $app->history()->batch(). A _source.json is no longer read, so fold it into its _data.json before upgrading.
  • Page::isVirtual(), Page::virtual(), and the "virtual" and "children": {"source": true} view keys. A source implements PageSource.
  • Pages built from content with $app->page($uri, $data).