Guidance for AI agents (and humans) working inside the bagisto/bagisto-varnish package. The README is the user-facing install and configuration guide; this file is how the package is wired into Bagisto and what to watch for when changing it.
php artisan boost:add-skill bagisto/bagisto-varnish
Save this content to: AGENTS.md
# AGENTS.md — Bagisto Varnish
Guidance for AI agents (and humans) working inside the `bagisto/bagisto-varnish` package.
The [README](./README.md) is the user-facing install and configuration guide; this file is
how the package is wired into Bagisto and what to watch for when changing it.
## Overview
The package puts Varnish in front of Bagisto: storefront responses are tagged so Varnish can
cache and ban them, and the parts of the page that differ per customer are fetched separately
so a cached page stays correct for everyone.
- **Namespace:** `Webkul\Varnish` → `src/`
- **Branch:** a single `master`, tracking the newest Bagisto line — currently **2.5** (Laravel 13,
PHP 8.4). There is no long-lived 2.4 branch; that line is held by the **`v2.1.0`** tag, so a 2.4
fix means branching from the tag. The README carries the
[version matrix](./README.md#version-compatibility) — update it there, not here, whenever a
release targets a new Bagisto line.
- **Registration:** `VarnishServiceProvider` is added to the application's
`bootstrap/providers.php` **last**. Composer auto-discovery is deliberately not declared.
## Why the provider must be registered last
`boot()` calls `$router->aliasMiddleware('cache.response', VarnishCacheMiddleware::class)` —
`Http\Middleware\VarnishCache`, imported under that alias — replacing the alias **Bagisto's own
`ShopServiceProvider` already registers** for `Webkul\Shop\Http\Middleware\CacheResponse`. Core's
storefront routes carry that alias, so this swap is what redirects them from Bagisto's built-in
page cache to Varnish tagging.
It is a deliberate takeover, not an accident, and it is decided by boot order: registered before
the Shop package, Shop overwrites it again and nothing is tagged. Verify it after any change:
```bash
php artisan tinker --execute='echo app("router")->getMiddleware()["cache.response"];'
# expect Webkul\Varnish\Http\Middleware\VarnishCache
```
## The ESI holes — the heart of the package
A cached page cannot carry customer-specific markup, so the package cuts those blocks out and
fetches them per request. Every fragment is served by `EsiController::loadView()` at
`/esi?tag=<tag>` (route `varnish.esi.load`), and `src/Config/varnish.php` maps each tag to a view
under `esi.views`. There are **two** mechanisms, and which one a hole uses is not a free choice:
**1. `<x-varnish::dynamic-view view="…">` — for the four dropdowns.**
`src/Resources/views/shop/components/dynamic-view/index.blade.php` registers a Vue component that
`fetch`es the fragment and injects it with `v-html`. Used in the two overridden headers for
`customer-desktop-dropdown`, `customer-mobile-md-dropdown`, `customer-mobile-sm-dropdown` and
`customer-account-profile-drawer`. The HTML comment above each hole states the reason:
**Varnish's own ESI cannot resolve a Blade component**, so the package does the fetch itself.
Two consequences:
- **It fetches on the first `mousemove` or `touchstart`, not on mount.** Until the visitor moves
the pointer the dropdown is genuinely empty. A `curl` of the page, and a Playwright test that
clicks without moving first, both see nothing — that is the design, not a bug.
- **`v-html` content is not compiled by Vue.** A fragment must be plain HTML and Blade; a Vue
component or directive inside one silently does nothing. The current fragments observe this.
**2. A real `<esi:include>` — for `customer-status`.**
`src/Resources/views/shop/view-render-events/customer-status.blade.php` is a hidden
`<span id="isCustomerFlag">` holding a genuine `<esi:include src="/esi?tag=customer-status" />`,
which Varnish resolves because it is plain markup. The fragment is one line —
`{{ auth()->guard('customer')->check() }}` — so the span reads `1` or empty.
It reaches the page through `Event::listen('bagisto.shop.layout.body.before', …)` →
`$viewRenderEventManager->addTemplate(...)`, so **no core view is edited to place it**. This is
what the third override depends on: `components/products/card.blade.php` differs from core in one
line, reading `document.getElementById('isCustomerFlag')` instead of calling
`auth()->guard('customer')->check()` while the page is being cached. Remove the listener and every
product card decides wishlist state from whoever warmed the cache.
**The three overridden views** are published into `resource_path('themes/default/views')`. There is
one set per Bagisto release whose views differ, and `VarnishServiceProvider::storefrontViewsDirectory()`
chooses which set to publish from `Webkul\Core\Core::BAGISTO_VERSION`:
| Set | Copied from |
|---|---|
| `publishables/views/shop-2.5.0` | Bagisto 2.5.0's views. Frozen, since that release will not change. |
| `publishables/views/shop` | The newest release, currently 2.5.1. |
Each set must stay an exact copy of its own release plus the holes. The storefront CSS is compiled
from core's views alone, so a class that one release's markup introduced does not exist in another
release's build, and the 2.5.1 header reads the category tree under a different `localStorage` key
from the one 2.5.0's home page writes. Every set has the same three files:
| Override | Why |
|---|---|
| `components/layouts/header/desktop/bottom.blade.php` | one dropdown hole |
| `components/layouts/header/mobile/index.blade.php` | three holes |
| `components/products/card.blade.php` | the one-line `isCustomerFlag` read |
`/flashes` (`varnish.session.flashes`) answers the session's flash messages as JSON, because a cached
page cannot carry them. **Nothing calls it yet.** On a cacheable route, core's flash group prints the
`<bagisto-response-cache-session-flashes>` placeholder, and with Spatie's middleware replaced nothing
fills it in, so flash messages on cached pages are currently lost.
### Keeping the overrides current — read before upgrading Bagisto
**An override only replaces a customer block; everything else in those three files is core's
markup, and it goes stale silently.** A stale override does not error — it quietly reverts core's
newer header to the version the package was forked from.
When a Bagisto release changes those views, **re-derive the newest set from core's current file**
rather than merging the old one forward. If the stores on the previous release are still supported,
first freeze the current `shop` set as `shop-<previous release>`, then move the version boundary in
`storefrontViewsDirectory()`. A three-way merge looks like it works and leaves structurally broken
Blade, because the region being replaced moves:
- In Bagisto 2.4 the `@guest('customer')` / `@auth('customer')` pair sat **inside one**
`<x-slot:content>`.
- In 2.5 each branch wraps **its own content slot** (`<x-slot:content>` for guests,
`<x-slot:content class="p-0!">` for authenticated customers).
The package supplies **one** content slot holding the dynamic view; the fragment branches on
guest/auth internally, so it serves both.
**The fragments go stale the same way**, and are easier to forget: each one is a copy of the
customer block core used to render inline, so when core restyles its dropdown the fragment keeps
the old markup. Diff each fragment against the matching block in core's current
`header/desktop/bottom.blade.php` and `header/mobile/index.blade.php` — compare the routes,
translation keys, `view_render_event` hooks and config checks rather than the Tailwind class
order, which differs harmlessly. The fragments are **shared by every set**, so a class in one has to
exist in the compiled CSS of every supported release. Where it does not, use an inline `style`, as
the desktop dropdown does for `contain: inline-size`.
**`php artisan view:cache` is not a syntax check** — it reports success on Blade whose compiled
PHP has a parse error, and the error only fires when the page renders. After touching an override,
compile it and lint the output, then load the page and watch the browser console: unbalanced
slots surface as a **Vue compiler error**, not a PHP one.
## Purging
`Providers/EventServiceProvider` bans the matching tags on product, category, CMS page, review,
URL-rewrite, order, refund, channel, configuration and theme-section events — create/update on the
`after` hook, delete on the `before` one, because the row has to still exist to build its tags.
`FlushVarnishCache` is the console entry point.
Purges go to the **Varnish Host URL**, not the backend URL — they are different settings, and
pointing them at Bagisto means no ban ever reaches Varnish.
## Admin configuration
`src/Config/system.php` is merged into `core` and nests under the top-level `cache_management`
group. Bagisto ships that group from 2.4 onwards — on 2.5 it comes from `Webkul\Admin`'s own
`system.php` — so the file **declares it only when it is absent**. Do not remove that guard, or the
section is declared twice.
The guard reads `config('core')` at merge time, which is the **second** reason the provider is
registered last: run before `AdminServiceProvider`, it sees no `cache_management`, declares its
own, and Admin's later merge appends a duplicate. Check after a change:
```bash
php artisan tinker --execute='echo collect(config("core"))->pluck("key")->filter(fn($k)=>$k==="cache_management")->count();'
# expect 1
```
## Conventions
- **Translation keys are kebab-case, route names are snake_case.** The same feature is
`shop::app.eu-withdrawal.guest-dropdown.link` as a key and `shop.eu_withdrawal.guest.lookup` as
a route. Never rename one by searching for the other's spelling.
- **All 23 Bagisto locales** (2.5.1 added `sw`) exist under `src/Resources/lang/`. `en` is the canonical structure;
every other locale carries the identical key set. `php artisan bagisto:translations:check` does
**not** cover this package — it scans only `base_path('packages/Webkul')`, so a clone symlinked
in through a path repository is never read. Compare each locale against `en` yourself.
- **Code style:** `vendor/bin/pint`, run from the application root.
- After changing providers, config or routes: `php artisan optimize:clear`, and
`php artisan responsecache:clear` before trusting what a page renders.
## Verifying a change
```bash
php artisan vendor:publish --provider="Webkul\Varnish\Providers\VarnishServiceProvider" --force
php artisan optimize:clear && php artisan responsecache:clear
# every ESI fragment must answer 200
for t in customer-desktop-dropdown customer-mobile-md-dropdown customer-mobile-sm-dropdown \
customer-account-profile-drawer customer-status; do
curl -s -o /dev/null -w "$t %{http_code}\n" "$APP_URL/esi?tag=$t"
done
```
Then load the storefront and confirm the browser console is clean — the overrides are where
breakage shows up first.
Guidance for AI agents (and humans) working inside the bagisto/bagisto-varnish package.
The README is the user-facing install and configuration guide; this file is
how the package is wired into Bagisto and what to watch for when changing it.
The package puts Varnish in front of Bagisto: storefront responses are tagged so Varnish can cache and ban them, and the parts of the page that differ per customer are fetched separately so a cached page stays correct for everyone.
Webkul\Varnish → src/master, tracking the newest Bagisto line — currently 2.5 (Laravel 13,
PHP 8.4). There is no long-lived 2.4 branch; that line is held by the v2.1.0 tag, so a 2.4
fix means branching from the tag. The README carries the
version matrix — update it there, not here, whenever a
release targets a new Bagisto line.VarnishServiceProvider is added to the application's
bootstrap/providers.php last. Composer auto-discovery is deliberately not declared.boot() calls $router->aliasMiddleware('cache.response', VarnishCacheMiddleware::class) —
Http\Middleware\VarnishCache, imported under that alias — replacing the alias Bagisto's own
ShopServiceProvider already registers for Webkul\Shop\Http\Middleware\CacheResponse. Core's
storefront routes carry that alias, so this swap is what redirects them from Bagisto's built-in
page cache to Varnish tagging.
It is a deliberate takeover, not an accident, and it is decided by boot order: registered before the Shop package, Shop overwrites it again and nothing is tagged. Verify it after any change:
php artisan tinker --execute='echo app("router")->getMiddleware()["cache.response"];'
# expect Webkul\Varnish\Http\Middleware\VarnishCache
A cached page cannot carry customer-specific markup, so the package cuts those blocks out and
fetches them per request. Every fragment is served by EsiController::loadView() at
/esi?tag=<tag> (route varnish.esi.load), and src/Config/varnish.php maps each tag to a view
under esi.views. There are two mechanisms, and which one a hole uses is not a free choice:
1. <x-varnish::dynamic-view view="…"> — for the four dropdowns.
src/Resources/views/shop/components/dynamic-view/index.blade.php registers a Vue component that
fetches the fragment and injects it with v-html. Used in the two overridden headers for
customer-desktop-dropdown, customer-mobile-md-dropdown, customer-mobile-sm-dropdown and
customer-account-profile-drawer. The HTML comment above each hole states the reason:
Varnish's own ESI cannot resolve a Blade component, so the package does the fetch itself.
Two consequences:
mousemove or touchstart, not on mount. Until the visitor moves
the pointer the dropdown is genuinely empty. A curl of the page, and a Playwright test that
clicks without moving first, both see nothing — that is the design, not a bug.v-html content is not compiled by Vue. A fragment must be plain HTML and Blade; a Vue
component or directive inside one silently does nothing. The current fragments observe this.2. A real <esi:include> — for customer-status.
src/Resources/views/shop/view-render-events/customer-status.blade.php is a hidden
<span id="isCustomerFlag"> holding a genuine <esi:include src="/esi?tag=customer-status" />,
which Varnish resolves because it is plain markup. The fragment is one line —
{{ auth()->guard('customer')->check() }} — so the span reads 1 or empty.
It reaches the page through Event::listen('bagisto.shop.layout.body.before', …) →
$viewRenderEventManager->addTemplate(...), so no core view is edited to place it. This is
what the third override depends on: components/products/card.blade.php differs from core in one
line, reading document.getElementById('isCustomerFlag') instead of calling
auth()->guard('customer')->check() while the page is being cached. Remove the listener and every
product card decides wishlist state from whoever warmed the cache.
The three overridden views are published into resource_path('themes/default/views'). There is
one set per Bagisto release whose views differ, and VarnishServiceProvider::storefrontViewsDirectory()
chooses which set to publish from Webkul\Core\Core::BAGISTO_VERSION:
| Set | Copied from |
|---|---|
publishables/views/shop-2.5.0 |
Bagisto 2.5.0's views. Frozen, since that release will not change. |
publishables/views/shop |
The newest release, currently 2.5.1. |
Each set must stay an exact copy of its own release plus the holes. The storefront CSS is compiled
from core's views alone, so a class that one release's markup introduced does not exist in another
release's build, and the 2.5.1 header reads the category tree under a different localStorage key
from the one 2.5.0's home page writes. Every set has the same three files:
| Override | Why |
|---|---|
components/layouts/header/desktop/bottom.blade.php |
one dropdown hole |
components/layouts/header/mobile/index.blade.php |
three holes |
components/products/card.blade.php |
the one-line isCustomerFlag read |
/flashes (varnish.session.flashes) answers the session's flash messages as JSON, because a cached
page cannot carry them. Nothing calls it yet. On a cacheable route, core's flash group prints the
<bagisto-response-cache-session-flashes> placeholder, and with Spatie's middleware replaced nothing
fills it in, so flash messages on cached pages are currently lost.
An override only replaces a customer block; everything else in those three files is core's markup, and it goes stale silently. A stale override does not error — it quietly reverts core's newer header to the version the package was forked from.
When a Bagisto release changes those views, re-derive the newest set from core's current file
rather than merging the old one forward. If the stores on the previous release are still supported,
first freeze the current shop set as shop-<previous release>, then move the version boundary in
storefrontViewsDirectory(). A three-way merge looks like it works and leaves structurally broken
Blade, because the region being replaced moves:
@guest('customer') / @auth('customer') pair sat inside one
<x-slot:content>.<x-slot:content> for guests,
<x-slot:content class="p-0!"> for authenticated customers).The package supplies one content slot holding the dynamic view; the fragment branches on guest/auth internally, so it serves both.
The fragments go stale the same way, and are easier to forget: each one is a copy of the
customer block core used to render inline, so when core restyles its dropdown the fragment keeps
the old markup. Diff each fragment against the matching block in core's current
header/desktop/bottom.blade.php and header/mobile/index.blade.php — compare the routes,
translation keys, view_render_event hooks and config checks rather than the Tailwind class
order, which differs harmlessly. The fragments are shared by every set, so a class in one has to
exist in the compiled CSS of every supported release. Where it does not, use an inline style, as
the desktop dropdown does for contain: inline-size.
php artisan view:cache is not a syntax check — it reports success on Blade whose compiled
PHP has a parse error, and the error only fires when the page renders. After touching an override,
compile it and lint the output, then load the page and watch the browser console: unbalanced
slots surface as a Vue compiler error, not a PHP one.
Providers/EventServiceProvider bans the matching tags on product, category, CMS page, review,
URL-rewrite, order, refund, channel, configuration and theme-section events — create/update on the
after hook, delete on the before one, because the row has to still exist to build its tags.
FlushVarnishCache is the console entry point.
Purges go to the Varnish Host URL, not the backend URL — they are different settings, and pointing them at Bagisto means no ban ever reaches Varnish.
src/Config/system.php is merged into core and nests under the top-level cache_management
group. Bagisto ships that group from 2.4 onwards — on 2.5 it comes from Webkul\Admin's own
system.php — so the file declares it only when it is absent. Do not remove that guard, or the
section is declared twice.
The guard reads config('core') at merge time, which is the second reason the provider is
registered last: run before AdminServiceProvider, it sees no cache_management, declares its
own, and Admin's later merge appends a duplicate. Check after a change:
php artisan tinker --execute='echo collect(config("core"))->pluck("key")->filter(fn($k)=>$k==="cache_management")->count();'
# expect 1
shop::app.eu-withdrawal.guest-dropdown.link as a key and shop.eu_withdrawal.guest.lookup as
a route. Never rename one by searching for the other's spelling.sw) exist under src/Resources/lang/. en is the canonical structure;
every other locale carries the identical key set. php artisan bagisto:translations:check does
not cover this package — it scans only base_path('packages/Webkul'), so a clone symlinked
in through a path repository is never read. Compare each locale against en yourself.vendor/bin/pint, run from the application root.php artisan optimize:clear, and
php artisan responsecache:clear before trusting what a page renders.php artisan vendor:publish --provider="Webkul\Varnish\Providers\VarnishServiceProvider" --force
php artisan optimize:clear && php artisan responsecache:clear
# every ESI fragment must answer 200
for t in customer-desktop-dropdown customer-mobile-md-dropdown customer-mobile-sm-dropdown \
customer-account-profile-drawer customer-status; do
curl -s -o /dev/null -w "$t %{http_code}\n" "$APP_URL/esi?tag=$t"
done
Then load the storefront and confirm the browser console is clean — the overrides are where breakage shows up first.
How can I help you explore Laravel packages today?