Virtual Pages

Render pages backed by external JSON or API data through the normal routing and template pipeline.

What a virtual page is

A Page backed by data that was never read from disk, for exposing an external API (events, products, any JSON) as a renderable page.

A virtual page is never injected globally. It exists only because a real page, with a real folder and index.json, routes to it explicitly, the same constraint that governs addons. PhlatPage stays a flat-file CMS first; API integration sits on top.

Creating one

App::page(string $uri, array $data) already supports this. A non-empty $data skips every disk read, and a view in $data still resolves the right view-bound Page subclass:

$app->page('events/summer-fest', $syntheticData);

The result is a normal Page, with the same class and template resolution as a real one.

In a router

A real page's router builds the data and returns the page. Handlers may return a different Page:

// site/views/events/router.php, for the real page at data/pages/events/
use Phlat\App;
use Phlat\Page;
use Phlat\Exceptions\NotFoundException;
use Phlat\Toolkit\Hash;

return function (App $app, Page $page): void {
    $page->router->get('{slug}', function ($slug) use ($app, $page) {
        $record = (new EventsSource())->find($slug);
        if (!$record) throw new NotFoundException();

        return $app->page("{$page->url()}/{$slug}", [
            'uuid' => Hash::sha256("events:{$slug}"),
            'view' => 'event',
            'data' => $record,
        ]);
    });
};

Use a conventional URI like "{$page->url()}/{$slug}" and url(), template resolution and everything else derived from the path just work.

Listing

For an index, build each page the same way and collect them in a PageCollection, which accepts appended instances:

$events = new PageCollection($app);
foreach ($source->all() as $slug => $record) {
    $events->append($app->page("{$page->url()}/{$slug}", [
        'uuid' => Hash::sha256("events:{$slug}"),
        'view' => 'event',
        'data' => $record,
    ]));
}

Deliberately left out

  • A provider interface. A data source is a plain class with whatever lookups suit it. Standardize once a second real source needs a shared shape.
  • Registry indexing. Virtual pages aren't in the registry, so find(), search and sitemaps don't see them. Add a source column and a boot-time load if that is ever needed.
  • A save() guard. Nothing stops save() on a virtual page from writing real files. Virtual pages only come out of GET handlers and never reach the admin edit flow, so add a guard only if that stops being true.