Ship images, JavaScript and other theme assets
Everything under a theme's public/ folder is exposed on the storefront at /themes/<vendor>/<name>/<path>. That is the only URL prefix that works for theme assets. It is the single most common source of "the image is 404-ing on production" mistakes, so lock in the convention early.
The URL layout
A file at custom/themes/hw/mytheme/public/img/hero.png is served at /themes/hw/mytheme/img/hero.png. The public/ segment is stripped, and the vendor and lowercased theme name are prepended.
custom/themes/hw/mytheme/
└── public/
├── css/ → /themes/hw/mytheme/css/*
├── js/ → /themes/hw/mytheme/js/*
├── img/ → /themes/hw/mytheme/img/*
└── fonts/ → /themes/hw/mytheme/fonts/*The vendor segment is required. A path that skips it (/themes/mytheme/img/hero.png) always 404s, no matter what the folder structure looks like on disk.
Reference an asset from twig
Use the Laravel asset() helper with the theme-relative path. Append ?v={{ hwCacheId }} so the browser cache invalidates on the next compile or update.
<img src="{{ asset('themes/hw/mytheme/img/hero.png') }}?v={{ hwCacheId }}" alt="Hero">
<script src="{{ asset('themes/hw/mytheme/js/menu.js') }}?v={{ hwCacheId }}" defer></script>Hardcoded paths (<script src="/themes/hw/mytheme/js/menu.js">) also work but bypass the app URL and cache buster. Prefer asset().
Reference an asset from SCSS
Use the same absolute URL, no helper needed. The compile output lives at /assets/css/, so relative url() paths would resolve there, not next to your SCSS source.
body {
background-image: url("/themes/hw/mytheme/img/bg.png");
}The cache buster
hwCacheId is a global twig variable that changes whenever theme CSS is recompiled or the branding is saved. Bumping it forces the browser to re-fetch stylesheets, scripts and images that changed. Every asset link the theme emits should include it as a query string, or a customer will keep seeing your old CSS after a deploy.
JavaScript conventions
Ship scripts under public/js/ and load them from the twig template that needs them. Prefer defer so the script waits for the DOM without blocking the page.
<script src="{{ asset('themes/hw/mytheme/js/evo-slider.js') }}?v={{ hwCacheId }}" defer></script>When several CMS elements depend on the same script (a slider used by header and testimonials, for example), put the script tag in a shared twig partial and include it from every element that needs it. Browsers de-duplicate identical <script src> tags, so an element rendered five times on a page only downloads its script once.
Fonts
Fonts the admin picks in the branding tab are downloaded automatically, stored under /public/assets/fonts/system/<slug>/, and referenced by @font-face rules the compile pipeline emits into the theme CSS. You do not ship or reference them yourself.
Ship a font manually only when the theme relies on a fixed typeface that must not follow the admin's choice. Drop the woff2 files under public/fonts/ and declare @font-face in your SCSS.
Every theme asset URL must include the vendor segment. /themes/hw/mytheme/img/logo.png works; /themes/mytheme/img/logo.png is broken. This trips up almost everyone at least once.