Create a Blog Application Using Spring Boot and Thymeleaf

by Didin J. on Aug 28, 2025 Create a Blog Application Using Spring Boot and Thymeleaf

Learn how to build a blog app with Spring Boot 3, Thymeleaf, Hibernate, and MySQL. Step-by-step guide with admin dashboard, fragments, and styling.

Build a full-featured (but concise) blog with Spring Boot 3.5.x, Spring Data JPA, Thymeleaf, Validation, H2 (dev) and Bootstrap 5. You’ll implement posts, categories, and comments; add validation; and style it with Bootstrap.

Stack: Java 21, Spring Boot 3.5.x, Web, Thymeleaf, Data JPA, Validation, Lombok, H2 (dev), PostgreSQL (optional)

What You’ll Build

  • Public pages: Home (list of posts with search & pagination), Post detail (with comments + add comment form)

  • Admin pages: Create/Edit/Delete posts and categories (simple, no auth – optional security can be added later)

  • Data model: Post, Category, Comment with timestamps and validation

Prerequisites

  • Java 21 installed

  • Maven (or Gradle)

  • IDE (IntelliJ IDEA, VS Code, Eclipse)

  • Basic Spring Boot familiarity


Project Setup (Spring Initializr)

Use https://start.spring.io and generate a Maven project:

  • Group: com.djamware.blog

  • Artifact: blog-app

  • Java: 21

  • Dependencies: Spring Web, Thymeleaf, Spring Data JPA, Validation, H2 Database, Lombok, (optional) Spring Boot DevTools

Create a Blog Application Using Spring Boot and Thymeleaf - Spring Initailzr

Unzip and open in your IDE.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.djamware.blog</groupId>
	<artifactId>blog-app</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>blog-app</name>
	<description>Demo project for Spring Boot</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

Gradle users: Add starters accordingly. The code that follows is identical.


Configuration

Use YAML for clarity.

src/main/resources/application.yml

spring:
  datasource:
    url: jdbc:h2:mem:blogdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
    open-in-view: false
thymeleaf:
  cache: false
h2:
  console:
    enabled: true
    path: /h2-console
mvc:
  hiddenmethod:
    filter:
      enabled: true

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.orm.jdbc.bind: trace
server:
  port: 8085

For PostgreSQL, replace the datasource with your JDBC URL/driver and add the org.postgresql:postgresql dependency.


Enable JPA Auditing

src/main/java/com/djamware/blog/BlogAppApplication.java

package com.djamware.blog.blog_app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class BlogAppApplication {

	public static void main(String[] args) {
		SpringApplication.run(BlogAppApplication.class, args);
	}

}


Domain Model (Entities)

model/Post.java

package com.djamware.blog.blog_app.model;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Lob;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "posts")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 3, max = 150)
    @Column(nullable = false, length = 150)
    private String title;

    @NotBlank
    @Lob
    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    @Builder.Default
    private List<Comment> comments = new ArrayList<>();

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedAt;
}

model/Category.java

package com.djamware.blog.blog_app.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "categories")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 3, max = 60)
    @Column(nullable = false, unique = true, length = 60)
    private String name;

    @Size(max = 255)
    private String description;
}

model/Comment.java

package com.djamware.blog.blog_app.model;

import java.time.LocalDateTime;

import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "comments")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 2, max = 80)
    @Column(nullable = false, length = 80)
    private String author;

    @NotBlank
    @Size(min = 3, max = 1000)
    @Column(nullable = false, length = 1000)
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id", nullable = false)
    private Post post;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;
}


Repositories

repository/PostRepository.java

package com.djamware.blog.blog_app.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import com.djamware.blog.blog_app.model.Post;

public interface PostRepository extends JpaRepository<Post, Long> {
    Page<Post> findByTitleContainingIgnoreCase(String title, Pageable pageable);
}

repository/CategoryRepository.java

package com.djamware.blog.blog_app.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.djamware.blog.blog_app.model.Category;

public interface CategoryRepository extends JpaRepository<Category, Long> {
    Optional<Category> findByNameIgnoreCase(String name);
}

repository/CommentRepository.java

package com.djamware.blog.blog_app.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.djamware.blog.blog_app.model.Comment;

public interface CommentRepository extends JpaRepository<Comment, Long> {
}


Services

Interfaces

service/PostService.java

