summaryrefslogtreecommitdiff
path: root/clams-server/src/main/java/no/eliashaugsbakk
diff options
context:
space:
mode:
authorElias Haugsbakk <[email protected]>2026-07-06 21:12:40 +0200
committerElias Haugsbakk <[email protected]>2026-07-06 21:12:40 +0200
commit463c202d80f57efb8e46bab4ee4ad7038444b25d (patch)
tree1b1675a02f57ad720a0ffbab5e99d80e2763d8f4 /clams-server/src/main/java/no/eliashaugsbakk
parent05f77926b147fe3a849eb1dbeeed2afcf97ad866 (diff)
implement post searching
Diffstat (limited to 'clams-server/src/main/java/no/eliashaugsbakk')
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java2
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java147
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostMetaData.java2
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java3
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java43
-rw-r--r--clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/BlogSearchService.java49
6 files changed, 185 insertions, 61 deletions
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java
index e456e7b..5ba7c31 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/Main.java
@@ -27,8 +27,8 @@ public class Main {
config.routes.get("/projects", ctx -> ctx.render("templates/projects.html",
Map.of("page_title", "Projects - Elias Haugsbakk", "page_css", "projects")));
+ config.routes.get("/blog", blogController::handleBlogRequest);
config.routes.get("/blog/{slug}", blogController::handleGetPost);
- config.routes.get("/blog", blogController::handleGetOverview);
config.events.serverStopped(dbManager::close);
}).start(7070);
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java
index 9111a0a..b82c3fe 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/controller/BlogController.java
@@ -3,86 +3,123 @@ package no.eliashaugsbakk.clams.server.controller;
import io.javalin.http.Context;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import no.eliashaugsbakk.clams.server.model.Post;
+import no.eliashaugsbakk.clams.server.model.PostMetaData;
import no.eliashaugsbakk.clams.server.repository.BlogPostRepo;
import no.eliashaugsbakk.clams.server.repository.BlogPostRepoSqlite;
import no.eliashaugsbakk.clams.server.repository.SqliteManager;
+import no.eliashaugsbakk.clams.server.service.BlogSearchService;
import no.eliashaugsbakk.clams.server.utils.MarkdownConverter;
public class BlogController {
+ private static final ZoneId OSLO_ZONE = ZoneId.of("Europe/Oslo");
+ private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MMM dd, yyyy");
+
private final BlogPostRepo blogPostRepo;
+ private final BlogSearchService blogSearchService;
+
+ public record SearchResultItem(String title, String slug, String summary, String formattedDate) {}
public BlogController(SqliteManager sqliteManager) {
this.blogPostRepo = new BlogPostRepoSqlite(sqliteManager);
+ this.blogSearchService = new BlogSearchService(blogPostRepo);
}
public void handleGetPost(Context ctx) {
- blogPostRepo.getPost(ctx.pathParam("slug")).ifPresentOrElse(post -> ctx.render(
- "templates/post.html",
- Map.of("page_title", post.title() + " -- Elias Haugsbakk",
- "page_css", "post",
- "title", post.title(),
- "published_date", post.timePublished().atZone(ZoneId.of("Europe/Oslo")).format(
- DateTimeFormatter.ofPattern("MMM dd, yyyy")),
- "author", "by Elias Haugsbakk",
- "content", MarkdownConverter.convertToHtml(post.content()),
- "back_to_index", "<a href=\"/blog/\">back to all posts</a>"
- )), () -> ctx.status(404));
+ var postOpt = blogPostRepo.getPost(ctx.pathParam("slug"));
+
+ if (postOpt.isEmpty()) {
+ ctx.status(404);
+ return;
+ }
+
+ Post post = postOpt.get();
+ String formattedDate = post.timePublished().atZone(OSLO_ZONE).format(DATE_FORMATTER);
+
+ ctx.render("templates/post.html", Map.of(
+ "page_title", post.title() + " -- Elias Haugsbakk",
+ "page_css", "post",
+ "title", post.title(),
+ "published_date", formattedDate,
+ "author", "by Elias Haugsbakk",
+ "content", MarkdownConverter.convertToHtml(post.content()),
+ "back_to_index", "<a href=\"/blog/\">back to all posts</a>"
+ ));
}
- public void handleGetOverview(Context ctx) {
- StringBuilder allPosts = new StringBuilder();
-
- int prevYear = blogPostRepo.listPostsByPublishedDesc().getFirst().published()
- .atZone(ZoneId.of("Europe/Oslo")).getYear();
-
- allPosts.append(String.format("""
- <details class="year-accordion" open>
- <summary class="year-toggle">%d</summary>
- <ul class="post-list">
- """, prevYear));
-
- for (var post : blogPostRepo.listPostsByPublishedDesc()) {
- int year = post.published().atZone(ZoneId.of("Europe/Oslo")).getYear();
- if (year == prevYear) {
- allPosts.append(String.format("""
- <li><a href="/blog/%s">%s</a></li>
- """, post.slug(), post.title()));
- } else {
- allPosts.append(String.format("""
- </ul>
- </details>
- <details class="year-accordion">
- <summary class="year-toggle">%d</summary>
- <ul class="post-list">
- <li><a href="/blog/%s">%s</a></li>
- """, year, post.slug(), post.title()));
- prevYear = year;
- }
+ public void handleBlogRequest(Context ctx) {
+ String searchTerm = ctx.queryParam("search");
+ if (searchTerm != null) {
+ handleSearch(ctx, searchTerm);
+ } else {
+ handleBlogIndex(ctx);
}
- allPosts.append("""
- </ul>
- </details>
- """);
+ }
+ private void handleBlogIndex(Context ctx) {
- var featuredSlugs = List.of("test-page", "another-one");
+ List<PostMetaData> allPosts = blogPostRepo.listPostsMetaData();
+ Map<Integer, List<PostMetaData>> postsByYear = groupPostsByYear(allPosts);
- StringBuilder featuredPosts = new StringBuilder();
- featuredPosts.append("<ul class=\"recent-list\">");
+ // Manually list featured posts
+ var featuredSlugs = List.of(
+ ""
+ );
+ List<Post> featuredPosts = new ArrayList<>();
for (String slug : featuredSlugs) {
- blogPostRepo.getPost(slug).ifPresent(post -> featuredPosts.append(String.format("""
- <li>
- <a href="/blog/%s">%s</a> <span class="featured-desc">-- %s</span>
- </li>
- """, post.slug(), post.title(), post.summary())));
+ blogPostRepo.getPost(slug).ifPresent(featuredPosts::add);
}
- featuredPosts.append("</ul>");
ctx.render("templates/blog.html",
- Map.of("page_title", "Elias Haugsbakk's Blog", "page_css", "blog", "all_posts",
- allPosts.toString(), "recent_posts", featuredPosts.toString()));
+ Map.of(
+ "page_title", "Elias Haugsbakk's Blog",
+ "page_css", "blog",
+ "posts_by_year", postsByYear,
+ "featured_posts", featuredPosts,
+ "search_value", ""
+ ));
+ }
+
+ private void handleSearch(Context ctx, String searchTerm) {
+ String query = searchTerm.trim();
+ List<PostMetaData> results = query.isEmpty() ? List.of() : blogSearchService.searchPosts(query);
+
+ List<SearchResultItem> resultItems = results.stream()
+ .map(post -> new SearchResultItem(
+ post.title(),
+ post.slug(),
+ post.summary() != null ? post.summary() : "",
+ post.timePublished().atZone(OSLO_ZONE).format(DATE_FORMATTER)
+ ))
+ .toList();
+
+ ctx.render("templates/blog-search.html",
+ Map.of(
+ "page_title", "Search Results -- Elias Haugsbakk",
+ "page_css", "blog",
+ "search_term", query,
+ "results", resultItems,
+ "results_count", resultItems.size()
+ ));
+ }
+
+ private Map<Integer, List<PostMetaData>> groupPostsByYear(List<PostMetaData> posts) {
+ Map<Integer, List<PostMetaData>> unsortedGroups = posts.stream()
+ .collect(Collectors.groupingBy(
+ post -> post.timePublished().atZone(OSLO_ZONE).getYear(),
+ Collectors.toList()
+ ));
+
+ Map<Integer, List<PostMetaData>> sortedGroups = new TreeMap<>(Comparator.reverseOrder());
+ sortedGroups.putAll(unsortedGroups);
+
+ return sortedGroups;
}
}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostMetaData.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostMetaData.java
index 98a1c49..44d667d 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostMetaData.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/model/PostMetaData.java
@@ -2,5 +2,5 @@ package no.eliashaugsbakk.clams.server.model;
import java.time.Instant;
-public record PostMetaData(String title, String slug, Instant published, Instant lastEdit) {
+public record PostMetaData(String title, String slug, String summary, Instant timePublished, Instant lastEdit) {
}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java
index d63e095..8e43e2c 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepo.java
@@ -6,6 +6,7 @@ import no.eliashaugsbakk.clams.server.model.Post;
import no.eliashaugsbakk.clams.server.model.PostMetaData;
public interface BlogPostRepo {
- List<PostMetaData> listPostsByPublishedDesc();
+ List<PostMetaData> listPostsMetaData();
Optional<Post> getPost(String slug);
+ List<PostMetaData> searchPostsBody(String query);
}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java
index 2979589..77a7407 100644
--- a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/repository/BlogPostRepoSqlite.java
@@ -19,9 +19,9 @@ public class BlogPostRepoSqlite implements BlogPostRepo {
}
@Override
- public List<PostMetaData> listPostsByPublishedDesc() {
+ public List<PostMetaData> listPostsMetaData() {
String sql = """
- SELECT title, slug, published, last_edited
+ SELECT title, slug, summary, published, last_edited
FROM posts
ORDER BY published DESC
""";
@@ -35,13 +35,14 @@ public class BlogPostRepoSqlite implements BlogPostRepo {
while (resultSet.next()) {
String title = resultSet.getString("title");
String slug = resultSet.getString("slug");
+ String summary = resultSet.getString("summary");
Instant published = Instant.parse(resultSet.getString("published"));
String lastEditRaw = resultSet.getString("last_edited");
Instant lastEdit = (lastEditRaw != null) ? Instant.parse(lastEditRaw) : null;
- posts.add(new PostMetaData(title, slug, published, lastEdit));
+ posts.add(new PostMetaData(title, slug, summary, published, lastEdit));
}
return posts;
@@ -83,4 +84,40 @@ public class BlogPostRepoSqlite implements BlogPostRepo {
throw new RepoException("Error fetching full post by slug: " + slug, e);
}
}
+
+ @Override
+ public List<PostMetaData> searchPostsBody(String query) {
+ String sql = """
+ SELECT title, slug, summary, published, last_edited
+ FROM posts
+ WHERE content LIKE ?
+ ORDER BY published DESC
+ """;
+
+ List<PostMetaData> posts = new ArrayList<>();
+
+ try (Connection conn = manager.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ String likeParam = "%" + query + "%";
+ stmt.setString(1, likeParam);
+
+ try (ResultSet resultSet = stmt.executeQuery()) {
+ while (resultSet.next()) {
+ String title = resultSet.getString("title");
+ String slug = resultSet.getString("slug");
+ String summary = resultSet.getString("summary");
+ Instant published = Instant.parse(resultSet.getString("published"));
+ Instant lastEdit = Instant.parse(resultSet.getString("last_edited"));
+
+ posts.add(new PostMetaData(title, slug, summary, published, lastEdit));
+ }
+ }
+
+ return posts;
+
+ } catch (SQLException e) {
+ throw new RepoException("Error searching posts for query: " + query, e);
+ }
+ }
}
diff --git a/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/BlogSearchService.java b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/BlogSearchService.java
new file mode 100644
index 0000000..26595f1
--- /dev/null
+++ b/clams-server/src/main/java/no/eliashaugsbakk/clams/server/service/BlogSearchService.java
@@ -0,0 +1,49 @@
+package no.eliashaugsbakk.clams.server.service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import no.eliashaugsbakk.clams.server.model.PostMetaData;
+import no.eliashaugsbakk.clams.server.repository.BlogPostRepo;
+
+public class BlogSearchService {
+ private final BlogPostRepo blogPostRepo;
+
+ public BlogSearchService(BlogPostRepo blogPostRepo) {
+ this.blogPostRepo = blogPostRepo;
+ }
+
+ public List<PostMetaData> searchPosts(String query) {
+ if (query == null || query.isBlank()) return List.of();
+
+ int titleValue = 10;
+ int summaryValue = 5;
+ int bodyValue = 1;
+
+ Map<PostMetaData, Integer> searchRanking = new HashMap<>();
+ String lowerQuery = query.toLowerCase();
+
+ List<PostMetaData> postsMetaData = blogPostRepo.listPostsMetaData();
+ List<PostMetaData> bodyResults = blogPostRepo.searchPostsBody(query);
+
+ postsMetaData.stream()
+ .filter(post -> post.title().toLowerCase().contains(lowerQuery))
+ .forEach(post -> searchRanking.merge(post, titleValue, Integer::sum));
+
+ postsMetaData.stream()
+ .filter(post -> post.summary() != null && post.summary().toLowerCase().contains(lowerQuery))
+ .forEach(post -> searchRanking.merge(post, summaryValue, Integer::sum));
+
+ bodyResults.forEach(post -> searchRanking.merge(post, bodyValue, Integer::sum));
+
+ for (var item : searchRanking.entrySet()) {
+ IO.println(item.getKey());
+ IO.println(item.getValue());
+ }
+
+ return searchRanking.entrySet().stream()
+ .sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue()))
+ .map(Map.Entry::getKey)
+ .toList();
+ }
+}