Edge-Side Includes
Craft Cloud has basic support for the ESI standard (opens new window), allowing you to include dynamic regions in an otherwise statically-cached page.
These substitutions are performed at the edge, meaning clients do not see additional HTTP round-trips for each fragment, and the final document is indistinguishable from one generated entirely by Craft.
ESI fragments are always cached independently (if eligible), and stitched together in our runtime.
#Templating
Use the helper function cloud.esi()
1.78.0+
2.22.0+ to include another template:
<header>
<h1>{{ siteName }}</h1>
{# ... #}
{{ cloud.esi('_account/nav.twig') }}
</header>
<nav aria-labelledby="account-navigation">
<h2 id="account-navigation">Account navigation</h2>
<ul>
{% if currentUser %}
<li>
{{ tag('a', {
text: 'Dashboard',
href: siteUrl('my-account/dashboard'),
}) }}
</li>
<li>
{{ tag('a', {
text: 'Log out',
href: logoutUrl,
}) }}
</li>
{% else %}
<li>
{{ tag('a', {
text: 'Log in',
href: loginUrl,
}) }}
</li>
{% endif %}
</ul>
</nav>
Cloud generates a signed URL, which means you can:
- Pass tamper-proof variables to the included template;
- Include private templates that are otherwise inaccessible via normal routing;
#Limitations
Edge-side includes are not a silver bullet for dynamic content.
#Performance
A response can never be sent faster than the slowest sub-resource request, so splitting a page into parts isn’t always the correct path.
Craft Cloud caches sub-resources independently, and can retrieve cached fragments efficiently. Any sub-resource that does make it to the Craft application, however, is handled as a separate invocation, and may result in longer service than rendering the entire page dynamically.
To illustrate, suppose you had a carousel of ten featured locations that each had a “daytime” and “nighttime” image attached to them, which you display based on the location’s current time. Putting an edge-side include inside that loop (individually wrapping each project thumbnail) might cause 10 sub-requests. Wrapping the entire carousel in an include would only trigger a single sub-request. While the service time for the whole carousel may be slightly longer than an individual slide, grouping the dynamic content means the total number of requests that reach the origin is kept to a minimum.
We discuss an alternative to edge-side includes in the dynamic content section of the static caching article.
We strongly discourage using edge-side includes inside one another.
If you have mixed static and dynamic content downstream of an include, render it all at once using regular Twig includes.
#Variables
Only scalar values can be passed to cloud.esi().
The extension recursively validates arguments to prevent issues when constructing a signed URL.
Because the parent and nested requests do not share memory, you must re-fetch any complex objects like elements:
<h1>{{ siteName }}</h1>
{{ cloud.esi('_partials/hero-carousel', {
sourceId: entry.id,
}) }}
{# Mark this dynamic include as cacheable for 10 minutes: #}
{% expires in 10 minutes %}
{% set sourceId = craft.app.request.getParam('sourceId') %}
{% set source = craft.entries()
.id(sourceId)
.one() %}
{# Get slides from the source and randomize their order: #}
{% set slides = source.heroSlides.all() | shuffle %}
{% for slide in slides %}
{# ... #}
{% endfor %}
Also unlike the built-in {% include %} tag and {{ include() }} function, the current Twig “context” cannot be passed to the included templates.
Features like the _globals collection do not share state.
#Side Effects
Templates that depend on (or cause) side-effects of the sequence in which they are rendered, like registering scripts and styles, modifying the _globals collection, or setting headers.
#Content Types
Only text/html and text/plain responses are parsed for edge-side include tags.
A sub-resource may be any compatible content type (i.e. not binary or streams), i.e. application/json.
#Development
Outside of cloud, the module silently falls back to synchronous rendering.
This behavior is not identical to include tag or function: the included template does not have access to the parent context, and only some variables can be passed.