package com.djamware.blog.blog_app.service;

import java.util.Optional;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import com.djamware.blog.blog_app.model.Post;

public interface PostService {
    Page<Post> list(String q, Pageable pageable);

    Optional<Post> get(Long id);

    Post save(Post post);

    void delete(Long id);
}

service/CategoryService.java

package com.djamware.blog.blog_app.service;

import java.util.List;
import java.util.Optional;

import com.djamware.blog.blog_app.model.Category;

public interface CategoryService {
    List<Category> list();

    Optional<Category> get(Long id);

    Category save(Category category);

    void delete(Long id);
}

service/CommentService.java

package com.djamware.blog.blog_app.service;

import com.djamware.blog.blog_app.model.Comment;

public interface CommentService {
    Comment save(Comment comment);
}

Implementations

service/PostServiceImpl.java

package com.djamware.blog.blog_app.service;

import java.util.Optional;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import com.djamware.blog.blog_app.model.Post;
import com.djamware.blog.blog_app.repository.PostRepository;

@Service
public class PostServiceImpl implements PostService {

    private final PostRepository postRepository;

    public PostServiceImpl(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    @Override
    public Page<Post> list(String q, Pageable pageable) {
        if (q == null || q.isBlank()) {
            return postRepository.findAll(pageable);
        }
        return postRepository.findByTitleContainingIgnoreCase(q.trim(), pageable);
    }

    @Override
    public Optional<Post> get(Long id) {
        return postRepository.findById(id);
    }

    @Override
    public Post save(Post post) {
        return postRepository.save(post);
    }

    @Override
    public void delete(Long id) {
        postRepository.deleteById(id);
    }
}

service/CategoryServiceImpl.java

package com.djamware.blog.blog_app.service;

import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;

import com.djamware.blog.blog_app.model.Category;
import com.djamware.blog.blog_app.repository.CategoryRepository;

@Service
public class CategoryServiceImpl implements CategoryService {

    private final CategoryRepository categoryRepository;

    public CategoryServiceImpl(CategoryRepository categoryRepository) {
        this.categoryRepository = categoryRepository;
    }

    @Override
    public List<Category> list() {
        return categoryRepository.findAll();
    }

    @Override
    public Optional<Category> get(Long id) {
        return categoryRepository.findById(id);
    }

    @Override
    public Category save(Category category) {
        return categoryRepository.save(category);
    }

    @Override
    public void delete(Long id) {
        categoryRepository.deleteById(id);
    }
}

service/CommentServiceImpl.java

package com.djamware.blog.blog_app.service;

import org.springframework.stereotype.Service;

import com.djamware.blog.blog_app.model.Comment;
import com.djamware.blog.blog_app.repository.CommentRepository;

@Service
public class CommentServiceImpl implements CommentService {

    private final CommentRepository commentRepository;

    public CommentServiceImpl(CommentRepository commentRepository) {
        this.commentRepository = commentRepository;
    }

    @Override
    public Comment save(Comment comment) {
        return commentRepository.save(comment);
    }
}


Controller Layer

The controller layer handles HTTP requests and responses, connecting the service layer with the view templates (Thymeleaf). We'll create two controllers:

  1. BlogController – handles public blog pages (list and details).

  2. AdminController – handles blog management (create, edit, delete).

1. Create BlogController

controller/BlogController.java

package com.djamware.blog.blog_app.controller;

import java.util.Optional;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import com.djamware.blog.blog_app.model.Post;
import com.djamware.blog.blog_app.service.PostService;

@Controller
public class BlogController {

    private final PostService postService;

    public BlogController(PostService postService) {
        this.postService = postService;
    }

    // List all posts
    @GetMapping("/")
    public String viewHomePage(Model model) {
        Pageable pageable = PageRequest.of(0, 10);
        model.addAttribute("posts", postService.list("", pageable));
        return "index";
    }

    // View post details
    @GetMapping("/post/{id}")
    public String viewPost(@PathVariable Long id, Model model) {
        Optional<Post> post = postService.get(id);
        model.addAttribute("post", post.get());
        return "post";
    }
}

2. Create AdminController

controller/AdminController.java

package com.djamware.blog.blog_app.controller;

import java.util.Optional;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.djamware.blog.blog_app.model.Post;
import com.djamware.blog.blog_app.service.PostService;

@Controller
@RequestMapping("/admin")
public class AdminController {

