SvelteKit static/ + src/app.html

SvelteKit Favicon Generator

SvelteKit is the odd one out: the static assets folder is called static/, not public/, and the head template is src/app.html with a %sveltekit.assets% placeholder in front of every path.

Drop an image
Drop a logo or image
PNG, JPG, WebP · square 512×512+ works best · non-square images are padded, never cropped · processed in your browser

Favicon Setup for Svelte — Free, Fast & Private

The name of the folder is configurable through the files.assets option in svelte.config.js, but almost nobody changes it, and every SvelteKit tutorial assumes static/. If you have been pasting instructions written for Vite or Next and wondering why nothing is being served, the folder you created is probably named public/ and is being ignored.

Where each generated file goes in Svelte

The generator above emits seven files with fixed names. Static assets in Svelte live in static/, and the markup is written in src/app.html. This is the mapping between the two.

Generated fileGoes toNotes
favicon.icostatic/favicon.icoNote the folder name — static/, not public/.
favicon-16x16.pngstatic/favicon-16x16.pngLinked from app.html.
favicon-32x32.pngstatic/favicon-32x32.pngLinked from app.html.
favicon-48x48.pngstatic/favicon-48x48.pngOptional loose copy of an .ico entry.
apple-touch-icon.pngstatic/apple-touch-icon.pngReplaces the default favicon.png the skeleton ships.
android-chrome-192x192.pngstatic/android-chrome-192x192.pngManifest icon.
android-chrome-512x512.pngstatic/android-chrome-512x512.pngManifest icon and splash.

src/app.html is a template, not a page

Every SvelteKit response is rendered into src/app.html. It contains two placeholders that the framework fills in — %sveltekit.head% for anything SvelteKit or your components contributed to the head, and %sveltekit.body% for the app itself. Whatever you write directly into that head is present on every route in the application, server-rendered and prerendered alike, which makes it the right home for favicons.

The skeleton project ships one line: a link element pointing at favicon.png, prefixed with %sveltekit.assets%. Replace that single line with the full block and you are done. There is no per-route configuration to think about and no plugin to install.

The one thing not to do is put favicon markup inside a <svelte:head> block in +layout.svelte. It technically works — it lands inside %sveltekit.head% — but it now runs through the head reconciler on every client-side navigation, and it is one more thing to keep in sync if you ever add a second root layout. Reserve <svelte:head> for the tags that genuinely vary per route, like titles and canonical URLs.

src/app.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%sveltekit.assets%/favicon.ico" sizes="16x16 32x32 48x48" />
    <link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png" />
    <link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png" />
    <link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    %sveltekit.head%
  </head>
  <body data-sveltekit-preload-data="hover">
    <div style="display: contents">%sveltekit.body%</div>
  </body>
</html>

Why %sveltekit.assets% and not a plain slash

A bare href="/favicon.ico" works right up until the app is deployed somewhere other than the root of a domain. SvelteKit supports that case through the paths.base option in svelte.config.js — set it to "/docs" and every internal link the framework generates gets that prefix. Hardcoded absolute paths in app.html do not, because SvelteKit has no reason to parse your handwritten HTML looking for URLs to rewrite.

%sveltekit.assets% is the escape hatch. It resolves to paths.assets when you have configured an external asset host, falls back to paths.base otherwise, and becomes an empty string when neither is set — which is why the placeholder version is byte-identical to the plain version on a root deployment and correct everywhere else. Using it costs nothing and removes an entire class of staging-only bug.

The exception is favicon.ico itself, which browsers will request at the literal root path regardless of what any tag says. On a base-path deployment that request lands outside your app, so a copy of the .ico at the true server root remains worthwhile insurance if you control that server.

What adapters do with static/

Whichever adapter you use, the contents of static/ are copied into the build output and served from the root of the app. With adapter-static, the entire site is prerendered and static/ files land beside the generated HTML, so the deployed tree is exactly what you would expect from a plain static host. With adapter-node they are served by the Node server, and with the platform adapters they usually become CDN-served static files, sometimes with far-future cache headers applied automatically.

That last detail is worth a moment. SvelteKit fingerprints the JavaScript and CSS it builds and can cache them immutably, but nothing in static/ is fingerprinted, so an aggressive CDN cache rule that covers the whole output can pin your old favicon in place for a very long time. If your host lets you set per-path cache headers, favicons deserve a shorter max-age than the hashed bundles.

svelte.config.js — base path and asset host

import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter(),
    paths: {
      base: '/docs',                  // deployed under a subpath
      // assets: 'https://cdn.example.com'  // optional external asset host
    },
  },
};

Plain Svelte without Kit

If your project has no src/routes directory and no svelte.config.js with a kit block, you are on the plain Svelte + Vite template, and everything above is the wrong advice. That template follows the ordinary Vite conventions: static files go in public/, index.html sits at the project root and is edited directly, and paths are plain absolute slashes with no placeholder.

The quickest way to tell them apart is to look for src/app.html. If it exists, you are in SvelteKit and static/ is your folder. If instead there is an index.html next to package.json, you are in a Vite app that happens to compile Svelte components, and the React or Vue instructions apply verbatim.

Whatever you paste, the artwork itself never leaves your machine. The set above is rendered with the Canvas API and the .ico container is assembled in JavaScript on your device, so an unreleased logo stays unreleased. If the 16px preview looks like mush, that is the honest signal to simplify the mark before it ships to a Svelte tab near you.

How it works

  1. Generate the set: Drop a square logo into the tool above. Everything renders on your device — the artwork never uploads.
  2. Copy into static/: SvelteKit uses static/, not public/. Delete the skeleton favicon.png while you are in there.
  3. Edit src/app.html: Replace the single default icon line with the full block, keeping the %sveltekit.assets% prefix on every href.
  4. Build and inspect: Run the build and confirm the files sit at the root of the output alongside the generated HTML, and that _app/ contains only hashed bundles.

Frequently asked questions

Does SvelteKit use public/ or static/ for favicons?
static/. The folder name is configurable through files.assets in svelte.config.js, but static/ is the default and what every tutorial assumes. Creating a public/ folder in a SvelteKit project does nothing — it is simply not served.
Where do the link tags go in SvelteKit?
src/app.html, inside the head, above the %sveltekit.head% placeholder. That template renders for every route, so a single edit covers the whole app with no per-route work.
What does %sveltekit.assets% actually resolve to?
The paths.assets value if you have set an external asset host, otherwise paths.base, and an empty string when neither is configured. That makes it identical to a plain leading slash on a root deployment and correct on a subpath deployment, where a hardcoded slash would break.
Should I use <svelte:head> in +layout.svelte instead?
It works, but it is the wrong tool here. Favicons never change between routes, so putting them in app.html keeps them out of the client-side head reconciler and out of any second root layout you add later. Save <svelte:head> for titles and canonical URLs.
The skeleton ships static/favicon.png — do I delete it?
Yes, once your own files are in place and app.html no longer references it. Leaving it costs nothing functionally but tends to get accidentally re-linked later when someone copies a snippet from the SvelteKit docs.
Why does my favicon go stale after a deploy?
Nothing in static/ is fingerprinted, unlike the hashed bundles under _app/. If your host applies one far-future cache rule to the entire build output, the old icon stays pinned. Give static/ a shorter max-age, or version the filename when you rebrand.
I am using plain Svelte, not SvelteKit.
Then you are on a normal Vite project: files go in public/, and you edit the index.html at the project root with plain absolute paths. The presence of src/app.html is the reliable signal that you are in SvelteKit and that static/ is the right folder.

All Image Tools

Solutions by use case