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