Vite 8 Broke Our Static Assets: Fixing Broken Icons in a Sage/Laravel Vite Plugin Theme
Every SVG icon on our Nynaeve WordPress theme — the icons in service-hero, trust-bar, feature-cards, icon-grid, and contact-section blocks — quietly turned into broken images. Not one icon, all of them, in the editor and on the live site. Nothing in our theme code had changed. What had changed was Vite: an upgrade to Vite 8 silently stopped bundling our static assets, and the block binding that resolved icon URLs had nothing left to resolve. Here’s what broke, why, and the one-option fix.
Quick Summary: Vite 8 tree-shakes any import.meta.glob(...) call whose return value is unused — exactly the pattern Laravel’s Vite integration used to recommend for pulling static assets (images, icons, fonts) into the build manifest without importing them from JS. If nothing consumes the glob’s return value, Rolldown removes it, the assets never enter the module graph, and Vite::asset() throws for every path that isn’t there. laravel-vite-plugin v3.0.0 added an assets option that uses Vite’s emitFile API instead, which isn’t affected by tree-shaking. Adding one line to vite.config.js fixed it.
What We Saw
We were drafting new pages using blocks that rely on imagewize/theme-icon, a custom WordPress block-binding source that resolves an SVG icon path (e.g. icon-shield.svg) to a built asset URL at render time. Every icon came back as a broken-image placeholder — locally and, checked via curl, on production too. Not a stale cache, not a stale hash: a fresh npm run build and a grep of the resulting manifest for icons returned zero matches. (A build that succeeds but whose changes still don’t appear is a different failure mode — usually a caching layer in a Trellis and Bedrock stack rather than the build itself; we walk through those separately in our Trellis and Bedrock local theme workflow guide.) Here, though, the icon files were structurally excluded from the build, full stop.
The binding source itself was straightforward and hadn’t changed:
register_block_bindings_source('imagewize/theme-icon', [
'get_value_callback' => function (array $source_args, \WP_Block $block_instance, string $attribute_name): ?string {
// ...
return Vite::asset('resources/images/icons/'.$icon_path);
},
]);
Vite::asset() only resolves paths that exist as keys in public/build/manifest.json. If a file was never part of Vite’s module graph, the call throws and our callback returns null — which is exactly what was happening for every icon.
Root Cause: Vite 8 Tree-Shakes Unused import.meta.glob Calls
For years, the documented way to get static assets — images, fonts, anything referenced only from Blade, never imported by JS or CSS — into Vite’s manifest was a throwaway glob call in the entry point:
import.meta.glob([
'../images/**',
'../fonts/**',
]);
Nothing ever used the return value — the point was purely the side effect of pulling matched files into the build. That’s exactly the shape of code a bundler’s dead-code elimination is designed to remove, and Vite 8 (via its Rolldown-based build) started doing precisely that. As tracked in vitejs/vite issue #21876, Vite 8 began emitting a /* #__PURE__ */ annotation on converted globs and tree-shaking them away when unreferenced — silently, with no warning and no build error. The files simply stopped appearing in the manifest.
This is what made it nasty to diagnose: it isn’t a “forgot to rebuild” problem, and there’s no error to grep the logs for. A completely clean, successful npm run build just quietly drops assets it used to include.
The Fix: The assets Option in laravel-vite-plugin v3
laravel-vite-plugin v3.0.0 shipped Vite 8 support (laravel/vite-plugin PR #342) along with a replacement mechanism built specifically for this: an assets option that uses Vite’s emitFile API inside a buildStart hook rather than a glob import, so it’s structurally immune to tree-shaking. Laravel’s own docs describe it plainly:
“To accomplish this, you need to make Vite aware of your assets by specifying them in the plugin’s
assetsoption. This option is intended for static files that you want to reference directly withVite::asset… Prior to version 3 of the Laravel Vite plugin, static assets had to be imported in your application’s entry point usingimport.meta.glob. Theassetsoption was introduced due to changes in Vite 8.”
— Laravel 13.x docs, “Processing Static Assets With Vite”
The fix in site/web/app/themes/nynaeve/vite.config.js was one added key on the existing laravel() plugin call:
laravel({
input: [ /* ... */ ],
assets: ['resources/images/**'],
refresh: true,
}),
After rebuilding, the manifest went from zero entries under resources/images/icons/** to 49, and every icon path our theme’s enqueue_block_editor_assets hook expects (icon-shield.svg, icon-clock.svg, the elayne/ subfolder icons, and so on) resolved to a real built file again.
This shipped in our theme as imagewize/nynaeve PR #141, released in Nynaeve v2.15.3.
Update (July 2026): We followed up with a cleanup in imagewize/nynaeve PR #142 (Nynaeve v2.15.4) that deletes the now-dead import.meta.glob(['../images/**', '../fonts/**']) call from resources/js/app.js entirely. Once Vite 8 tree-shakes it, that line does nothing — and both paths it used to cover are already handled by supported mechanisms: images through the assets option in vite.config.js (the fix above), and fonts through real url() references in resources/css/fonts.css, which keep them in the module graph. A npm run build confirmed every font and image manifest entry stayed intact after removal, making it a true no-op. If you apply the assets-option fix, don’t leave the old glob call sitting there as dead code — remove it.
Not a Breaking Change for Existing Content
Because our block-binding source stores the stable, un-hashed args.path (e.g. icon-shield.svg) rather than a resolved, hashed URL, every existing icon-grid, feature-cards, trust-bar, and service-hero instance started resolving correctly the moment the manifest was fixed — no re-insertion, no content migration. Storing a stable reference and resolving it in PHP at render time is the same pattern we use elsewhere in this theme — it’s how we add Article schema to posts as hand-rolled JSON-LD instead of leaning on a plugin. The only instances that stayed broken were pre-binding blocks that had hardcoded an old hashed url directly in content; those needed a one-time re-insert, but that’s a separate, unrelated issue.
If you’d worked around this with the common stopgap — uploading each icon to the Media Library and linking the resulting /app/uploads/... URL directly in page content — that workaround still works, but it’s now unnecessary going forward, and it doesn’t automatically get replaced by the binding; new content can go back to using the binding cleanly.
Affected Versions
| Component | Version |
|---|---|
| Vite | 8.0.16 (any Vite 8.x with the tree-shaking change) |
| laravel-vite-plugin | < 3.0.0 lacks the fix; 3.1.0 confirmed working |
| Framework | Sage 11 / Acorn (any Laravel-Vite integration using the old glob pattern) |
If You’re Hitting This Too
- Grep your
public/build/manifest.jsonfor the directory your static assets live in, right after a cleannpm run build. Zero matches confirms this issue rather than a caching problem. - Search your entry points (
app.js,editor.js) for an unusedimport.meta.glob(...)call targeting images/fonts/icons — that’s the pattern Vite 8 now tree-shakes away. - Confirm your
laravel-vite-pluginversion supports theassetsoption (v3.0.0+); upgrade if not. - Move the glob’s path patterns into the plugin’s
assetsoption instead of leaving them as a bareimport.meta.globcall, then rebuild and re-check the manifest.
Frequently Asked Questions
- Why did my Vite build succeed but my images/icons still 404? Vite 8 tree-shakes unused
import.meta.glob(...)calls, which was the standard way to pull static assets into the manifest without importing them from JS. If the glob’s return value isn’t used, the assets are silently dropped from the build — no error, no warning. - What is the assets option in laravel-vite-plugin? A configuration option (added in v3.0.0) that tells the plugin which static file globs to emit into the build manifest via Vite’s
emitFileAPI, so they’re versioned and resolvable throughVite::asset()— without needing an unusedimport.meta.globimport and without being affected by tree-shaking. - Do I need to re-insert existing content after this fix? Only if that content hardcoded an old, hashed Vite URL directly. If it references a binding source by stable path (like ours does), existing blocks resolve correctly automatically once the manifest is fixed — no content migration needed.
- Is this specific to WordPress or Sage? No — it’s a Vite 8 /
laravel-vite-pluginissue that affects any Laravel (or Laravel-adjacent, like Sage/Acorn) application relying on the oldimport.meta.globpattern for static assets referenced only from Blade or PHP.
Stuck on a Vite, Sage, or Build Tooling Bug?
We build and maintain Sage-based WordPress themes and troubleshoot the Vite/Laravel/Acorn build chain for agencies and freelance developers — including exactly this kind of “the build succeeds but assets silently break” issue.
- Sage 10 / 11 theme builds, upgrades, and migrations
- Vite / laravel-vite-plugin / Acorn build troubleshooting
- Custom block bindings and WordPress block development
- White-label WordPress development for agencies