Read-Only Repository This is a read-only subtree split from the main repository. Please submit issues and pull requests to toppynl/symfony-astro.
Extends Twig's {% include %} tag with prerender(false) and defer(true) modifiers for progressive rendering patterns. This package enables both client-side lazy loading via htmx and server-side streaming via deferred slots, allowing templates to render immediate skeleton placeholders while content loads asynchronously.
composer require toppy/twig-prerender- PHP 8.4 or higher
amphp/amp^3.0toppy/twig-streaming(required fordefer(true)server-side streaming)
{# Client-side lazy loading - renders skeleton, htmx fetches content #}
{% include 'components/reviews.html.twig' prerender(false) skeleton('skeletons/reviews.html.twig') %}
{# Server-side streaming - renders skeleton, streams content when ready #}
{% include 'components/stock.html.twig' defer(true) skeleton('skeletons/stock.html.twig') %}| Class | Purpose |
|---|---|
PrerenderExtension |
Twig extension providing modifier functions and runtime rendering methods |
IncludeTokenParser |
Parses extended {% include %} syntax with modifiers |
PrerenderIncludeNode |
Compiles prerender(false) to htmx placeholder output |
DeferIncludeNode |
Compiles defer(true) to streaming slot placeholder output |
ContextEncryptor |
Encrypts RequestContext for secure URL transport (AES-256-GCM) |
The IncludeTokenParser extends Twig's standard {% include %} tag to support additional modifiers:
| Modifier | Type | Description |
|---|---|---|
prerender(false) |
bool | Skip server-side rendering; client loads via htmx |
defer(true) |
bool | Render skeleton immediately; stream content via slots |
skeleton('path.twig') |
string | Template path for loading placeholder |
fallback('error.twig') |
string | Template or inline text for error state (defer only) |
id('custom-id') |
string | Custom DOM element ID (defer only) |
Standard Twig include arguments (with, only, ignore missing) remain fully supported:
{% include 'component.twig' defer(true) skeleton('loading.twig') with {id: item.id} only %}Use prerender(false) when you want the client to fetch content after initial page load. This outputs an htmx-powered placeholder that automatically fetches the rendered template via a fragment endpoint.
{% include 'components/reviews.html.twig'
prerender(false)
skeleton('skeletons/reviews.html.twig')
with {productId: product.id} %}Generated output:
<div hx-get="/_fragment/components/reviews.html.twig?ctx=<encrypted>"
hx-trigger="load"
hx-swap="outerHTML">
<!-- skeleton content rendered here -->
</div>The ctx parameter contains an encrypted RequestContext to preserve request state during the fragment fetch, preventing tampering via AES-256-GCM authenticated encryption.
When to use:
- Non-critical content that can load after initial paint
- Content requiring additional API calls that would block TTFB
- User-specific content on cached pages (ESI-like pattern)
- Components with slow data dependencies
Use defer(true) for server-side out-of-order streaming. The skeleton renders immediately in the response stream, then the actual content is pushed as a reconciliation fragment when ready.
{% include 'components/stock-status.html.twig'
defer(true)
skeleton('skeletons/stock.html.twig')
id('stock-widget') %}Generated output (initial flush):
<div id="stock-widget">
<!-- skeleton content -->
</div>Streamed later when data resolves:
<template id="tmpl_stock-widget">
<!-- actual content -->
</template>
<script id="script_stock-widget">
(function(){
var t=document.getElementById('tmpl_stock-widget'),
s=document.getElementById('stock-widget');
if(t&&s)s.replaceChildren(...t.content.cloneNode(true).childNodes);
t?.remove();
document.getElementById('script_stock-widget')?.remove();
})();
</script>When to use:
- Critical content that must be in the initial response (SEO)
- Components with async data that can resolve in parallel
- Sub-100ms TTFB optimization with FrankenPHP streaming
- Real-time data that benefits from immediate shell rendering
Provide a fallback template or inline text for error handling:
{# Template fallback #}
{% include 'components/stock.html.twig'
defer(true)
skeleton('skeletons/stock.html.twig')
fallback('errors/stock-unavailable.html.twig') %}
{# Inline fallback (detected by missing .twig extension) #}
{% include 'components/stock.html.twig'
defer(true)
skeleton('skeletons/stock.html.twig')
fallback('Unable to load stock information') %}By default, slot IDs are generated deterministically from the template path and request context. Use id() to specify a custom ID for JavaScript targeting:
{% include 'components/cart-count.html.twig'
defer(true)
skeleton('skeletons/cart.html.twig')
id('cart-badge') %}The skeleton resolution follows a fallback chain:
- Explicit skeleton parameter:
skeleton('skeletons/custom.html.twig') - Convention-based:
template.skeleton.html.twigalongsidetemplate.html.twig - Default fallback:
skeletons/default.html.twig - Hardcoded fallback:
<div class="skeleton"></div>
Skeleton template example:
{# skeletons/product-card.html.twig #}
<div class="product-card skeleton">
<div class="skeleton-image"></div>
<div class="skeleton-text skeleton-title"></div>
<div class="skeleton-text skeleton-price"></div>
</div>Convention-based skeleton:
templates/
components/
product-card.html.twig # Main component
product-card.skeleton.html.twig # Auto-discovered skeleton
prerender(false) and defer(true) cannot be used together as they represent different loading strategies:
{# ERROR: SyntaxError thrown #}
{% include 'component.twig' defer(true) prerender(false) %}For prerender(false), ensure htmx is loaded in your base template:
<script src="https://unpkg.com/htmx.org@2.0.0"></script>The fragment endpoint (/_fragment/...) must be implemented to:
- Decrypt the
ctxparameter usingContextEncryptor - Restore the
RequestContext - Render and return the template fragment
The defer(true) modifier requires the twig-streaming package for slot management:
composer require toppy/twig-streamingWithout it, using defer(true) throws a RuntimeException with installation instructions.
Register the extension with required dependencies:
use Toppy\TwigPrerender\PrerenderExtension;
use Toppy\TwigPrerender\Service\ContextEncryptor;
use Toppy\TwigStreaming\Slot\SlotRegistry;
use Toppy\TwigStreaming\Slot\SlotRenderer;
$encryptor = new ContextEncryptor($secretKey); // 32-byte key for AES-256
$contextResolver = /* your ContextResolverInterface implementation */;
$slotRegistry = new SlotRegistry();
$slotRenderer = new SlotRenderer();
$twig->addExtension(new PrerenderExtension(
$encryptor,
$contextResolver,
$slotRegistry, // null if not using defer(true)
$slotRenderer, // null if not using defer(true)
));When using with toppy/symfony-async-twig-bundle, the extension is auto-configured via dependency injection. See the bundle documentation for configuration options.
Run the test suite:
cd src/Toppy/Component/TwigPrerender
./vendor/bin/phpunitOr from the monorepo root:
make demo-shell
cd /app/src/Toppy/Component/TwigPrerender && ./vendor/bin/phpunitProprietary - see LICENSE file for details.