Thymeleaf Fragments That Actually Scale: th:include vs th:replace for Blog Layouts
th:include and th:replace look interchangeable but produce different HTML. Learn why th:replace should be your default for blog headers, footers, and post cards, with a worked Spring Boot example.
14 Jul 2026, 22:11 UTC

Every blog template starts the same way: one HTML file, then two, then fifteen, all with the same header and footer pasted in. The first time you change the navigation, you edit fifteen files and miss two. Thymeleaf's fragment system solves this, but the moment you reach for it you hit a real decision: th:include or th:replace? They look interchangeable in tutorials. They are not, and picking the wrong one quietly produces invalid HTML that browsers then "fix" for you in surprising ways.
The short version: use th:replace for almost everything. The rest of this post explains why, with a concrete header/footer setup you can adapt directly.
What a fragment actually is
A fragment is a named, reusable chunk of markup living in its own file. You declare it with th:fragment and reference it from other templates with the expression syntax ~{file :: fragmentName}. Because Thymeleaf is a natural templating engine, the fragment file is still valid HTML on its own — you can open footer.html directly in a browser and preview it without running the application. That alone is worth the setup cost compared to JSP includes or server-side includes.
The difference that matters
Given this fragment in templates/fragments/footer.html:
<footer th:fragment="footer">
<p>© 2026 My Blog</p>
</footer>And this usage in a blog page:
<div th:include="~{fragments/footer :: footer}"></div>th:include copies the contents of the fragment into the host element, keeping the host tag. The rendered output is:
<div>
<p>© 2026 My Blog</p>
</div>Note what happened: the fragment's own <footer> tag was discarded, and only its children were inserted. Your semantic <footer> element vanished, replaced by a meaningless <div>.
th:replace substitutes the host element entirely with the fragment, tag and all:
<div th:replace="~{fragments/footer :: footer}"></div>renders as:
<footer>
<p>© 2026 My Blog</p>
</footer>The fragment author controls the final markup. The host tag is just a placeholder. This is almost always what you want for headers, footers, nav bars, and post cards — the fragment file reads exactly like the output it produces, which keeps the browser-preview benefit honest.
A worked blog layout
Assume a Spring Boot app with the Thymeleaf starter (behavior described here is consistent across Thymeleaf 3.0+). Put shared pieces under src/main/resources/templates/fragments/:
<!-- fragments/header.html -->
<header th:fragment="header">
<nav>
<a th:href="@{/}">Home</a>
<a th:href="@{/archive}">Archive</a>
</nav>
</header>Then compose a post page:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:replace="~{fragments/header :: header}"></div>
<article>
<h1 th:text="${post.title}">Post title</h1>
<div th:utext="${post.body}">Post body</div>
</article>
<div th:replace="~{fragments/footer :: footer}"></div>
</body>
</html>Fragments can also take parameters, which is how you build a reusable post card:
<!-- fragments/postCard.html -->
<section th:fragment="card(post)">
<h2 th:text="${post.title}">Title</h2>
<p th:text="${post.summary}">Summary</p>
</section><div th:replace="~{fragments/postCard :: card(post=${p})}"></div>To verify it works: start the app, request the page, and view source in the browser. You should see the <header> and <footer> tags in place of the placeholder <div>s — if you see the <div> wrapping fragment children instead, you used th:include. While iterating, set spring.thymeleaf.cache=false in application.properties so template edits show up without a restart; re-enable caching in production, since parsing templates on every request is wasted work.
Trade-offs and limitations
Fragment composition is not free. Each th:replace is a template resolution at render time, and deeply nested fragment hierarchies become hard to trace — when the output is wrong, you may be hunting through three levels of includes. Keep composition shallow: one layout level plus leaf components covers most blogs.
Two concrete gotchas. First, fragment files must be well-formed markup; a mismatched tag inside a fragment produces a template parsing error at render time, not a friendly warning. Second, th:include still has one legitimate niche: when you genuinely want to inject only the fragment's children into an existing wrapper you control. If that's not your intent, it's the wrong tool. (Note also that Thymeleaf 3.1 added th:insert as the clearer-named synonym for th:include; if you're on 3.1+, prefer the newer name for readability.)
Actionable takeaway
Default to th:replace for every shared component, write each fragment as the exact markup you want in the output, and verify with a single view-source check that the semantic tags survived. Reserve th:include/th:insert for the rare case where the wrapper belongs to the calling page. Your future self — editing the nav in exactly one place — will thank you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.