Thymeleaf templates
Besides the API the blog needs ordinary pages for people: a list of posts and a page for one post. They are assembled on the server by a template engine — it takes an HTML file with special attributes and fills in the data. In Spring Boot the usual choice is Thymeleaf. Its templates are valid HTML: you can open the file in a browser even without the server.
Setup
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>Spring Boot looks for templates in the src/main/resources/templates/ folder — create it.
A page controller
// src/main/java/com/example/blog/PageController.java
package com.example.blog;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class PageController {
private final PostService postService;
private final CommentService commentService;
public PageController(PostService postService, CommentService commentService) {
this.postService = postService;
this.commentService = commentService;
}
@GetMapping("/")
public String index(Model model) {
var page = PageRequest.of(0, 10, Sort.by("createdAt").descending());
model.addAttribute("posts", postService.findAll(page).getContent());
return "posts/list";
}
@GetMapping("/posts/{id}")
public String show(@PathVariable Long id, Model model) {
model.addAttribute("post", postService.findById(id));
model.addAttribute("comments", commentService.findByPost(id));
return "posts/show";
}
}This is @Controller, not @RestController: the method returns a template name. The string "posts/list" means the templates/posts/list.html file. Model is the set of data for the template: whatever you put in with addAttribute, the template sees by name. The services are the same ones the API uses.
The list template
<!-- src/main/resources/templates/posts/list.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Blog</title>
</head>
<body>
<h1>Posts</h1>
<p th:if="${posts.isEmpty()}">No posts yet.</p>
<ul>
<li th:each="post : ${posts}">
<a th:href="@{/posts/{id}(id=${post.id})}" th:text="${post.title}">Title</a>
<small th:text="|by ${post.author}|">by anna</small>
</li>
</ul>
</body>
</html>| Attribute | What it does |
|---|---|
th:text="${post.title}" | Replaces the tag's text with the value; HTML special characters are escaped |
th:each="post : ${posts}" | Repeats the tag for each list element |
th:if="${...}" | Outputs the tag only if the condition is true |
th:href="@{/posts/{id}(id=${post.id})}" | Builds a link and fills in the parameter |
The |by ${post.author}| notation joins text and a value. The text inside the tags ("Title") is a placeholder for viewing the file without the server; Thymeleaf replaces it.
The result
Open http://localhost:8080/ or run curl http://localhost:8080/:
<h1>Posts</h1>
<ul>
<li>
<a href="/posts/1">Hello, Spring</a>
<small>by anna</small>
</li>
</ul>The th:* attributes are gone from the response — the browser gets clean HTML. The posts/show.html template works the same way: the title, the body and a th:each over the comments. A date there is easy to print with ${#temporals.format(post.createdAt, 'dd.MM.yyyy HH:mm')}.
Common mistake
If the template name does not match a file, for example you returned "posts/lst", the page answers 500 and the log says:
Error resolving template [posts/lst], template might not exist or might not be accessible by any of the configured Template ResolversCheck the path: the name counts from the templates folder, without .html at the end. And do not mix up the annotations: @RestController would send the browser just the text posts/list.