DEV Community

Roshan Adhikari
Roshan Adhikari

Posted on • Edited on • Originally published at roshanadhikary.com.np

Build a Markdown-based Blog with Spring Boot - Part 5

Welcome to another instalment of the "Build a Markdown Blog with Spring Boot" series. You can find the previous parts right here: Part 1 | Part 2 | Part 3 | Part 4

Recap

Right before this part, we have finished building how the blog works -- we use Markdown files stored in resources/posts directory to populate our database and then fetch the posts on respective requests using our PostController class.

Now, we need to render the posts. For this, we use Thymeleaf and Bootstrap. We already have a Thymeleaf dependency specified in our POM.

Bootstrap WebJar

For Bootstrap, we just need to add its WebJar in the pom.xml file.

<!-- https://mvnrepository.com/artifact/org.webjars/bootstrap -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.3.1</version>
</dependency>
view raw pom.xml hosted with ❤ by GitHub

Again, load the Maven changes in your POM file. Now we're ready to use Bootstrap in our Thymeleaf templates.

Header for our templates

Inside the resources/templates directory, create a new template: header.html. This will be the header portion of our blog that will be present in every page.

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<!-- Link the Bootstrap Webjar -->
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
th:href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}"
>
</head>
<body>
<!-- Declare the following div as a header fragment -->
<div class="container" th:fragment="header" style="text-align:center;margin-top:10px;">
<p class="title">Personal blog by Roshan</p>
<p class="lead">
<a class="card-link" href="/posts" role="button">Home</a>
<a class="card-link" href="https://www.github.com/roshanadh" target="_blank" role="button">GitHub</a>
<a class="card-link" href="https://roshanadhikari.name.np" target="_blank" role="button">Portfolio</a>
<a class="card-link" href="https://www.linkedin.com/in/roshanadh/" target="_blank" role="button">LinkedIn</a>
</p>
<hr>
</div>
</body>
</html>
view raw header.html hosted with ❤ by GitHub

Notice how we specify both href and th:href attributes when linking Bootstrap. This is the reason why Thymeleaf is a natural template engine: Templates written in Thymeleaf can be rendered with or without the server running. When we run the server, Thymeleaf reads the th:href attribute and links Bootstrap using the WebJar resource. When the server isn't running, the browser uses the href attribute to link Bootstrap.

Creating a home page

Next, create a template, posts.html, that will serve as our home page.

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Posts | Roshan Adhikari</title>
<!-- Link Bootstrap WebJar -->
</head>
<body>
<!-- Insert the header frament -->
<div th:insert="header"></div>
<div class="container">
<!-- For each post -->
<div th:each="post : ${posts}" class="card">
<div class="card-body">
<h5 class="card-title" th:utext="${post.title}">Post title</h5>
<h6 class="card-subtitle mb-2"
th:text="${#temporals.monthName(post.dateTime) + ' ' + #temporals.day(post.dateTime) + ', ' + #temporals.year(post.dateTime)}">LocalDateTime</h6>
<p class="card-text" th:text="${post.synopsis} + '...'">Post synopsis</p>
<a href="#" th:href="'/posts/' + ${post.id}" th:text="${'Read more'}" class="card-link">Read more</a>
</div>
</div>
</div>
</body>
</html>
view raw posts.html hosted with ❤ by GitHub

Here, for each post retrieved from the database, a card element is created. We also use the #temporals utility provided by Thymeleaf to format our LocalDateTime instance so that we can render the month name, day, and year in the format: July 1, 2021.

You can additionally use a pagination element from Bootstrap to fetch paginated posts. (In this article, we will do it the manual way -- using request queries to specify the size and page.)

The one where we render a post

Now onto our post.html template. This will be the page that renders a single blog post in full.

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:if="${error}"> Error | Roshan Adhikari </title>
<title th:unless="${error}" th:text="${post.title + ' | Roshan Adhikari'}"> | Roshan Adhikari </title>
<!-- Link Bootstrap WebJar -->
</head>
<body>
<!-- Insert the header frament -->
<div th:insert="header"></div>
<div class="d-flex justify-content-center" style="margin: 10px;">
<!-- If no post by the given ID exists -->
<div th:if="${error}" class="alert alert-danger" role="alert">
Sorry, the post you requested doesn't exist!
</div>
<!-- If a post by the given ID exists -->
<article th:unless="${error}">
<h1 th:utext="${post.title}">Title</h1>
<p class="card-subtitle mb-2"
th:text="${#temporals.monthName(post.dateTime) + ' ' + #temporals.day(post.dateTime) + ', ' + #temporals.year(post.dateTime)}">LocalDateTime</p>
<p th:utext="${post.content}">Content</p>
</article>
</div>
</body>
</html>
view raw post.html hosted with ❤ by GitHub

In this template, we employ some error-handling mechanism. Go back to the PostController.getPostById method. Notice how if a post by a given ID doesn't exist, we add an error attribute with no-post value in the model.

In post.html if the model contains the error attribute, we change the title as well as the body to indicate an error. Otherwise, we render the respective blog post.

This is it for the coding portion of this series. To test if the pagination works, add some more Markdown files in the resources/posts directory, and fetch them from the website.

Running the application

Run the Spring Boot application and wait until the JVM starts running.

Visit localhost:8080 to see your most recent blog posts.

localhost:8080/posts renders a list of the most recent blog posts

To modify the pagination parameters, we can use query strings in our request. For instance, localhost:8080/posts?page=0&size=3 should render the three most recent blog posts.

Rendering three most recent blog posts using query strings

To render a blog post in full, we can use the ID of that post, or simply click the Read more hyperlink.

Rendering a full blog post

Code

We have successfully built the blog application that we set out to build. In the following article, we will see how we can deploy our application to Heroku, host our database on RemoteMySQL, and use Heroku's Automatic Deployment feature to update our blog whenever we add a new blog post.

This session's code has been pushed to the GitHub repository; for previous sessions, check here.

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay