How to Add a Favicon to Spring Boot + Thymeleaf
How to Add a Favicon to Your Spring Boot + Thymeleaf Project
You've been heads-down building features and suddenly someone points out the site still has the default browser icon in the tab. Setting up a favicon is straightforward, but there are a few gotchas in the Spring Boot + Thymeleaf stack that can eat your time if you don't know about them. Here's the full walkthrough.
Generate the Favicon Files
Head to favicon.io and create your icon from text or an image. Download the zip, which contains:
favicon.ico— legacy multi-size icon (16×16, 32×32)favicon-16x16.png,favicon-32x32.png— modern browser tabsapple-touch-icon.png(180×180) — iOS home screenandroid-chrome-192x192.png,android-chrome-512x512.png— Android and PWAsite.webmanifest— PWA manifest file
Place the Files
Drop them into your static resources directory. If you need separate favicons for different sections or tenants, use subdirectories.
src/main/resources/static/favicon/
├── main/
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ └── site.webmanifest
└── secondary/
└── (same structure)
Declare in the HTML Head
Add the link tags to your Thymeleaf layout's <head>. Use th:href="@{...}" so the context path is resolved automatically — important if your app doesn't run at /.
<link rel="icon" type="image/x-icon" th:href="@{/favicon/main/favicon.ico}">
<link rel="icon" type="image/png" sizes="32x32" th:href="@{/favicon/main/favicon-32x32.png}">
<link rel="icon" type="image/png" sizes="16x16" th:href="@{/favicon/main/favicon-16x16.png}">
<link rel="apple-touch-icon" sizes="180x180" th:href="@{/favicon/main/apple-touch-icon.png}">
<link rel="manifest" th:href="@{/favicon/main/site.webmanifest}">
If you have multiple favicon sets, wrap each in a Thymeleaf fragment and conditionally include them from your layout.
Update site.webmanifest
Make sure the icon paths in the manifest match your actual file locations:
{
"name": "",
"short_name": "",
"icons": [
{ "src": "/favicon/main/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/favicon/main/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Handle Spring Security
Even if you have anyRequest().permitAll(), add the favicon path to WebSecurity ignoring. This skips the entire security filter chain for these requests, removing unnecessary overhead.
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/static/**", "/favicon/**");
}
Fix "Manifest: Line: 1, column: 1, Syntax error"
This error means the JSON parser failed on the very first character. The JSON itself is usually fine — the culprit is a UTF-8 BOM (Byte Order Mark, EF BB BF) prepended to the file. Some editors on Windows add this silently.
In VS Code, click the encoding indicator in the bottom bar, choose "Save with Encoding", and select UTF-8 (without BOM). Also verify the file starts with { — no blank lines or whitespace before it.
Deal with Aggressive Caching
Browsers cache favicons hard. If your new icon doesn't appear after deployment, clear the browser cache or navigate directly to the favicon URL and hard-refresh with Ctrl+Shift+R. As a last resort, append a version query parameter:
<link rel="icon" ... th:href="@{/favicon/main/favicon-32x32.png?v=2}">
Summary
Three steps: place files under static/ → add <link> tags in your layout head → add the path to Security ignoring. Copy the HTML snippet from favicon.io and swap href for th:href="@{...}". Watch out for the BOM encoding trap on the manifest file.