SSI & ESI compatibility
<bext-include> is bext-native, but existing sites and CDN configs use two other industry-standard syntaxes: SSI (nginx and Apache) and ESI (Varnish, Akamai, Fastly). bext supports both natively. All three syntaxes parse into the same directive and flow through the same resolver chain — which means existing SSI/ESI tags automatically gain bext's caching, auth gating, experiments, and streaming.
<!-- nginx/Apache SSI -->
<!--#include virtual="/partials/nav" -->
<!-- W3C ESI -->
<esi:include src="/partials/nav" />
<!-- bext-native -->
<bext-include src="/__bext/nav" site="docs"></bext-include>
All three produce the same resolved HTML.
SSI — nginx & Apache Server Side Includes
bext supports the most commonly used SSI directive: #include.
<!--#include virtual="..." -->
Server-relative path. Resolves to {site_root}/path.
<body>
<!--#include virtual="/partials/header.html" -->
<main>...</main>
<!--#include virtual="/partials/footer.html" -->
</body>
<!--#include file="..." -->
Document-relative path. Resolves relative to the file being served.
<!-- served from /blog/post.html -->
<!-- this resolves to /blog/_includes/sidebar.html -->
<!--#include file="_includes/sidebar.html" -->
<!--#include virtual="/path" stub="errname" -->
With an error stub:
<!--#include virtual="/optional/widget.html" stub="widget_error" -->
When the include fails, the stub name identifies the error block to emit. bext passes the stub value to the resolver as an attribute.
SSI directives bext does NOT support
- <!--#echo var="NAME" --> — environment variable interpolation.
- <!--#if expr="..." --> ... <!--#endif --> — conditional blocks.
- <!--#set var="NAME" value="VALUE" --> — variable assignment.
- <!--#exec cmd="..." --> — shell command execution (security reasons).
- <!--#config --> — SSI configuration directives.
These are rare in practice and mostly for server metadata. If you need them, use <bext-include> or a V8-rendered component.
ESI — Edge Side Includes
bext supports the most commonly used ESI directives.
<esi:include src="..." />
Basic include:
<esi:include src="/partials/nav" />
<esi:include src="/partials/nav"></esi:include> <!-- both forms OK -->
<esi:include src="..." alt="..." />
Fallback source:
<esi:include src="/api/live" alt="/api/cached" />
If /api/live fails, bext tries /api/cached. If both fail, follow the onerror strategy.
<esi:include src="..." onerror="continue" />
Silent-drop on failure:
<esi:include src="/api/optional" onerror="continue" />
If all resolution paths fail, the tag is replaced with an empty string (no leftover <esi:include> markup).
<esi:remove>...</esi:remove>
Block of content that's stripped when ESI is active. Use this for fallback markup that shows when ESI isn't processing (e.g., the static file served directly by origin without a CDN).
<!-- bext sees ESI; strips this block entirely -->
<esi:remove>
<p>This is what you'd see without ESI processing.</p>
</esi:remove>
<esi:include src="/partials/live-content" />
<!--esi ... -->
Content that's ONLY visible when ESI is processing. The wrapper comment is stripped, revealing the inner content:
<!-- Without ESI: browser sees the comment (ignored) -->
<!-- With ESI: wrapper stripped, content revealed -->
<!--esi <p>This shows only when ESI is processed.</p> -->
The pattern: use <esi:remove> for "fallback when ESI is NOT processing" and <!--esi --> for "content when ESI IS processing." They're complementary — together they let you write a single HTML document that works both with and without ESI.
ESI directives bext does NOT support
- <esi:try> / <esi:attempt> / <esi:except> — complex error handling.
- <esi:choose> / <esi:when> / <esi:otherwise> — conditionals.
- <esi:vars> — variable interpolation.
- <esi:inline> — prefetching.
Most of these have direct analogs in bext attributes (fallback, auth, experiment, etc.). If you need them during migration, convert to <bext-include>.
All three together
A single HTML document can mix all three syntaxes:
<!DOCTYPE html>
<html>
<body>
<!-- bext-native: full feature set -->
<bext-include src="/__bext/nav" site="docs" ttl="5m"></bext-include>
<main>
<h1>Welcome</h1>
<!-- Legacy SSI: kept from the nginx era -->
<!--#include virtual="/_partials/promo-banner.html" -->
<!-- ESI from the CDN era: works with or without ESI-aware cache -->
<esi:include src="/_partials/live-ticker" alt="/_partials/static-ticker" />
<esi:remove>
<p>Ticker unavailable (legacy fallback).</p>
</esi:remove>
</main>
<!--#include virtual="/_partials/footer.html" -->
</body>
</html>
All three resolve through the same pipeline. Apply caching, auth, or experiments via config rules — which target the resolved src, not the syntax:
[[include.rules]]
# Applies to ALL THREE above — bext-include, SSI, and ESI
pattern = "/_partials/*"
ttl = "1h"
tags = ["chrome"]
[[include.rules]]
pattern = "/api/live*"
ttl = "10s"
timeout = "200ms"
fallback = "/_partials/fallback.html"
Migration path
From nginx with SSI
bext auto-detects SSI. Existing <!--#include --> tags work unchanged.
# existing nginx config
server {
root /var/www/site;
location / {
ssi on;
try_files $uri $uri/index.html;
}
}
With bext:
# bext.config.toml — SSI enabled by default
[[apps]]
hostname = "example.com"
root = "./site"
No changes to your HTML. You immediately gain:
- Caching (add [[include.rules]] with ttl).
- Mtime-based invalidation (works automatically).
- Timeout + fallback chain (add to rules).
- Tag-based purge (add tags to rules).
From a CDN with ESI
Varnish / Akamai / Fastly ESI configurations port directly:
# Varnish VCL
sub vcl_backend_response {
set beresp.do_esi = true;
}
With bext:
# bext.config.toml — ESI always on
No VCL, no CDN-specific syntax. bext processes ESI at the origin. You also gain features CDN ESI doesn't have: vary-aware caching, streaming, realtime updates, auth gating.
Gradual enhancement
You don't have to rewrite anything. Existing SSI/ESI keeps working. Use config rules to add bext features incrementally:
# Week 1: add caching to the hottest partials
[[include.rules]]
pattern = "/_partials/nav*"
ttl = "10m"
tags = ["chrome"]
# Week 2: add timeout+fallback to API includes
[[include.rules]]
pattern = "/api/*"
timeout = "200ms"
fallback = "/_partials/api-fallback.html"
# Week 3: add vary for personalized fragments
[[include.rules]]
pattern = "/partials/personalized*"
vary = ["role", "locale"]
ttl = "5m"
No HTML changes. Each rule takes effect on the next deploy.
Feature parity table
| Feature | <bext-include> |
SSI | ESI |
|---|---|---|---|
| Basic include | ✅ src= |
✅ virtual=, file= |
✅ src= |
| Fallback source | ✅ fallback= |
❌ (via config) | ✅ alt= |
| Silent on error | ✅ onerror= |
❌ (via config) | ✅ onerror= |
| TTL / cache | ✅ ttl= |
❌ (via config) | ❌ (via config) |
| Vary | ✅ vary= |
❌ (via config) | ❌ (via config) |
| Auth gate | ✅ auth= |
❌ (via config) | ❌ (via config) |
| Experiment | ✅ experiment= |
❌ (via config) | ❌ (via config) |
| Realtime | ✅ realtime= |
❌ | ❌ |
| Streaming | ✅ loading= |
❌ | ❌ |
| Remove block | ❌ | ❌ | ✅ <esi:remove> |
| ESI-only reveal | ❌ | ❌ | ✅ <!--esi --> |
For SSI and ESI, features not available as attributes work via config rules matching the src path. This means you can achieve full bext functionality with SSI/ESI syntax — just declare the behavior in bext.config.toml instead of on the tag.
See also
- Overview — full include system
- Migration from nginx — complete migration guide
- Configuration — per-source rules for SSI/ESI features