    private final PostService postService;

    public AdminController(PostService postService) {
        this.postService = postService;
    }

    // List posts for admin
    @GetMapping("/posts")
    public String listPosts(Model model) {
        Pageable pageable = PageRequest.of(0, 10);
        model.addAttribute("posts", postService.list("", pageable));
        return "admin/posts";
    }

    // Show form to create a new post
    @GetMapping("/posts/new")
    public String showCreateForm(Model model) {
        model.addAttribute("post", new Post());
        return "admin/create_post";
    }

    // Save new post
    @PostMapping("/posts")
    public String savePost(@ModelAttribute("post") Post post) {
        postService.save(post);
        return "redirect:/admin/posts";
    }

    // Show form to edit an existing post
    @GetMapping("/posts/edit/{id}")
    public String showEditForm(@PathVariable Long id, Model model) {
        model.addAttribute("post", postService.get(id));
        return "admin/edit_post";
    }

    // Update post
    @PostMapping("/posts/{id}")
    public String updatePost(@PathVariable Long id, @ModelAttribute("post") Post post) {
        Optional<Post> existingPost = postService.get(id);
        existingPost.get().setTitle(post.getTitle());
        existingPost.get().setContent(post.getContent());
        postService.save(existingPost.get());
        return "redirect:/admin/posts";
    }

    // Delete post
    @GetMapping("/posts/delete/{id}")
    public String deletePost(@PathVariable Long id) {
        postService.delete(id);
        return "redirect:/admin/posts";
    }
}


Thymeleaf Templates

Create a new folder inside src/main/resources/templates and add the following files:

1. index.html (Homepage – list of posts)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>My Blog</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" />
  </head>
  <body>
    <h1>Welcome to My Blog</h1>

    <div th:if="${posts.isEmpty()}">
      <p>No blog posts available.</p>
    </div>

    <div th:each="post : ${posts}">
      <h2>
        <a th:href="@{/post/{id}(id=${post.id})}" th:text="${post.title}"></a>
      </h2>
      <p th:text="${#strings.abbreviate(post.content, 150)}"></p>
      <hr />
    </div>
  </body>
</html>

2. post.html (Single post details)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title th:text="${post.title}">Post</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" />
  </head>
  <body>
    <h1 th:text="${post.title}"></h1>
    <p th:text="${post.content}"></p>
    <a th:href="@{/}">← Back to Home</a>
  </body>
</html>

3. admin/posts.html (Admin – list of posts)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Admin - Posts</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" />
  </head>
  <body>
    <h1>Manage Posts</h1>

    <a th:href="@{/admin/posts/new}">+ Create New Post</a>

    <table border="1" cellpadding="10">
      <thead>
        <tr>
          <th>ID</th>
          <th>Title</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
        <tr th:each="post : ${posts}">
          <td th:text="${post.id}"></td>
          <td th:text="${post.title}"></td>
          <td>
            <a th:href="@{/admin/posts/edit/{id}(id=${post.id})}">Edit</a> |
            <a
              th:href="@{/admin/posts/delete/{id}(id=${post.id})}"
              onclick="return confirm('Are you sure you want to delete this post?');"
              >Delete</a
            >
          </td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

4. admin/create_post.html (Admin – create post)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Create Post</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" />
  </head>
  <body>
    <h1>Create New Post</h1>

    <form th:action="@{/admin/posts}" th:object="${post}" method="post">
      <label>Title:</label><br />
      <input type="text" th:field="*{title}" required /><br /><br />

      <label>Content:</label><br />
      <textarea th:field="*{content}" rows="6" cols="40" required></textarea
      ><br /><br />

      <button type="submit">Save</button>
    </form>

    <a th:href="@{/admin/posts}">← Back</a>
  </body>
</html>

5. admin/edit_post.html (Admin – edit post)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>Edit Post</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" />
  </head>
  <body>
    <h1>Edit Post</h1>

    <form
      th:action="@{/admin/posts/{id}(id=${post.id})}"
      th:object="${post}"
      method="post"
    >
      <label>Title:</label><br />
      <input type="text" th:field="*{title}" required /><br /><br />

      <label>Content:</label><br />
      <textarea th:field="*{content}" rows="6" cols="40" required></textarea
      ><br /><br />

      <button type="submit">Update</button>
    </form>

