Server-Side Includes
<bext-include> is bext's composition primitive. It sits at the intersection of caching, streaming, auth, experimentation, and realtime to assemble pages from independent fragments — all at the server level, before the response leaves the edge.
<bext-include src="/__bext/nav" site="docs"></bext-include>
The server replaces this element with the resolved HTML before sending the response. Every integration — caching, auth, A/B tests, live updates — is a declarative attribute on the element.
bext also supports the industry-standard SSI (<!--#include virtual="..." -->) and ESI (<esi:include>) syntaxes, so existing nginx and CDN configurations migrate to bext with zero code changes and get bext's full feature set automatically.
Three syntaxes, one system
All three syntaxes parse into the same directive and flow through the same resolver pipeline:
<!-- bext-native, richest feature set -->
<bext-include src="/__bext/nav" site="docs"></bext-include>
<!-- nginx / Apache SSI -->
<!--#include virtual="/__bext/nav" -->
<!-- W3C ESI (Varnish, Akamai, Fastly) -->
<esi:include src="/__bext/nav" />
Use <bext-include> for new sites. Use SSI when migrating from nginx. Use ESI when migrating from a CDN.
What you can do with includes
Each capability is a single attribute on <bext-include>. Mix and match.
Cache with TTL and stale-while-revalidate
<bext-include src="/__bext/nav"
ttl="5m" swr="1h" tags="chrome"
></bext-include>
The nav is rendered once, served from cache for 5 minutes, then served stale for up to an hour while revalidating in the background. Tagged chrome so you can purge all chrome fragments at once via the cache purge API. Backed by bext's tiered L0/L1/L2 cache.
Vary by dimension
<bext-include src="/partials/pricing-table"
vary="role,locale,geo"
ttl="10m"
></bext-include>
One element, separate cache entries per (role × locale × geo) combination. Admins in France see different prices than viewers in the US — and each variant is cached independently.
Stream lazy includes
<bext-include src="/partials/recommendations"
loading="lazy"
>Loading recommendations...</bext-include>
The page shell arrives immediately with a placeholder. The slow recommendations include resolves in the background and streams in as a later chunk. No blocking.
Server-side auth gating
<bext-include src="/partials/admin-toolbar"
auth="required" role="admin"
vary="role" ttl="30m"
>
<!-- fallback: hidden from non-admins entirely -->
</bext-include>
The admin toolbar is only rendered for authenticated admins. Non-admins see the fallback content (or nothing). No layout shift, no client-side flicker, no leaked markup.
A/B experiments
<bext-include src="/partials/hero"
experiment="hero-redesign"
></bext-include>
Assigns each user to a variant (weighted random, sticky cookie), rewrites src to the variant-specific source, and caches each arm independently.
Live-updating fragments
<bext-include src="/partials/order-status"
realtime="orders:123"
></bext-include>
SSR on first load; thereafter, a client-side SSE subscription re-fetches the fragment whenever the server publishes to the orders:123 channel. Dashboards and notification badges without a state management library.
File-based partials
<bext-include src="/_partials/footer.html"></bext-include>
<!-- SSI-compatible -->
<!--#include virtual="/_partials/footer.html" -->
Static HTML files from the site's _partials/ directory. Mtime-based cache invalidation means editing the file busts the cache automatically.
Timeout, fallback, circuit breaking
<bext-include src="/api/live-scores"
timeout="200ms"
fallback="/_partials/cached-scores.html"
onerror="continue"
></bext-include>
Give up after 200ms, try a fallback partial, and silently drop if everything fails. Plus a circuit breaker that skips calls to persistently failing sources.
Render components via V8
<bext-include src="./components/PricingTable.tsx"
render="v8" props='{"plan":"pro"}'
></bext-include>
Compile and SSR a React component in bext's V8 isolate pool — no Bun process needed. Optional hydrate attribute bridges to the island system for client-side interactivity.
Full attribute reference
<bext-include
src="/partials/example"
ttl="5m" swr="1h" tags="chrome,nav"
vary="role,locale,geo,device"
loading="eager"
auth="required" role="admin,editor"
experiment="layout-test"
realtime="channel-name"
realtime-mode="replace"
realtime-event="update"
timeout="200ms"
fallback="/cache/primary" fallback-2="/static/default.html"
onerror="continue"
render="v8" props='{"x":1}' hydrate
></bext-include>
| Attribute | Type | Default | Purpose |
|---|---|---|---|
src |
path / URL | required | What to include |
ttl |
duration | none | Cache lifetime (or adaptive) |
swr |
duration | none | Stale-while-revalidate window |
tags |
CSV | none | Cache tags for grouped purge |
vary |
CSV | none | Cache vary dimensions |
loading |
eager|lazy |
eager |
Inline vs streamed |
auth |
required|anonymous |
none | Authentication gate |
role |
CSV | none | Role-based gate |
experiment |
name | none | A/B experiment |
realtime |
channel | none | Realtime subscription |
realtime-mode |
replace|append|prepend |
replace |
Update behavior |
realtime-event |
event name | none | Filter subscription to event type |
timeout |
duration | from config | Resolution timeout |
fallback |
src | none | Fallback source on failure |
fallback-2 |
src | none | Second fallback |
onerror |
continue|abort |
abort |
Error strategy |
render |
v8|jsc|quickjs |
none | Render engine |
props |
JSON string | null |
Props for rendered component |
hydrate |
boolean | false | Wrap in island for hydration |
How the resolver chain works
Every include — from any syntax — flows through the same pipeline:
Parse directive
↓
Security (depth, count, allow/deny)
↓
Cache (hit = return stored HTML)
↓
Auth gate (reject before resolution cost)
↓
Experiment (rewrite src to variant)
↓
Plugin resolvers (custom, pattern-matched)
↓
Built-in resolvers (/__bext/* sources)
↓
File resolver (/_partials/*)
↓
V8 renderer (render="...")
↓
Fallback resolver (fallback, onerror, inline)
Each resolver in the chain either returns content (stop) or passes to the next. This composition is how a single <bext-include> tag can be cached, auth-gated, vary'd by role, and stream in lazily — just by combining attributes.
Performance
- No per-request allocation when the HTML has no include tags — early bail via .contains().
- Stampede guard prevents thundering herd on cache misses.
- Adaptive TTL keeps popular includes cached longer (hit-count-based).
- Per-chunk compression on streaming lazy includes.
- Mtime cache on file partials means unchanged files never re-read from disk.
- Circuit breaker skips resolution for persistently failing sources.
Where to go next
- Caching & TTL — per-include cache lifecycle
- Vary-aware caching — dimension-based cache keys
- Auth-gated includes — server-side authorization
- Streaming includes — progressive delivery
- Experiment variants — A/B testing
- Realtime includes — live fragment updates
- File partials — filesystem-based includes
- Timeout & Fallback — resilience
- Component rendering — V8-backed SSR
- SSI & ESI compatibility — nginx and CDN migration
- Configuration reference — bext.config.toml
- Plugin API — custom resolvers and hooks