Edge Side Includes for mixing cached and dynamic content.
Edge Side Includes (ESI) allows you to cache most of a page while keeping specific sections dynamic. This is perfect for pages that are mostly static but contain personalized fragments.
<esi:include src="/esi/user-greeting" />
This fetches /esi/user-greeting from your origin and inserts the response.
<esi:include src="/esi/user-greeting" onerror="continue" />
If the fragment fails to load, the page continues without it.
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<!-- Dynamic user greeting -->
<esi:include src="/esi/user-header" />
<!-- Cached main content -->
<main>
<h1>Welcome to our store</h1>
<p>This content is cached.</p>
</main>
<!-- Dynamic cart widget -->
<esi:include src="/esi/mini-cart" />
</body>
</html>
Your origin serves the fragments:
// /esi/user-header
Route::get('/esi/user-header', function() {
if (auth()->check()) {
return view('partials.user-greeting', ['user' => auth()->user()]);
}
return view('partials.guest-greeting');
});
// /esi/mini-cart
Route::get('/esi/mini-cart', function() {
return view('partials.mini-cart', ['cart' => Cart::get()]);
});
By default, fragments are NOT cached (fetched on every request). To cache fragments, send cache headers:
// Cache fragment for 5 minutes
return response()
->view('partials.user-greeting')
->header('Cache-Control', 'public, max-age=300');
The main page can be cached normally. ESI tags are processed during delivery.
Cache-Control: public, max-age=3600
Each fragment can have its own cache rules:
| Fragment | Recommended TTL |
|---|---|
| User greeting | No cache (dynamic) |
| Mini cart | No cache (dynamic) |
| Recently viewed | 5 minutes |
| Product recommendations | 15 minutes |
| Footer links | 1 hour |
Each ESI include adds latency. Minimize fragments per page.
Good: 1-3 fragments per page Avoid: 10+ fragments per page
NordicCDN fetches multiple fragments in parallel when possible:
<!-- These fetch in parallel -->
<esi:include src="/esi/header" />
<esi:include src="/esi/sidebar" />
<esi:include src="/esi/footer" />
Keep fragments small. Large fragments negate caching benefits.
ESI fragments receive the visitor's cookies:
// Fragment can access session
Route::get('/esi/cart-count', function() {
return response()->json([
'count' => session('cart_count', 0)
]);
});
For user-specific fragments:
Route::get('/esi/account-menu', function() {
if (!auth()->check()) {
return '<a href="/login">Sign In</a>';
}
return view('partials.account-menu');
})->middleware('web'); // Include session middleware
For WordPress with ESI:
// functions.php
function esi_user_header() {
if (is_user_logged_in()) {
echo '<esi:include src="/wp-json/mysite/v1/user-header" />';
} else {
echo '<a href="/wp-login.php">Log In</a>';
}
}
// REST endpoint
add_action('rest_api_init', function() {
register_rest_route('mysite/v1', '/user-header', [
'methods' => 'GET',
'callback' => function() {
$user = wp_get_current_user();
return 'Welcome, ' . esc_html($user->display_name);
},
'permission_callback' => '__return_true'
]);
});
If you see raw <esi:include> tags:
text/htmlESI adds complexity. Use JavaScript for simple updates (cart count, notifications).
Even personalized content often has cacheable patterns:
// Cache by user role, not by user
Route::get('/esi/admin-menu', function() {
$role = auth()->user()?->role ?? 'guest';
return response()
->view("partials.admin-menu.{$role}")
->header('Cache-Control', 'private, max-age=300');
});
Always use onerror="continue" for non-critical fragments:
<esi:include src="/esi/recommendations" onerror="continue" />
Track fragment response times separately from main pages.