    <a th:href="@{/admin/posts}">← Back</a>
  </body>
</html>

✅ With these templates in place, the application will have both public-facing blog pages and an admin dashboard to manage posts.


Styling and Layout with Thymeleaf Fragments

We’ll use a base layout and fragments to make the views modular and maintainable.

1. Create fragments/header.html

<!-- src/main/resources/templates/fragments/header.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title th:text="${pageTitle}">My Blog</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" />
  </head>
  <body>
    <header>
      <nav>
        <a th:href="@{/}">Home</a>
        <a th:href="@{/admin/posts}">Admin</a>
      </nav>
    </header>
    <main></main>
  </body>
</html>

2. Create fragments/footer.html

<!-- src/main/resources/templates/fragments/footer.html -->
</main>
<footer>
    <p>&copy; 2025 My Blog App - Powered by Spring Boot & Thymeleaf</p>
</footer>
</body>
</html>

3. Update Views to Use Fragments

Example: index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>My Blog</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" />
  </head>
  <body>
    <!-- Header fragment -->
    <div th:replace="fragments/header :: header"></div>

    <main>
      <h1>Latest Posts</h1>
      <ul>
        <li th:each="post : ${posts}">
          <a th:href="@{'/post/' + ${post.id}}" th:text="${post.title}"></a>
        </li>
      </ul>
    </main>

    <!-- Footer fragment -->
    <div th:replace="fragments/footer :: footer"></div>
  </body>
</html>

Example: post.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <title>My Blog</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" />
  </head>
  <body>
    <!-- Header fragment -->
    <div th:replace="fragments/header :: header"></div>

    <main>
      <article>
        <h1 th:text="${post.title}"></h1>
        <p th:text="${post.content}"></p>
      </article>
    </main>

    <!-- Footer fragment -->
    <div th:replace="fragments/footer :: footer"></div>
  </body>
</html>

4. Add CSS Stylesheet

Create a CSS file:

📂 src/main/resources/static/css/style.css

body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
  background: #fafafa;
  color: #333;
}

header {
  background: #2c3e50;
  color: #fff;
  padding: 1rem;
}

header nav a {
  color: #fff;
  margin-right: 1rem;
  text-decoration: none;
}

header nav a:hover {
  text-decoration: underline;
}

main {
  padding: 2rem;
}

h1 {
  color: #2c3e50;
}

ul {
  list-style-type: none;
  padding: 0;
}

ul li {
  margin: 0.5rem 0;
}

footer {
  background: #2c3e50;
  color: #fff;
  text-align: center;
  padding: 1rem;
  margin-top: 2rem;
}

✅ You can apply the same way to the Admin pages. Now all views share the same layout, and you only need to maintain one header and footer.


Run and Preview

Run the Spring Boot application

mvn clean spring-boot:run

admin/new

Create a Blog Application Using Spring Boot and Thymeleaf - admin new

admin/posts

Create a Blog Application Using Spring Boot and Thymeleaf - admin posts

home

Create a Blog Application Using Spring Boot and Thymeleaf - home

post

Create a Blog Application Using Spring Boot and Thymeleaf - post


Conclusion

In this tutorial, we built a simple Blog Application with Spring Boot 3, Thymeleaf, Hibernate, and MySQL. You learned how to:

  • Set up a Spring Boot project with Maven and configure it for Java 21.

  • Create models (Post, Category) and connect them with Hibernate JPA.

  • Implement a service layer to handle business logic.

  • Build controllers for both public pages and an admin dashboard.

  • Use Thymeleaf templates to render dynamic content.

  • Apply fragments for reusable header and footer sections.

  • Add custom styling with CSS for a consistent layout.

This setup gives you a strong foundation for a more feature-rich blog system. From here, you can extend the application with:

  • User authentication and roles (Spring Security) for multiple authors/admins.

  • Rich text editors (e.g., TinyMCE, Quill) for writing posts.

  • Image upload support for featured images.

  • Search and pagination for better content navigation.

  • REST APIs if you want to expose your blog content to mobile apps or frontend frameworks like React, Angular, or Vue.

With Spring Boot’s flexibility and Thymeleaf’s simplicity, you now have all the essentials to build and scale your own blogging platform. 🚀

You can get the full source code on our GitHub.

That's just the basics. If you need more in-depth learning about Java and Spring Framework, you can take the following cheap course:

Thanks!