📝 SpringBoot Blog Project – Introduction
The SpringBoot Blog Project is a dynamic, full-stack web application designed to facilitate user engagement through rich content creation, publication, and management. Built with a robust Java-based backend using the Spring Boot framework, this blog system delivers high performance and modular scalability. It includes both an admin interface for managing posts, categories, and comments, and a frontend interface for public readers to browse articles with aesthetic layout and responsiveness.The project is integrated with MySQL for persistent storage of posts, users, and categories, and uses Thymeleaf as the template engine for seamless rendering of dynamic HTML content. On the frontend, HTML5, CSS3, and JavaScript ensure a modern and user-friendly UI with responsive design. This architecture is ideal for learning and implementing MVC (Model-View-Controller) concepts, layered architecture, RESTful APIs, and secure form handling.In summary, this blog system serves as a perfect foundation for developers looking to build real-world web applications using Spring Boot and modern web technologies. It’s ideal for students, hobbyists, and professionals aiming to showcase their full-stack Java development skills.
🧱 Project Folder Structure & Technology Stack
✅ Technologies Used
Frontend: HTML5, CSS3, JavaScript
Backend: Java, Spring Boot, Thymeleaf
Database: MySQL
Template Engine: Thymeleaf
company name : acesoftech
Project Name: Blog
📁 Project Structure Overview
E:\JAVA\SPRINGBOOT\BLOG\SRC ├───main │ ├───java │ │ └───com │ │ └───acesoftech │ │ └───blog │ │ │ SpringbootBlogApplication.java │ │ │ │ │ ├───config │ │ │ SecurityConfig.java │ │ │ │ │ ├───controller │ │ │ ├───admin │ │ │ │ AdminCategoryController.java │ │ │ │ AdminCommentController.java │ │ │ │ AdminDashboardController.java │ │ │ │ AdminLoginController.java │ │ │ │ AdminPostController.java │ │ │ │ │ │ │ └───frontend │ │ │ FrontendArticleController.java │ │ │ FrontendCategoryController.java │ │ │ FrontendHomeController.java │ │ │ FrontendInfoController.java │ │ │ │ │ ├───dto │ │ │ CategoryDTO.java │ │ │ PostDTO.java │ │ │ │ │ ├───entity │ │ │ Category.java │ │ │ Comment.java │ │ │ Post.java │ │ │ User.java │ │ │ │ │ ├───repository │ │ │ CategoryRepository.java │ │ │ CommentRepository.java │ │ │ PostRepository.java │ │ │ UserRepository.java │ │ │ │ │ └───service │ │ └───impl │ └───resources │ │ application.properties │ │ application.yml │ │ │ ├───static │ │ │ │ │ └───uploads │ │ 2bf1b56c-1a69-439a-8242-b7da38848a1a_f4.png │ │ │ └───templates │ ├───admin │ │ │ categories.html │ │ │ category-form.html │ │ │ comments.html │ │ │ dashboard.html │ │ │ layout.html │ │ │ login.html │ │ │ post-form.html │ │ │ posts.html │ │ │ │ │ └───fragments │ │ footer.html │ │ header.html │ │ │ └───front │ │ about.html │ │ contact.html │ │ index.html │ │ layout.html │ │ post-list-by-category.html │ │ post-view.html │ │ services.html │ │ │ └───fragments │ footer.html │ header.html │ sidebar.html │ └───test └───java └───com └───acesoftech └───blog BlogApplicationTests.java
application.properties
spring.application.name=acesoftech-blog server.port=8080 # Database Configuration spring.datasource.url=jdbc:mysql://localhost:3308/blogdb spring.datasource.username=myadmin spring.datasource.password=Angular13 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # File Upload Limits spring.servlet.multipart.max-file-size=64MB spring.servlet.multipart.max-request-size=64MB # JPA Configuration spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # Spring Security Default User (for basic auth) spring.security.user.name=admin spring.security.user.password=admin123
🧱 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.2.5</version> <!-- ✅ stable version --> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.acesoftech</groupId> <artifactId>blog</artifactId> <version>0.0.1-SNAPSHOT</version> <name>blog</name> <description>Demo project for Spring Boot</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-core</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </pluginRepository> </pluginRepositories> </project>
// src/main/java/com/acesoftech/blog/SpringbootBlogApplication.java
package com.acesoftech.blog; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringbootBlogApplication { public static void main(String[] args) { SpringApplication.run(SpringbootBlogApplication.class, args); } }
// src/main/java/com/acesoftech/blog/config/SecurityConfig.java
package com.acesoftech.blog.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // Define in-memory user with username and password here @Bean public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { UserDetails user = User.withUsername("admin") .password(passwordEncoder.encode("admin123")) .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(user); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeHttpRequests(auth -> auth .requestMatchers("/admin/**").authenticated() .anyRequest().permitAll() ) .formLogin(form -> form .loginPage("/admin/login") .defaultSuccessUrl("/admin/dashboard", true) .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/admin/login") .permitAll() ); return http.build(); } }
FRONT-END CONTROLLER
// src/main/java/com/acesoftech/blog/controller/admin/AdminCategoryController.java
package com.acesoftech.blog.controller.admin; import com.acesoftech.blog.entity.Category; import com.acesoftech.blog.repository.CategoryRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; @Controller @RequestMapping("/admin/categories") public class AdminCategoryController { @Autowired private CategoryRepository categoryRepository; // List all categories @GetMapping public String getAllCategories(Model model) { List<Category> categories = categoryRepository.findAll(); model.addAttribute("categories", categories); return "admin/categories"; } // Show form to add new category @GetMapping("/new") public String showAddForm(Model model) { model.addAttribute("category", new Category()); model.addAttribute("categories", categoryRepository.findAll()); // 👈 Add this return "admin/category-form"; } // Show form to edit category @GetMapping("/edit/{id}") public String showEditForm(@PathVariable Long id, Model model) { Category category = categoryRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id)); model.addAttribute("category", category); model.addAttribute("categories", categoryRepository.findAll()); // 👈 Add this return "admin/category-form"; } // Save category (add or update) @PostMapping("/save") public String saveCategory(@ModelAttribute Category category) { categoryRepository.save(category); return "redirect:/admin/categories"; } // Delete category @GetMapping("/delete/{id}") public String deleteCategory(@PathVariable Long id) { categoryRepository.deleteById(id); return "redirect:/admin/categories"; } }
// src/main/java/com/acesoftech/blog/controller/admin/AdminCommentController.java
package com.acesoftech.blog.controller.admin; import com.acesoftech.blog.entity.Comment; import com.acesoftech.blog.repository.CommentRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; @Controller @RequestMapping("/admin/comments") public class AdminCommentController { @Autowired private CommentRepository commentRepository; // Show list of all comments @GetMapping public String listComments(Model model) { List<Comment> comments = commentRepository.findAll(); model.addAttribute("comments", comments); return "admin/comments"; } // Approve comment (status = 1) @GetMapping("/approve/{id}") public String approveComment(@PathVariable Long id) { Comment comment = commentRepository.findById(id).orElse(null); if (comment != null) { comment.setStatus(1); commentRepository.save(comment); } return "redirect:/admin/comments"; } // Unapprove comment (status = 0) @GetMapping("/unapprove/{id}") public String unapproveComment(@PathVariable Long id) { Comment comment = commentRepository.findById(id).orElse(null); if (comment != null) { comment.setStatus(0); commentRepository.save(comment); } return "redirect:/admin/comments"; } // Delete comment @GetMapping("/delete/{id}") public String deleteComment(@PathVariable Long id) { commentRepository.deleteById(id); return "redirect:/admin/comments"; } }
// src/main/java/com/acesoftech/blog/controller/admin/AdminDashboardController.java
package com.acesoftech.blog.controller.admin; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/admin/dashboard") public class AdminDashboardController { @GetMapping public String dashboard(Model model) { model.addAttribute("pageTitle", "Admin Dashboard"); return "admin/dashboard"; // ✅ renders templates/admin/dashboard.html } }
// src/main/java/com/acesoftech/blog/controller/admin/AdminLoginController.java
package com.acesoftech.blog.controller.admin; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/admin") public class AdminLoginController { @GetMapping("/login") public String showLoginForm() { return "admin/login"; // Points to templates/admin/login.html } }
// src/main/java/com/acesoftech/blog/controller/admin/AdminPostController.java
package com.acesoftech.blog.controller.admin; import com.acesoftech.blog.entity.Category; import com.acesoftech.blog.entity.Post; import com.acesoftech.blog.repository.CategoryRepository; import com.acesoftech.blog.repository.PostRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; import java.util.UUID; import java.nio.file.Path; @Controller @RequestMapping("/admin/posts") public class AdminPostController { @Autowired private PostRepository postRepository; @Autowired private CategoryRepository categoryRepository; // ✅ Show all posts @GetMapping public String getAllPosts(Model model) { List<Post> posts = postRepository.findAll(); model.addAttribute("posts", posts); return "admin/posts"; } // ✅ Show form to add new post @GetMapping("/new") public String showAddForm(Model model) { model.addAttribute("post", new Post()); model.addAttribute("categories", categoryRepository.findAll()); return "admin/post-form"; } // ✅ Show form to edit existing post @GetMapping("/edit/{id}") public String showEditForm(@PathVariable Long id, Model model) { Post post = postRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("Invalid post ID: " + id)); model.addAttribute("post", post); model.addAttribute("categories", categoryRepository.findAll()); return "admin/post-form"; } // ✅ Save post (create/update) @PostMapping("/save") public String savePost(@ModelAttribute Post post, @RequestParam("imageFile") MultipartFile imageFile) throws IOException { if (!imageFile.isEmpty()) { String fileName = UUID.randomUUID() + "_" + imageFile.getOriginalFilename(); Path uploadPath = Paths.get("src/main/resources/static/uploads"); if (!Files.exists(uploadPath)) { Files.createDirectories(uploadPath); } Path filePath = uploadPath.resolve(fileName); Files.copy(imageFile.getInputStream(), filePath); post.setImageName(fileName); } postRepository.save(post); return "redirect:/admin/posts"; } // ✅ Delete post @GetMapping("/delete/{id}") public String deletePost(@PathVariable Long id) { postRepository.deleteById(id); return "redirect:/admin/posts"; } }
FRONT-END CONTROLELRS
// src/main/java/com/acesoftech/blog/controller/frontend/FrontendArticleController.java
package com.acesoftech.blog.controller.frontend; import com.acesoftech.blog.entity.Category; import com.acesoftech.blog.entity.Comment; import com.acesoftech.blog.entity.Post; import com.acesoftech.blog.repository.CategoryRepository; import com.acesoftech.blog.repository.CommentRepository; import com.acesoftech.blog.repository.PostRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.List; @Controller @RequestMapping("/post") public class FrontendArticleController { @Autowired private PostRepository postRepository; @Autowired private CommentRepository commentRepository; @Autowired private CategoryRepository categoryRepository; // ✅ Inject category repo @GetMapping("/{slug}") public String viewPost(@PathVariable String slug, Model model) { Post post = postRepository.findBySlug(slug); if (post == null) { return "redirect:/"; } // ✅ Only approved comments List<Comment> approvedComments = commentRepository.findByPostAndStatus(post, 1); post.setComments(approvedComments); // ✅ Get all categories for sidebar List<Category> categories = categoryRepository.findAll(); model.addAttribute("post", post); model.addAttribute("categories", categories); // ✅ Add to model return "front/post-view"; } @PostMapping("/{slug}/comment") public String submitComment(@PathVariable String slug, @RequestParam Long postId, @RequestParam String fullName, @RequestParam String title, @RequestParam String description) { Post post = postRepository.findById(postId).orElse(null); if (post == null) { return "redirect:/"; } Comment comment = new Comment(); comment.setFullName(fullName); comment.setTitle(title); comment.setDescription(description); comment.setPost(post); comment.setCreatedAt(LocalDateTime.now()); comment.setStatus(0); // Default to pending commentRepository.save(comment); return "redirect:/post/" + slug; } }
// src/main/java/com/acesoftech/blog/controller/frontend/FrontendCategoryController.java
package com.acesoftech.blog.controller.frontend; import com.acesoftech.blog.entity.Category; import com.acesoftech.blog.entity.Post; import com.acesoftech.blog.repository.CategoryRepository; import com.acesoftech.blog.repository.PostRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; @Controller @RequestMapping("/category") public class FrontendCategoryController { @Autowired private CategoryRepository categoryRepository; @Autowired private PostRepository postRepository; @GetMapping("/{id}") public String viewCategory(@PathVariable Long id, Model model) { Category category = categoryRepository.findById(id).orElse(null); if (category == null) { return "redirect:/"; // fallback if invalid } List<Post> posts = postRepository.findByCategoryAndActiveTrueOrderByIdDesc(category); List<Category> categories = categoryRepository.findAll(); // ✅ ADD THIS model.addAttribute("category", category); model.addAttribute("posts", posts); model.addAttribute("categories", categories); // ✅ ADD THIS return "front/post-list-by-category"; } }
// src/main/java/com/acesoftech/blog/controller/frontend/FrontendHomeController.java
package com.acesoftech.blog.controller.frontend; import com.acesoftech.blog.entity.Category; import com.acesoftech.blog.entity.Post; import com.acesoftech.blog.repository.CategoryRepository; import com.acesoftech.blog.repository.PostRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import java.util.List; @Controller public class FrontendHomeController { @Autowired private PostRepository postRepository; @Autowired private CategoryRepository categoryRepository; @GetMapping("/") public String home(Model model) { // ✅ Fetch 3 featured posts List<Post> featuredPosts = postRepository.findTop3ByFeaturedTrueAndActiveTrueOrderByIdDesc(); // ✅ Fetch 9 latest posts List<Post> latestPosts = postRepository.findTop9ByActiveTrueOrderByIdDesc(); // ✅ All categories for sidebar List<Category> categories = categoryRepository.findAll(); // ✅ Add to model model.addAttribute("featuredPosts", featuredPosts); model.addAttribute("posts", latestPosts); // still use 'posts' for latest model.addAttribute("categories", categories); return "front/index"; } }
// src/main/java/com/acesoftech/blog/controller/frontend/FrontendInfoController.java
package com.acesoftech.blog.controller.frontend; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; @Controller public class FrontendInfoController { @GetMapping("/about") public String about(Model model) { model.addAttribute("pageTitle", "About Us"); return "front/about"; // loads templates/front/about.html } @GetMapping("/contact") public String contact(Model model) { model.addAttribute("pageTitle", "Contact"); return "front/contact"; // loads templates/front/contact.html } @GetMapping("/services") public String services(Model model) { model.addAttribute("pageTitle", "Our Services"); return "front/services"; // loads templates/front/services.html } @GetMapping("/search") public String search(@RequestParam String keyword, Model model) { model.addAttribute("pageTitle", "Search"); model.addAttribute("keyword", keyword); return "front/search"; // loads templates/front/search.html } }
DTO FILES
// src/main/java/com/acesoftech/blog/dto/CategoryDTO.java
// Put content here.
// src/main/java/com/acesoftech/blog/dto/PostDTO.java
// Put content here.
ENTITY FILES
// src/main/java/com/acesoftech/blog/entity/Category.java
package com.acesoftech.blog.entity; import jakarta.persistence.*; import java.util.List; @Entity public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String slug; private String description; private Long parentId; private boolean active; @OneToMany(mappedBy = "category") private List<Post> posts; // Getter and Setter for id public Long getId() { return id; } public void setId(Long id) { this.id = id; } // Getter and Setter for name public String getName() { return name; } public void setName(String name) { this.name = name; } // Getter and Setter for slug public String getSlug() { return slug; } public void setSlug(String slug) { this.slug = slug; } // Getter and Setter for description public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } // Getter and Setter for parentId public Long getParentId() { return parentId; } public void setParentId(Long parentId) { this.parentId = parentId; } // Getter and Setter for active public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } // Getter and Setter for posts public List<Post> getPosts() { return posts; } public void setPosts(List<Post> posts) { this.posts = posts; } }
// src/main/java/com/acesoftech/blog/entity/Comment.java
package com.acesoftech.blog.entity; import jakarta.persistence.*; import java.time.LocalDateTime; @Entity public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String fullName; private String title; @Column(columnDefinition = "TEXT") private String description; private LocalDateTime createdAt = LocalDateTime.now(); // ✅ NEW FIELD: Comment status (0 = off, 1 = approved) @Column(nullable = false, columnDefinition = "INT DEFAULT 0") private int status = 0; @ManyToOne @JoinColumn(name = "post_id", nullable = false) private Post post; // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getFullName() { return fullName; } public void setFullName(String fullName) { this.fullName = fullName; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public Post getPost() { return post; } public void setPost(Post post) { this.post = post; } }
// src/main/java/com/acesoftech/blog/entity/Post.java
package com.acesoftech.blog.entity; import jakarta.persistence.*; import java.time.LocalDateTime; import java.util.List; // ✅ Imported for List<Comment> @Entity public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String slug; @Column(columnDefinition = "TEXT") private String content; private boolean featured; private boolean active; private String imageName; private LocalDateTime createdAt = LocalDateTime.now(); @ManyToOne @JoinColumn(name = "category_id") private Category category; // ✅ ADDED: Relationship to Comment entity @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) private List<Comment> comments; // Getters and Setters... public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getSlug() { return slug; } public void setSlug(String slug) { this.slug = slug; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public boolean isFeatured() { return featured; } public void setFeatured(boolean featured) { this.featured = featured; } public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } public String getImageName() { return imageName; } public void setImageName(String imageName) { this.imageName = imageName; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public Category getCategory() { return category; } public void setCategory(Category category) { this.category = category; } // ✅ ADDED: Getter and Setter for comments public List<Comment> getComments() { return comments; } public void setComments(List<Comment> comments) { this.comments = comments; } }
// src/main/java/com/acesoftech/blog/entity/User.java
package com.acesoftech.blog.entity; import jakarta.persistence.*; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String role; // Getters and Setters }
REPOSITIORY FILES
// src/main/java/com/acesoftech/blog/repository/CategoryRepository.java
package com.acesoftech.blog.repository; import com.acesoftech.blog.entity.Category; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface CategoryRepository extends JpaRepository<Category, Long> { Category findBySlug(String slug); }
// src/main/java/com/acesoftech/blog/repository/CommentRepository.java
package com.acesoftech.blog.repository; import com.acesoftech.blog.entity.Comment; import com.acesoftech.blog.entity.Post; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface CommentRepository extends JpaRepository<Comment, Long> { List<Comment> findByPostId(Long postId); List<Comment> findByPostAndStatus(Post post, int i); }
// src/main/java/com/acesoftech/blog/repository/PostRepository.java
package com.acesoftech.blog.repository; import com.acesoftech.blog.entity.Category; import com.acesoftech.blog.entity.Post; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface PostRepository extends JpaRepository<Post, Long> { Post findBySlug(String slug); List<Post> findByActiveTrueOrderByIdDesc(); List<Post> findByCategoryAndActiveTrueOrderByIdDesc(Category category); List<Post> findTop3ByFeaturedTrueAndActiveTrueOrderByIdDesc(); List<Post> findTop9ByActiveTrueOrderByIdDesc(); }
// src/main/java/com/acesoftech/blog/repository/UserRepository.java
package com.acesoftech.blog.repository; import com.acesoftech.blog.entity.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); }
📝 Template Files
<!– src/main/resources/templates/admin/layout.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:fragment="layout"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title th:text="${pageTitle} ?: 'Admin Dashboard'">Admin</title> <!-- ✅ Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- ✅ Font Awesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-8IhzHNd4zBhZXhfDBb+fDX2MyjTxLCPtGPl6SVFe0upgq+dUYzdc8JSn1tUCGURQuHBXJ7JG4aH9D9VqEvK0Kg==" crossorigin="anonymous" referrerpolicy="no-referrer"/> </head> <body> <!-- ✅ Admin Header --> <div th:replace="admin/fragments/header :: header"></div> <!-- ✅ Page Content --> <main class="container mt-4" th:insert="~{::content}" style="min-height: 550px;"></main> <!-- ✅ Admin Footer --> <div th:replace="admin/fragments/footer :: footer"></div> <!-- ✅ Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
<!– src/main/resources/templates/admin/categories.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout"> <body> <div th:fragment="content"> <div class="container py-4"> <h2 class="mb-4">Category List</h2> <div class="mb-3"> <a class="btn btn-success" th:href="@{/admin/categories/new}">+ Add New Category</a> </div> <table class="table table-bordered table-striped"> <thead class="table-dark"> <tr> <th>ID</th> <th>Name</th> <th>Slug</th> <th>Actions</th> </tr> </thead> <tbody> <tr th:each="cat : ${categories}"> <td th:text="${cat.id}"></td> <td th:text="${cat.name}"></td> <td th:text="${cat.slug}"></td> <td> <a th:href="@{/admin/categories/edit/{id}(id=${cat.id})}" class="btn btn-sm btn-primary me-1">Edit</a> <a th:href="@{/admin/categories/delete/{id}(id=${cat.id})}" onclick="return confirm('Are you sure you want to delete this category?')" class="btn btn-sm btn-danger">Delete</a> </td> </tr> </tbody> </table> </div> </div> </body> </html>
<!– src/main/resources/templates/admin/category-form.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout"> <body> <div th:fragment="content"> <div class="container py-4"> <h2 class="mb-4" th:text="${category.id != null} ? 'Edit Category' : 'Add New Category'"></h2> <form th:action="@{/admin/categories/save}" method="post" th:object="${category}" class="border p-4 rounded bg-light"> <input type="hidden" th:field="*{id}"/> <!-- Parent Category Dropdown --> <div class="mb-3"> <label for="parentId" class="form-label">Parent Category</label> <select id="parentId" class="form-select" th:field="*{parentId}"> <option value="">-- None --</option> <option th:each="c : ${categories}" th:value="${c.id}" th:text="${c.name}" th:selected="${c.id == category.parentId}"> </option> </select> </div> <div class="mb-3"> <label for="name" class="form-label">Name</label> <input id="name" type="text" class="form-control" th:field="*{name}" required /> </div> <div class="mb-3"> <label for="slug" class="form-label">Slug</label> <input id="slug" type="text" class="form-control" th:field="*{slug}" readonly /> </div> <div class="mb-3"> <label for="description" class="form-label">Description</label> <textarea id="description" class="form-control" th:field="*{description}" rows="4"></textarea> </div> <div class="form-check mb-3"> <input type="checkbox" class="form-check-input" id="active" th:field="*{active}" /> <label class="form-check-label" for="active">Active</label> </div> <button type="submit" class="btn btn-primary">Save</button> </form> </div> <!-- ✅ Auto-Slug Generator Script --> <script> document.addEventListener("DOMContentLoaded", function () { const nameInput = document.getElementById("name"); const slugInput = document.getElementById("slug"); nameInput.addEventListener("input", function () { const slug = nameInput.value .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); slugInput.value = slug; }); }); </script> </div> </body> </html>
<!– src/main/resources/templates/admin/comments.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout"> <body> <div th:fragment="content"> <div class="container py-4"> <h2 class="mb-4">Comment List</h2> <table class="table table-bordered table-striped"> <thead class="table-dark"> <tr> <th>ID</th> <th>Post Title</th> <th>Full Name</th> <th>Title</th> <th>Description</th> <th>Status</th> <th>Created At</th> <th>Actions</th> </tr> </thead> <tbody> <tr th:each="c : ${comments}"> <td th:text="${c.id}"></td> <td th:text="${c.post.title}"></td> <td th:text="${c.fullName}"></td> <td th:text="${c.title}"></td> <td th:text="${c.description}"></td> <td> <span th:text="${c.status == 1 ? 'Approved' : 'Pending'}" th:classappend="${c.status == 1 ? 'text-success' : 'text-warning'}"></span> </td> <td th:text="${#temporals.format(c.createdAt, 'dd MMM yyyy')}"></td> <td> <a th:href="@{/admin/comments/approve/{id}(id=${c.id})}" class="btn btn-sm btn-success me-1">Approve</a> <a th:href="@{/admin/comments/unapprove/{id}(id=${c.id})}" class="btn btn-sm btn-warning me-1">Unapprove</a> <a th:href="@{/admin/comments/delete/{id}(id=${c.id})}" onclick="return confirm('Are you sure you want to delete this comment?')" class="btn btn-sm btn-danger">Delete</a> </td> </tr> </tbody> </table> </div> </div> </body> </html>
<!– src/main/resources/templates/admin/dashboard.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout"> <body> <div th:fragment="content"> <h1>Welcome to the Admin Dashboard</h1> <p>This is your central control panel.</p> </div> </body> </html>
<!– src/main/resources/templates/admin/login.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title th:text="${pageTitle}">Admin Login</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body class="bg-light"> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-4"> <div class="card shadow"> <div class="card-body"> <h3 class="card-title text-center mb-4">Admin Login</h3> <form method="post" action="/admin/login"> <div class="mb-3"> <label class="form-label">Username</label> <input type="text" name="username" class="form-control" required> </div> <div class="mb-3"> <label class="form-label">Password</label> <input type="password" name="password" class="form-control" required> </div> <button type="submit" class="btn btn-dark w-100">Login</button> <div th:if="${param.error}" class="mt-3 alert alert-danger text-center"> Invalid credentials </div> </form> </div> </div> </div> </div> </div> </body> </html>
<!– src/main/resources/templates/admin/post-form.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout"> <body> <div th:fragment="content"> <div class="container py-4"> <h2 class="mb-4" th:text="${post.id != null} ? 'Edit Post' : 'Add New Post'"></h2> <form th:action="@{/admin/posts/save}" method="post" th:object="${post}" enctype="multipart/form-data" class="border p-4 rounded bg-light"> <input type="hidden" th:field="*{id}" /> <div class="mb-3"> <label for="title" class="form-label">Title</label> <input id="title" type="text" class="form-control" th:field="*{title}" required /> </div> <div class="mb-3"> <label for="slug" class="form-label">Slug</label> <input id="slug" type="text" class="form-control" th:field="*{slug}" readonly /> </div> <div class="mb-3"> <label for="content" class="form-label">Content</label> <textarea id="content" class="form-control" rows="5" th:field="*{content}" required></textarea> </div> <div class="mb-3"> <label for="category" class="form-label">Category</label> <select id="category" class="form-select" th:field="*{category.id}"> <option value="">-- Select Category --</option> <option th:each="cat : ${categories}" th:value="${cat.id}" th:text="${cat.name}" th:selected="${cat.id == post.category?.id}"> </option> </select> </div> <div class="mb-3"> <div th:if="${post.imageName != null}"> <img th:src="@{'/uploads/' + ${post.imageName}}" alt="Uploaded Image" class="img-thumbnail mb-2" width="150" height="100" style="object-fit: cover;" /> </div> <label for="imageFile" class="form-label">Image</label> <input type="file" id="imageFile" name="imageFile" class="form-control" /> </div> <div class="form-check form-switch mb-3"> <input type="checkbox" class="form-check-input" id="featured" th:field="*{featured}" /> <label class="form-check-label" for="featured">Featured</label> </div> <div class="form-check form-switch mb-3"> <input type="checkbox" class="form-check-input" id="active" th:field="*{active}" /> <label class="form-check-label" for="active">Active</label> </div> <button type="submit" class="btn btn-primary">Save Post</button> </form> </div> <script> document.addEventListener("DOMContentLoaded", function () { const titleInput = document.getElementById("title"); const slugInput = document.getElementById("slug"); titleInput.addEventListener("input", function () { const slug = titleInput.value.toLowerCase().trim() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); slugInput.value = slug; }); }); </script> </div> <!-- ✅ Automatic Slug Generator --> </body> </html>
<!– src/main/resources/templates/admin/posts.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="admin/layout :: layout"> <body> <div th:fragment="content"> <div class="container py-4"> <h2 class="mb-4">Post List</h2> <div class="mb-3"> <a th:href="@{/admin/posts/new}" class="btn btn-success">+ Add New Post</a> </div> <table class="table table-striped table-bordered"> <thead class="table-dark"> <tr> <th>ID</th> <th>Title</th> <th>Slug</th> <th>Image</th> <th>Category</th> <th>Featured</th> <th>Active</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 th:text="${post.slug}"></td> <td> <img th:src="@{'/uploads/' + ${post.imageName}}" alt="Post Image" width="80" height="60" style="object-fit: cover;" /> </td> <td th:text="${post.category.name}"></td> <td> <span class="badge bg-warning text-dark" th:if="${post.featured}">Featured</span> <span class="badge bg-secondary" th:unless="${post.featured}">Normal</span> </td> <td> <span class="badge bg-success" th:if="${post.active}">Active</span> <span class="badge bg-danger" th:unless="${post.active}">Inactive</span> </td> <td> <a th:href="@{/admin/posts/edit/{id}(id=${post.id})}" class="btn btn-sm btn-primary me-1">Edit</a> <a th:href="@{/admin/posts/delete/{id}(id=${post.id})}" onclick="return confirm('Are you sure you want to delete this post?')" class="btn btn-sm btn-danger">Delete</a> </td> </tr> </tbody> </table> </div> </div> </body> </html>
<!– src/main/resources/templates/admin/fragments/footer.html –>
<!-- templates/admin/fragments/footer.html --> <div th:fragment="footer"> <footer class="text-center mt-5 py-4 text-white" style="background: linear-gradient(135deg, #002272, #182848); box-shadow: 0 -2px 10px rgba(0,0,0,0.1);"> <div class="container"> <small>© 2025 <strong>Admin Panel</strong>. All rights reserved.</small> </div> </footer> </div>
<!– src/main/resources/templates/admin/fragments/header.html –>
<!-- templates/admin/fragments/header.html --> <div th:fragment="header"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container-fluid"> <a class="navbar-brand d-flex align-items-center" th:href="@{/admin/dashboard}"> <img src="https://www.acesoftech.com/wp-content/themes/acesoftech/assets/img/logo-mini/home-new-logo.png" alt="Logo" style="height: 40px; margin-right: 10px;"> <span>Admin Panel</span> </a> <!-- Toggle button for mobile view --> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#adminNavbar" aria-controls="adminNavbar" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <!-- Navbar content --> <div class="collapse navbar-collapse" id="adminNavbar"> <ul class="navbar-nav me-auto"> <li class="nav-item"><a class="nav-link" th:href="@{/admin/categories}">Categories</a></li> <li class="nav-item"><a class="nav-link" th:href="@{/admin/posts}">Posts</a></li> <li class="nav-item"><a class="nav-link" th:href="@{/admin/comments}">Comments</a></li> </ul> <!-- Logout Button --> <form th:action="@{/logout}" method="post" class="d-flex"> <button type="submit" class="btn btn-outline-light btn-sm">Logout</button> </form> </div> </div> </nav> </div>
<!– src/main/resources/templates/front/about.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="front/layout :: layout"> <body> <div th:fragment="content"> <div class="container py-4"> <h1>About Us</h1> <p>Welcome to the blog. This page gives information about our mission and team.</p> </div> </div> </body> </html>
<!– src/main/resources/templates/front/contact.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="front/layout :: layout"> <body> <div th:fragment="content"> <div class="container py-5"> <h1 class="mb-4">Contact Us</h1> <form method="post" action="/send-message"> <div class="mb-3"> <label for="name" class="form-label">Your Name</label> <input type="text" id="name" name="name" class="form-control" required placeholder="John Doe"> </div> <div class="mb-3"> <label for="email" class="form-label">Your Email</label> <input type="email" id="email" name="email" class="form-control" required placeholder="you@example.com"> </div> <div class="mb-3"> <label for="subject" class="form-label">Subject</label> <input type="text" id="subject" name="subject" class="form-control" placeholder="Subject"> </div> <div class="mb-3"> <label for="message" class="form-label">Message</label> <textarea id="message" name="message" rows="5" class="form-control" required placeholder="Write your message here..."></textarea> </div> <button type="submit" class="btn btn-primary">Send Message</button> </form> </div> </div> </body> </html>
<!– src/main/resources/templates/front/index.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="front/layout :: layout"> <body> <div th:fragment="content"> <div class="container mt-4"> <div class="row"> <!-- ✅ Main Blog Section --> <div class="col-md-9"> <h2 class="mb-4">Latest Posts</h2> <div class="row" th:if="${posts.size() > 0}"> <div class="col-md-4 mb-4" th:each="post : ${posts}"> <div class="card h-100 shadow-sm"> <img th:if="${post.imageName != null}" th:src="@{'/uploads/' + ${post.imageName}}" class="card-img-top" alt="Post Image" style="height: 200px; object-fit: cover;"> <div class="card-body d-flex flex-column"> <h5 class="card-title" th:text="${post.title}">Post Title</h5> <p class="card-text" th:text="${#strings.abbreviate(post.content, 100)}">Content...</p> <a th:href="@{'/post/' + ${post.slug}}" class="btn btn-outline-primary mt-auto">Read More</a> </div> </div> </div> </div> <div class="alert alert-info" th:if="${posts.size() == 0}"> No blog posts available yet. </div> </div> <!-- ✅ Sidebar injected as fragment --> <div class="col-md-3"> <div th:replace="front/fragments/sidebar :: sidebar"></div> </div> </div> </div> </div> </body> </html>
<!– src/main/resources/templates/front/layout.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:fragment="layout"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title th:text="${pageTitle} ?: 'Home - Blog'">Blog</title> <!-- ✅ Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- ✅ Font Awesome CDN --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-8IhzHNd4zBhZXhfDBb+fDX2MyjTxLCPtGPl6SVFe0upgq+dUYzdc8JSn1tUCGURQuHBXJ7JG4aH9D9VqEvK0Kg==" crossorigin="anonymous" referrerpolicy="no-referrer"/> <!-- ✅ Optional: Custom CSS --> <style> @media (max-width: 768px) { nav { flex-direction: column; align-items: flex-start; padding: 10px; } nav .navbar-brand { margin-bottom: 10px; } nav ul { flex-direction: column; width: 100%; padding-left: 0; } nav ul li { width: 100%; padding: 8px 0; } nav ul li a { display: block; width: 100%; } } </style> </head> <body> <!-- ✅ Header --> <div th:replace="front/fragments/header :: header"></div> <!-- ✅ Dynamic Page Content --> <div th:insert="~{::content}" style="min-height: 600px;"></div> <!-- ✅ Footer --> <div th:replace="front/fragments/footer :: footer"></div> <!-- ✅ Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
<!– src/main/resources/templates/front/post-list-by-category.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title th:text="${category.name} + ' - Category Posts'">Category</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" integrity="sha512-8IhzHNd4zBhZXhfDBb+fDX2MyjTxLCPtGPl6SVFe0upgq+dUYzdc8JSn1tUCGURQuHBXJ7JG4aH9D9VqEvK0Kg==" crossorigin="anonymous" referrerpolicy="no-referrer"/> </head> <body> <!-- ✅ Shared Header --> <div th:replace="front/fragments/header :: header"></div> <div class="container mt-4"> <div class="row"> <!-- ✅ Left Section: Posts in this Category --> <div class="col-md-9"> <h2 th:text="'Posts in Category: ' + ${category.name}" class="mb-4"></h2> <div class="row" th:if="${posts.size() > 0}"> <div class="col-md-4 mb-4" th:each="post : ${posts}"> <div class="card h-100 shadow-sm"> <img th:if="${post.imageName != null}" th:src="@{'/uploads/' + ${post.imageName}}" class="card-img-top" alt="Post Image" style="height: 200px; object-fit: cover;"> <div class="card-body d-flex flex-column"> <h5 th:text="${post.title}" class="card-title">Post Title</h5> <p th:text="${#strings.abbreviate(post.content, 100)}" class="card-text"></p> <a th:href="@{'/post/' + ${post.slug}}" class="btn btn-outline-primary mt-auto">Read More</a> </div> </div> </div> </div> <!-- Fallback: No posts in category --> <div class="alert alert-info" th:if="${posts.size() == 0}"> No posts found in this category. </div> </div> <!-- ✅ Right Section: Sidebar --> <div class="col-md-3"> <div th:replace="front/fragments/sidebar :: sidebar"></div> </div> </div> </div> <!-- ✅ Shared Footer --> <div th:replace="front/fragments/footer :: footer"></div> <!-- ✅ Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
<!– src/main/resources/templates/front/post-view.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title th:text="${post.title}">Post Title</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" integrity="sha512-8IhzHNd4zBhZXhfDBb+fDX2MyjTxLCPtGPl6SVFe0upgq+dUYzdc8JSn1tUCGURQuHBXJ7JG4aH9D9VqEvK0Kg==" crossorigin="anonymous" referrerpolicy="no-referrer"/> </head> <body> <!-- ✅ Shared Header --> <div th:replace="front/fragments/header :: header"></div> <div class="container mt-4"> <div class="row"> <!-- ✅ Main Post Content --> <div class="col-md-9"> <h1 th:text="${post.title}">Post Title</h1> <p class="text-muted mb-3"> <i class="fas fa-calendar-alt me-1"></i> <span th:text="'Published on: ' + ${#temporals.format(post.createdAt, 'dd MMM yyyy')}"></span> </p> <img th:if="${post.imageName != null}" th:src="@{'/uploads/' + ${post.imageName}}" class="img-fluid mb-4 rounded" alt="Post Image" /> <div th:if="${post.content != null}" th:utext="${post.content}" class="mb-5"></div> <!-- ✅ Comments Section --> <h3 class="mb-3"><i class="fas fa-comments me-2"></i>Comments</h3> <div th:if="${post.comments.size() > 0}"> <div class="mb-4" th:each="comment : ${post.comments}"> <div class="border rounded p-3 mb-3 bg-light"> <h5 class="mb-1" th:text="${comment.title}">Comment Title</h5> <small class="text-muted" th:text="'By ' + ${comment.fullName} + ' on ' + ${#temporals.format(comment.createdAt, 'dd MMM yyyy')}"></small> <p class="mt-2 mb-0" th:text="${comment.description}"></p> </div> </div> </div> <div th:if="${post.comments.size() == 0}" class="alert alert-info"> <i class="fas fa-info-circle"></i> No comments yet. Be the first to comment! </div> <hr class="my-5"> <!-- ✅ Comment Submission Form --> <h4 class="mb-3"><i class="fas fa-pen"></i> Leave a Comment</h4> <form th:action="@{'/post/' + ${post.slug} + '/comment'}" method="post"> <input type="hidden" name="postId" th:value="${post.id}" /> <div class="mb-3"> <label for="fullName" class="form-label">Full Name</label> <input type="text" class="form-control" id="fullName" name="fullName" required> </div> <div class="mb-3"> <label for="title" class="form-label">Comment Title</label> <input type="text" class="form-control" id="title" name="title" required> </div> <div class="mb-3"> <label for="description" class="form-label">Comment</label> <textarea class="form-control" id="description" name="description" rows="4" required></textarea> </div> <button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i> Submit Comment</button> </form> <a href="/" class="btn btn-outline-secondary mt-4"><i class="fas fa-arrow-left"></i> Back to Home</a> </div> <!-- ✅ Sidebar --> <div class="col-md-3"> <div th:replace="front/fragments/sidebar :: sidebar"></div> </div> </div> </div> <!-- ✅ Shared Footer --> <div th:replace="front/fragments/footer :: footer"></div> <!-- ✅ JS Bundle --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
<!– src/main/resources/templates/front/services.html –>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" th:replace="front/layout :: layout"> <body> <div th:fragment="content"> <div class="container py-5"> <h1 class="mb-4">Our Services</h1> <div class="row"> <div class="col-md-4 mb-4"> <div class="card h-100 shadow-sm"> <div class="card-body"> <h5 class="card-title"><i class="fas fa-code me-2 text-primary"></i> Web Development</h5> <p class="card-text">Custom web applications using modern technologies like Spring Boot, React, and more.</p> </div> </div> </div> <div class="col-md-4 mb-4"> <div class="card h-100 shadow-sm"> <div class="card-body"> <h5 class="card-title"><i class="fas fa-mobile-alt me-2 text-success"></i> Mobile Apps</h5> <p class="card-text">Cross-platform Android/iOS mobile app development with performance and sleek UI.</p> </div> </div> </div> <div class="col-md-4 mb-4"> <div class="card h-100 shadow-sm"> <div class="card-body"> <h5 class="card-title"><i class="fas fa-search me-2 text-warning"></i> SEO & Marketing</h5> <p class="card-text">Improve your website's visibility with our expert SEO and digital marketing strategies.</p> </div> </div> </div> </div> </div> </div> </body> </html>
<!– src/main/resources/templates/front/fragments/footer.html –>
<!-- templates/front/fragments/footer.html --> <div th:fragment="footer"> <footer class="bg-dark text-white text-center py-3 mt-5"> <div class="container"> <p class="mb-0">© <span th:text="${#dates.format(#dates.createNow(), 'yyyy')}">2025</span> Acesoftech Academy. All rights reserved.</p> <p class="mb-0 small">Developed by <strong>Acesoftech Academy Trainers</strong></p> </div> </footer> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </div>
<!– src/main/resources/templates/front/fragments/header.html –>
<!-- templates/front/fragments/header.html --> <header th:fragment="header" style="background-color: black;"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark container"> <a class="navbar-brand d-flex align-items-center" th:href="@{/}"> <img src="https://www.acesoftech.com/wp-content/themes/acesoftech/assets/img/logo-mini/home-new-logo.png" alt="Logo" style="height: 40px; margin-right: 10px;"> <span>My Blog</span> </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav ms-auto"> <li class="nav-item"><a class="nav-link" th:href="@{/}">Home</a></li> <li class="nav-item"><a class="nav-link" th:href="@{/about}">About</a></li> <li class="nav-item"><a class="nav-link" th:href="@{/services}">Services</a></li> <li class="nav-item"><a class="nav-link" th:href="@{/contact}">Contact</a></li> </ul> </div> </nav> </header>
<!– src/main/resources/templates/front/fragments/sidebar.html –>
<!-- templates/front/fragments/sidebar.html --> <div th:fragment="sidebar"> <div class="card shadow-sm mb-4"> <div class="card-header bg-primary text-white"> <i class="fas fa-folder-open"></i> Categories </div> <ul class="list-group list-group-flush"> <li class="list-group-item" th:each="category : ${categories}"> <i class="fas fa-folder text-secondary me-2"></i> <a th:href="@{'/category/' + ${category.id}}" th:text="${category.name}"></a> </li> </ul> </div> <!-- Optional: Future widget section --> <!-- <div class="card shadow-sm mb-4"> <div class="card-header bg-success text-white"> 🔥 Popular Posts </div> <ul class="list-group list-group-flush"> <li class="list-group-item">Post A</li> <li class="list-group-item">Post B</li> </ul> </div> --> </div>