Search by

contenir / contenir-preview

peptolab

The Contenir preview protocol: signed preview tokens from the CMS, their verification on a site, and the request-scoped context a site renders a previewed record from.

Package info

github.com/contenir/contenir-preview

pkg:composer/contenir/contenir-preview

Statistics

Installs: 1 092

Dependents: 3

Suggesters: 1

Stars: 0

Open Issues: 0

0.1.x-dev 2026-10-18 01:29 UTC

This package is auto-updated.

Last update: 2026-10-18 02:17:45 UTC


README

The Contenir preview protocol: the CMS signs a preview token carrying a record and its current values, saved or not, and posts it into a frame on the site; the site verifies the token and renders the page that shows the record with those values in place of the stored ones.

This package is framework-free and holds the parts both sides share. The renderer for each site framework lives in its own package:

The protocol itself is in docs/protocol.md.

Shared configuration

Both sides build the same PreviewConfig: who issues tokens, for which site (its base URL), and the secrets that sign them by key id (at least 32 bytes each).

use Contenir\Preview\PreviewConfig;

$config = new PreviewConfig(
    issuer:   'https://admin.example.com',
    audience: 'https://www.example.com',
    secrets:  ['2026-10' => $secret],
    signWith: '2026-10',
);

On the CMS: signing

use Contenir\Preview\PreviewRecord;
use Contenir\Preview\Token\TokenSigner;

$token = (new TokenSigner($config, $clock))->sign(
    new PreviewRecord('site.page.home', ['resource_id' => '101'], $formValues),
    (string) $userId,
);

Post the token as the contenir_preview field into a frame aimed at $config->endpoint(): the site's base URL (the audience) with the protocol's fixed path, /api/resource/preview. Sign a new token each time the preview refreshes: each is accepted once and lives five minutes.

On a site: verifying and rendering

A framework adapter does this for you. Underneath, it is:

use Contenir\Preview\Clock\SystemClock;
use Contenir\Preview\PreviewContext;
use Contenir\Preview\PreviewEntry;
use Contenir\Preview\Replay\FileReplayGuard;
use Contenir\Preview\Resolver\PathResolver;
use Contenir\Preview\Resolver\ResolverRegistry;
use Contenir\Preview\Resolver\RouteResolver;
use Contenir\Preview\Token\TokenVerifier;

$clock   = new SystemClock();
$context = new PreviewContext();
$entry   = new PreviewEntry(
    new TokenVerifier($config, $clock, new FileReplayGuard(sys_get_temp_dir(), $clock)),
    new ResolverRegistry([
        'site.page.home'        => new PathResolver('/{slug}'),
        'library.news.articles' => new RouteResolver('news/article', ['slug' => '{slug}']),
    ]),
    $context,
);

$target = $entry->open($method, $postedToken); // throws InvalidPreview with its HTTP status
// render $target as the site normally would: a path, a named route, or a handler it dispatches

A resolver can also answer with PreviewTarget::handler($controllerOrMiddleware, $params), which the adapter dispatches without routing: the way to preview drafts, records not saved yet, and sites whose routes are built from published records (contenir-workflow ships such a resolver).

Accepted tokens are remembered so each is used once: FileReplayGuard for one server (any existing directory; it touches only its own files), CacheReplayGuard over a shared PSR-16 cache for several. InMemoryReplayGuard lasts one process and suits only long-running servers and tests.

Wherever the site loads a previewable section's records, it lets the context lay the preview's values over them:

$row = $context->overlay('site.page.home', ['resource_id' => $row['resource_id']], $row);

and, for a record not saved yet, $context->unsaved('site.page.home') gives its values.

Every preview answer carries PreviewResponse::headers() (no caching or indexing, framed only by the CMS), and a refusal answers with PreviewResponse::refusal() and the refusal's status.

Container services

ConfigProvider registers the services a renderer needs in any PSR-11 container, built from the application's contenir_preview configuration (Container\PreviewOptions documents its shape): the options, the clock, the replay guard (a PSR-16 cache when one is named, otherwise files), the resolver registry, the token verifier, the entry, the response and the preview context. Each section's resolver is a path pattern (/{slug}), a named route with its parameters, or the name of a PreviewResolverInterface service. Framework adapters merge getDependencies() into their own configuration and add what their framework needs.

PreviewMarker::mark() places the visible "Preview" badge in a page's HTML.

Quality

composer check          # Mago format, lint and analyze, then the tests
composer mutation-test  # Infection