
Understanding and Using Thymeleaf Fragments in Spring Boot with Kotlin
Master Thymeleaf fragments to build maintainable Spring Boot templates with Kotlin. This comprehensive guide covers everything from basic fragment creation to advanced techniques like parameterized fragments and layout templates, complete with real-world code examples.
Thymeleaf fragments are reusable components that help you maintain consistent layouts across your web application.
Instead of duplicating HTML code across multiple templates, you can create fragments once and reuse them throughout your application. In this tutorial, you'll learn how to use Thymeleaf fragments effectively in a Spring Boot application with Kotlin.
Prerequisites
To follow along with this tutorial, you'll need:
- Kotlin 1.9.0 or later
- Spring Boot 3.2.0 or later
- A basic understanding of Spring Boot and Thymeleaf
- Your favorite IDE (IntelliJ IDEA recommended)
Introduction
When building web applications, you often need to reuse certain elements across multiple pages, such as:
- Navigation bars
- Footers
- Sidebars
- Common scripts and stylesheets
- Reusable UI components
Thymeleaf fragments solve this problem by allowing you to define these elements once and reuse them across your templates.
Project Setup
First, let's create a new Spring Boot project with Kotlin and Thymeleaf. Add the following dependencies to your build.gradle.kts
:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib")
}
Creating Your First Fragment
Let's start by creating a simple navigation bar fragment. Create a new file src/main/resources/templates/fragments/navbar.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<nav th:fragment="navbar" class="navbar">
<div class="navbar-brand">
<a href="/" class="navbar-item">My Application</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<a href="/" class="navbar-item">Home</a>
<a href="/about" class="navbar-item">About</a>
<a href="/contact" class="navbar-item">Contact</a>
</div>
</div>
</nav>
</body>
</html>
The key part here is the th:fragment="navbar"
attribute, which defines this navigation bar as a reusable fragment named "navbar". You might notice that we included the full HTML structure (doctype, html, head, and body tags) even though we're only using a small part of it. This is because Thymeleaf requires fragments to be in valid HTML documents to process them correctly.
This requirement serves several purposes:
- It ensures that your IDE can provide proper HTML validation and autocompletion while you're working on fragments
- It allows Thymeleaf to process the fragment in the correct HTML context, which is important for proper namespace handling
- It makes fragments more maintainable since they're valid HTML documents that can be previewed independently
Don't worry about the extra tags adding overhead – when Thymeleaf processes the fragment (using th:replace
or th:insert
), it only includes the specific fragment content, not the surrounding HTML structure. The outer HTML structure is essentially documentation for developers and tooling.
Creating a Layout Template
A layout template serves as a master template for your application's pages, providing a consistent structure across your site. Think of it as a blueprint that defines where different parts of your page will go. This is particularly useful for maintaining consistency across your application while keeping your code DRY (Don't Repeat Yourself).
The layout template typically includes:
- Common HTML structure
- Shared resources (CSS and JavaScript files)
- Placeholder regions that individual pages can fill with their specific content
- Common components like navigation and footer
Let's create a base layout template that other pages can extend. We'll place it at src/main/resources/templates/layouts/main.html
. This location follows a common convention of keeping layouts separate from regular templates:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<!-- The title will be replaced by each page's specific title -->
<title th:replace="${title}">Default Title</title>
<!-- Common CSS and JavaScript that will be shared across all pages -->
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/main.js" defer></script>
</head>
<body>
<!-- Include the navbar fragment we created earlier -->
<div th:replace="~{fragments/navbar :: navbar}"></div>
<!--
Main content area - this will be replaced by each page's specific content
The default content is shown when viewing the template file directly
-->
<main th:replace="${content}">
<p>Default content</p>
</main>
<!--
Footer defined as a fragment within the layout itself
This is an alternative to creating a separate footer.html file
-->
<footer th:fragment="footer" class="footer">
<div class="content">
<p>© 2024 Your Application. All rights reserved.</p>
</div>
</footer>
</body>
</html>
Let's break down the key components of this layout template:
- Look in the
fragments/navbar.html
file - Find the element with
th:fragment="navbar"
- Replace this div with that fragment's content
Inline Fragment Definition:
<footer th:fragment="footer" class="footer">
This demonstrates that you can define fragments directly in your layout template. This approach is useful for smaller fragments that are specific to the layout and won't be reused elsewhere.
Content Placeholder:
<main th:replace="${content}">
<p>Default content</p>
</main>
This is where each page's specific content will be inserted. The default content inside the <main>
tag is only shown when viewing the template file directly and helps with development by providing visual feedback.
Navbar Integration:
<div th:replace="~{fragments/navbar :: navbar}"></div>
This line incorporates our previously created navbar fragment. The syntax ~{fragments/navbar :: navbar}
tells Thymeleaf to:
Shared Resources:
<link rel="stylesheet" href="/css/styles.css">
<script src="/js/main.js" defer></script>
These resources will be included in every page that uses this layout. Notice they're placed in the layout template to avoid repeating them in each individual page.
Replaceable Title:
<title th:replace="${title}">Default Title</title>
The th:replace="${title}"
attribute indicates that this element will be replaced with a title provided by each individual page. The "Default Title" text is shown when viewing the template file directly and helps with development.
By using this layout template, you ensure that all pages in your application will have:
- The same basic HTML structure
- Consistent styling and JavaScript resources
- A common navigation bar
- A consistent footer
- Their own specific title and main content
This approach significantly reduces duplication and makes it easier to maintain a consistent look and feel across your application. When you need to make a change that affects all pages (like adding a new CSS file or updating the footer), you only need to modify this layout template once, and all pages using it will automatically reflect the changes.
Using Fragments in Pages
Let's create a home page that uses our layout and fragments. Create src/main/resources/templates/home.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
th:replace="~{layouts/main :: html(
title=~{::title},
content=~{::main}
)}">
<head>
<title>Home - My Application</title>
</head>
<body>
<main>
<h1>Welcome to My Application</h1>
<p>This is the home page content.</p>
</main>
</body>
</html>
Creating the Controller
Now, let's create a simple controller to serve our pages:
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
@Controller
class HomeController {
@GetMapping("/")
fun home(): String {
return "home"
}
@GetMapping("/about")
fun about(): String {
return "about"
}
@GetMapping("/contact")
fun contact(): String {
return "contact"
}
}
Advanced Fragment Usage
Parameterized Fragments
You can create fragments that accept parameters, making them more flexible. Here's an example of a reusable card component:
<!-- fragments/card.html -->
<div th:fragment="card(title, content)" class="card">
<div class="card-header">
<h3 th:text="${title}">Card Title</h3>
</div>
<div class="card-content">
<p th:text="${content}">Card content goes here.</p>
</div>
</div>
Using the parameterized fragment:
<div th:replace="~{fragments/card :: card(
title='Welcome',
content='This is a parameterized card component.'
)}"></div>
Nested Fragments
You can also nest fragments within other fragments. Here's an example of a sidebar with nested menu items:
<!-- fragments/sidebar.html -->
<div th:fragment="sidebar">
<aside class="sidebar">
<div th:replace="~{fragments/sidebar :: menu-item('Home', '/')}"></div>
<div th:replace="~{fragments/sidebar :: menu-item('About', '/about')}"></div>
<div th:replace="~{fragments/sidebar :: menu-item('Contact', '/contact')}"></div>
</aside>
</div>
<div th:fragment="menu-item(text, url)" class="menu-item">
<a th:href="${url}" th:text="${text}">Menu Item</a>
</div>
Best Practices
- Use Semantic Names: Give your fragments clear, semantic names that describe their purpose.
- Keep Fragments Focused: Each fragment should have a single responsibility. Don't create fragments that try to do too much.
- Use Parameters: Make fragments flexible by accepting parameters when appropriate.
- Document Fragment Parameters: When creating complex fragments that accept parameters, document what parameters are required and their purpose.
Organize Fragments Logically: Keep your fragments organized in a dedicated directory structure, such as:
templates/
├── fragments/
│ ├── navbar.html
│ ├── footer.html
│ └── common/
│ ├── head.html
│ └── scripts.html
├── layouts/
│ └── main.html
└── pages/
├── home.html
├── about.html
└── contact.html
Common Pitfalls and Solutions
Problem 1: Fragment Not Found
If you see a "fragment not found" error, check:
- The fragment file path is correct
- The fragment name matches exactly
- You're using the correct syntax (
~{path :: fragment-name}
)
Problem 2: CSS/JavaScript Not Loading
When using fragments, ensure:
- Resource paths are correct relative to the application context
- You're including necessary resources in the layout template
- Static resource handling is properly configured in Spring Boot
Problem 3: Circular Fragment Dependencies
Avoid creating circular dependencies between fragments. If you need shared functionality, consider:
- Creating a common parent fragment
- Using utility fragments
- Refactoring to remove the circular dependency
Conclusion
Thymeleaf fragments are a powerful way to create maintainable and DRY templates in your Spring Boot application. By following the practices outlined in this guide, you can create a clean and maintainable template structure that scales well as your application grows.
For more advanced usage, consider exploring:
- Fragment expressions
- Fragment inheritance
- Conditional fragment inclusion
- Fragment preprocessing