contenir / contenir-preview
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.
Requires
- php: ~8.3.0 || ~8.4.0 || ~8.5.0
- psr/clock: ^1.0
- psr/container: ^1.1 || ^2.0
- psr/simple-cache: ^1.0 || ^2.0 || ^3.0
Requires (Dev)
- contenir/contenir-qa-tools: 0.1.x-dev
- infection/infection: ^0.35.6
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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:
contenir/contenir-preview-laminas-mvccontenir/contenir-preview-mezzio(planned)
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