10.8 Global Page Resources in the assets/ Directory
Right, let’s talk about your assets/ directory. This isn’t just a junk drawer for your random files; it’s a meticulously organized, pre-compiled treasure chest. Think of it as your project’s VIP lounge. Files in here get special treatment: they’re bundled directly into your final application, untouched by the usual processing that other files (like those in lib/ or static/) might go through. Their names are hashed for cache-busting glory, and their original folder structure is (mostly) respected. It’s the first place you should reach for when you need an image, a font, a PDF, or any other static resource that’s core to your pages.
The assets/ Directory Structure and Import Behavior
You don’t just toss files in here willy-nilly. The structure you create within assets/ is meaningful. Let’s say you have this setup:
assets/
images/
logo.png
heroes/
main.jpg
docs/
spec.pdf
When you build your project, SvelteKit will faithfully recreate this structure within its internal build artifact. To actually get these files into your application, you must import them in your JavaScript. This is the magic incantation that tells the bundler (Vite) “hey, this file is important, include it!”
import logo from '$lib/assets/images/logo.png';
import heroImage from '$lib/assets/images/heroes/main.jpg';
import spec from '$lib/assets/docs/spec.pdf';
The $lib alias is key here. It points to src/lib, so we’re effectively importing from src/lib/assets/.... After this import, logo, heroImage, and spec won’t be the file contents themselves, but rather strings containing the final, hashed URL that Vite generates for the asset. This is the single most important concept to grasp. You’re importing a URL.
Why Importing a URL is Genius (and a Pain)
This mechanism is brilliant for one huge reason: cache busting. When you run vite build, Vite will output the file as something like logo-a1B2c3D4.png. If you ever change logo.png, the hash changes, and the filename changes. This forces browsers and CDNs to immediately fetch the new version, obliterating any stale cached copies. You never have to worry about users seeing an old image again.
The “pain” part is that you can’t dynamically construct these import paths. This will not work:
// THIS IS BROKEN CODE. DO NOT USE.
let imageName = 'logo.png';
import myImage from `$lib/assets/images/${imageName}`; // Nope.
The import path must be a static string literal. Why? Because all this import resolution happens at build time, not runtime. Vite needs to be able to see the path, find the file, and process it during the bundling phase. If you need dynamic assets, you’ll need to use the static/ directory instead, but you lose the cache-busting superpowers.
Referencing Assets in Your Markup
So you’ve imported this URL string. Now what? You use it anywhere a URL is expected.
In a regular <img> tag:
<script>
import logoUrl from '$lib/assets/images/logo.png';
</script>
<img src={logoUrl} alt="Company Logo" class="h-8 w-8" />
In a Svelte component’s style block, using url():
<script>
import heroUrl from '$lib/assets/images/heroes/main.jpg';
</script>
<div class="hero">
My cool content
</div>
<style>
.hero {
background-image: url({heroUrl});
}
</style>
This last example is a thing of beauty. Svelte and Vite work together to see that url({heroUrl}), resolve the imported variable to its final URL, and plop it right into your CSS.
Best Practices and the Font Gotcha
Organization is Key: Mimic your
src/routesstructure. If you have a page at/about/team, put its specific images inassets/images/about/team/. This saves you from the hell of 500 files in a singleassets/images/folder.Optimize Your Images Before Putting Them in
assets/: Vite will bundle them as-is. It won’t compress your 8MB PNG from your designer. Run them through something like Squoosh or ImageOptim first. Your users’ bandwidth will thank you.The Font Face Pitfall: This one gets everyone. You can’t use an imported URL in a
@font-facedeclaration in a.cssfile. The import magic only works within Svelte components. For global fonts, you have two options:- Put them in
static/and reference them with an absolute path (e.g.,url('/fonts/my-font.woff2')). You lose cache-busting. - Use a
<style global>block in your root+layout.svelteand import the font URLs there. This gives you cache-busting.
<!-- in src/routes/+layout.svelte --> <script> import myFontUrl from '$lib/assets/fonts/my-font.woff2'; </script> <style global> @font-face { font-family: 'MyFont'; src: url({myFontUrl}) format('woff2'); font-weight: 400; font-style: normal; } </style>- Put them in
It’s a slight hack, but it’s the price we pay for hashed perfection. A small quirk in an otherwise incredibly well-designed system.