Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

A shared layout and static files

All blog pages share a header with a menu, a footer and the stylesheet link. Copying them into every template means editing three places later. In Thymeleaf shared pieces go into fragments and are inserted with one line. Styles and images are static files: Spring Boot serves them without processing.

A file with fragments

<!-- src/main/resources/templates/fragments/layout.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head(title)">
    <meta charset="UTF-8">
    <title th:text="${title} + ' | Blog'">Blog</title>
    <link rel="stylesheet" th:href="@{/css/site.css}">
</head>
<body>
<header th:fragment="header">
    <a th:href="@{/}">Blog</a>
    <a th:href="@{/posts/new}">New post</a>
</header>
 
<footer th:fragment="footer">
    <p>Anna and Oleg, 2026</p>
</footer>
</body>
</html>

th:fragment gives a piece a name. The head fragment has a title parameter — this way each page passes its own window title.

Inserting fragments

<!-- src/main/resources/templates/posts/list.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('Posts')}"></head>
<body>
<header th:replace="~{fragments/layout :: header}"></header>
 
<h1>Posts</h1>
<!-- the post list, as in the templates lesson -->
 
<footer th:replace="~{fragments/layout :: footer}"></footer>
</body>
</html>

th:replace="~{fragments/layout :: header}" replaces the tag with the header fragment from templates/fragments/layout.html. Parameters go in parentheses after the name: on a post page it is head(${post.title}). Do the same in show.html and form.html.

Styles

/* src/main/resources/static/css/site.css */
body { font-family: sans-serif; max-width: 720px; margin: 0 auto; }
.error { color: #b00020; }

Everything in the static folder is available from the site root: the static/css/site.css file opens at /css/site.css. The link is built by th:href="@{/css/site.css}" — it takes a prefix into account if the application is ever deployed somewhere other than the server root.

The result

curl http://localhost:8080/ now returns the assembled page (a fragment):

<head>
    <meta charset="UTF-8">
    <title>Posts | Blog</title>
    <link rel="stylesheet" href="/css/site.css">
</head>
<body>
<header>
    <a href="/">Blog</a>
    <a href="/posts/new">New post</a>
</header>

The window title came from the parameter, the header from the shared file. Change the menu in layout.html, and it changes on every page.

What lives where

PathAddress in the browser
src/main/resources/static/css/site.css/css/site.css
src/main/resources/static/img/logo.png/img/logo.png
src/main/resources/templates/posts/list.htmlOnly through a controller

Templates are not reachable by address — only a controller shows them.

Common mistake

If you get the fragment path wrong, for example ~{fragments/layot :: header}, the page answers 500 and the log reports Error resolving template [fragments/layot]. The path in ~{...} counts from the templates folder, and the name after :: comes from th:fragment. And do not put CSS into templates: the browser never gets files from there, so the styles simply do not apply.

Thanks for reading!

Donate to the site — we work hard for you.