Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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>
AttributeWhat 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 Resolvers

Check 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.

Thanks for reading!

Donate to the site — we work hard for you.