summaryrefslogtreecommitdiff
path: root/clams-server/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'clams-server/src/main')
-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
-rw-r--r--clams-server/src/main/resources/public/css/blog.css87
-rw-r--r--clams-server/src/main/resources/public/css/common.css60
-rw-r--r--clams-server/src/main/resources/public/css/home.css18
-rw-r--r--clams-server/src/main/resources/public/css/post.css4
-rw-r--r--clams-server/src/main/resources/public/css/projects.css18
-rw-r--r--clams-server/src/main/resources/templates/blog-search.html41
-rw-r--r--clams-server/src/main/resources/templates/blog.html25
-rw-r--r--clams-server/src/main/resources/templates/layout.html5
-rw-r--r--clams-server/src/main/resources/templates/post.html1
15 files changed, 406 insertions, 99 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();
+ }
+}
diff --git a/clams-server/src/main/resources/public/css/blog.css b/clams-server/src/main/resources/public/css/blog.css
index c0fd080..f323de4 100644
--- a/clams-server/src/main/resources/public/css/blog.css
+++ b/clams-server/src/main/resources/public/css/blog.css
@@ -1,3 +1,13 @@
+.content-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 60px;
+ margin-top: 30px;
+}
+
+.left-column { text-align: right; }
+.right-column { text-align: left; }
+
.intro-text {
text-align: center;
max-width: 600px;
@@ -41,17 +51,6 @@
background: #ddd;
}
-@media (max-width: 850px) {
- .search-form {
- position: static;
- text-align: center;
- margin-bottom: 40px;
- }
- .search-form input {
- width: 250px;
- }
-}
-
.year-accordion {
margin-bottom: 1.5rem;
cursor: pointer;
@@ -92,3 +91,69 @@
color: var(--text-color);
opacity: 0.85;
}
+
+.search-results-container {
+ max-width: 600px;
+ margin: 30px auto 0 auto;
+}
+
+.search-results-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+.search-result-item {
+ margin-bottom: 1.25rem;
+ font-size: 1rem;
+ line-height: 1.6;
+}
+
+.search-result-date {
+ display: inline-block;
+ font-size: 0.9rem;
+ color: var(--text-color);
+ opacity: 0.65;
+ margin-right: 15px;
+ min-width: 110px;
+}
+
+.search-result-item a {
+ font-size: 1rem;
+}
+
+.search-result-summary {
+ font-weight: normal;
+ color: var(--text-color);
+ opacity: 0.85;
+}
+
+.no-results {
+ padding: 20px;
+ background: #e8e8e4;
+ border-radius: 4px;
+ text-align: center;
+ font-style: italic;
+}
+
+.back-to-blog {
+ margin-top: 40px;
+ display: inline-block;
+}
+
+@media (max-width: 768px) {
+ .content-grid {
+ grid-template-columns: 1fr;
+ gap: 40px;
+ }
+ .left-column, .right-column { text-align: center; }
+
+ .search-form {
+ position: static;
+ text-align: center;
+ margin-bottom: 40px;
+ }
+ .search-form input {
+ width: 250px;
+ }
+}
diff --git a/clams-server/src/main/resources/public/css/common.css b/clams-server/src/main/resources/public/css/common.css
index ffed73f..f5f6d2a 100644
--- a/clams-server/src/main/resources/public/css/common.css
+++ b/clams-server/src/main/resources/public/css/common.css
@@ -1,10 +1,19 @@
* { box-sizing: border-box; }
:root {
- --bg-color: #efefeb;
- --text-color: #373838;
+ --bg-color: #f4f4f0;
+ --text-color: #363737;
--border-color: #ccc;
- --font-serif: "Times New Roman", serif;
+ --font-serif: "Spectral", Georgia, serif;
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+}
+
+h1, h2 {
+ font-family: var(--font-sans), serif;
+}
+
+html {
+ scrollbar-gutter: stable;
}
html, body {
@@ -43,20 +52,9 @@ h2 {
border-bottom: 1px solid var(--border-color);
margin-bottom: 20px;
padding-bottom: 5px;
- text-transform: uppercase;
letter-spacing: 1px;
}
-.content-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 60px;
- margin-top: 30px;
-}
-
-.left-column { text-align: right; }
-.right-column { text-align: left; }
-
.main-container ul {
list-style-type: none;
padding: 0;
@@ -106,13 +104,7 @@ footer p {
opacity: 0.5;
}
-@media (max-width: 600px) {
- .content-grid {
- grid-template-columns: 1fr;
- gap: 40px;
- }
- .left-column, .right-column { text-align: center; }
-
+@media (max-width: 768px) {
footer p {
flex-direction: column;
gap: 10px;
@@ -148,3 +140,29 @@ footer p {
scrollbar-width: thin;
scrollbar-color: #d1d1cd var(--bg-color);
}
+
+.back-link-top {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ font-size: 0.85rem;
+ text-decoration: none;
+ font-weight: bold;
+ opacity: 0.7;
+ transition: opacity 0.2s;
+}
+
+.back-link-top:hover {
+ opacity: 1;
+}
+
+@media (max-width: 768px) {
+ .back-link-top {
+ position: static;
+ display: block;
+ margin-top: 10px;
+ margin-bottom: 20px;
+ text-align: left;
+ }
+}
+
diff --git a/clams-server/src/main/resources/public/css/home.css b/clams-server/src/main/resources/public/css/home.css
index 2cf0bd8..742ed38 100644
--- a/clams-server/src/main/resources/public/css/home.css
+++ b/clams-server/src/main/resources/public/css/home.css
@@ -1,3 +1,13 @@
+.content-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 60px;
+ margin-top: 30px;
+}
+
+.left-column { text-align: right; }
+.right-column { text-align: left; }
+
.profile-img {
border: 3px solid var(--text-color);
border-radius: 20px;
@@ -16,7 +26,13 @@
word-wrap: break-word;
}
-@media (max-width: 600px) {
+@media (max-width: 768px) {
+ .content-grid {
+ grid-template-columns: 1fr;
+ gap: 40px;
+ }
+ .left-column, .right-column { text-align: center; }
+
.profile-img {
margin: 0 auto 20px auto;
}
diff --git a/clams-server/src/main/resources/public/css/post.css b/clams-server/src/main/resources/public/css/post.css
index 8c4b91d..6a237e1 100644
--- a/clams-server/src/main/resources/public/css/post.css
+++ b/clams-server/src/main/resources/public/css/post.css
@@ -1,6 +1,7 @@
.post-header {
display: flex;
justify-content: space-between;
+ margin-top: 45px;
margin-bottom: 40px;
font-size: 0.9rem;
opacity: 0.7;
@@ -39,8 +40,9 @@
border-top: 1px dashed var(--border-color);
}
-@media (max-width: 600px) {
+@media (max-width: 768px) {
.post-header {
+ margin-top: 10px;
flex-direction: column;
align-items: center;
gap: 10px;
diff --git a/clams-server/src/main/resources/public/css/projects.css b/clams-server/src/main/resources/public/css/projects.css
index 4886a6d..3e5ccc0 100644
--- a/clams-server/src/main/resources/public/css/projects.css
+++ b/clams-server/src/main/resources/public/css/projects.css
@@ -1,3 +1,13 @@
+.content-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 60px;
+ margin-top: 30px;
+}
+
+.left-column { text-align: right; }
+.right-column { text-align: left; }
+
.projects-category {
margin-bottom: 80px;
}
@@ -63,3 +73,11 @@
opacity: 0.6;
margin: 0 10px;
}
+
+@media (max-width: 768px) {
+ .content-grid {
+ grid-template-columns: 1fr;
+ gap: 40px;
+ }
+ .left-column, .right-column { text-align: center; }
+}
diff --git a/clams-server/src/main/resources/templates/blog-search.html b/clams-server/src/main/resources/templates/blog-search.html
new file mode 100644
index 0000000..e2188cf
--- /dev/null
+++ b/clams-server/src/main/resources/templates/blog-search.html
@@ -0,0 +1,41 @@
+{% extends "templates/layout.html" %}
+
+{% block content %}
+<main class="main-container">
+ <a href="/blog" class="back-link-top">&larr; back</a>
+ <header>
+ <h1>Elias Haugsbakk's Blog</h1>
+ <p class="intro-text">For search: "<strong>{{ search_term }}</strong>" -- Found {{ results_count }} matching posts</p>
+
+ <form action="/blog" method="GET" class="search-form">
+ <input type="text" name="search" value="{{ search_term }}" placeholder="Search posts..." aria-label="Search blog posts">
+ <button type="submit">Search</button>
+ </form>
+ </header>
+
+ <div class="search-results-container">
+ {% if results is empty %}
+ <div class="no-results">
+ {% if search_term is empty %}
+ <p>Please enter a search term to search the blog posts. Or <a href="/blog">go back to all posts</a>.</p>
+ {% else %}
+ <p>Found no posts matching <strong>"{{ search_term }}"</strong>. <br>
+ <a href="/blog">Go back to all posts</a>.</p>
+ {% endif %}
+ </div>
+ {% else %}
+ <ul class="search-results-list">
+ {% for post in results %}
+ <li class="search-result-item">
+ <span class="search-result-date">{{ post.formattedDate }}</span>
+ <a href="/blog/{{ post.slug }}">{{ post.title }}</a>
+ {% if post.summary is not empty %}
+ <span class="search-result-summary">-- {{ post.summary }}</span>
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ </div>
+</main>
+{% endblock %}
diff --git a/clams-server/src/main/resources/templates/blog.html b/clams-server/src/main/resources/templates/blog.html
index 200a144..2c484a1 100644
--- a/clams-server/src/main/resources/templates/blog.html
+++ b/clams-server/src/main/resources/templates/blog.html
@@ -4,10 +4,12 @@
<main class="main-container">
<header>
<h1>Elias Haugsbakk's Blog</h1>
- <p class="intro-text">This is the home page for my blog. The list of blog posts is a diverse collection of some of my writings; if you're just browsing, this might be a good place to start.</p>
+ <p class="intro-text">This is the home page for my blog. The featured list is a diverse collection of some
+ of my writings; if you're just browsing, this might be a good place to start.</p>
<form action="/blog" method="GET" class="search-form">
- <input type="text" name="search" value="{{ search_value }}" placeholder="Search posts..." aria-label="Search blog posts">
+ <input type="text" name="search" value="{{ search_value }}" placeholder="Search posts..."
+ aria-label="Search blog posts">
<button type="submit">Search</button>
</form>
</header>
@@ -16,14 +18,29 @@
<div class="left-column">
<section id="all" class="blog-section">
<h2>All Posts</h2>
- {{ all_posts| raw }}
+ {% for group in posts_by_year %}
+ <details class="year-accordion" {% if loop.first %}open{% endif %}>
+ <summary class="year-toggle">{{ group.key }}</summary>
+ <ul class="post-list">
+ {% for post in group.value %}
+ <li><a href="/blog/{{ post.slug }}">{{ post.title }}</a></li>
+ {% endfor %}
+ </ul>
+ </details>
+ {% endfor %}
</section>
</div>
<div class="right-column">
<section id="recent" class="blog-section">
<h2>Featured</h2>
- {{ recent_posts | raw }}
+ <ul class="recent-list">
+ {% for post in featured_posts %}
+ <li>
+ <a href="/blog/{{ post.slug }}">{{ post.title }}</a> <span class="featured-desc">-- {{ post.summary }}</span>
+ </li>
+ {% endfor %}
+ </ul>
</section>
</div>
</div>
diff --git a/clams-server/src/main/resources/templates/layout.html b/clams-server/src/main/resources/templates/layout.html
index 4c8b8dc..1460721 100644
--- a/clams-server/src/main/resources/templates/layout.html
+++ b/clams-server/src/main/resources/templates/layout.html
@@ -6,6 +6,11 @@
<title>{{ page_title }}</title>
<link rel="stylesheet" href="/css/common.css">
<link rel="stylesheet" href="/css/{{ page_css }}.css">
+
+ <!-- Google Fonts link for Spectral (Regular 400 and Italic) -->
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
</head>
<body>
diff --git a/clams-server/src/main/resources/templates/post.html b/clams-server/src/main/resources/templates/post.html
index 7e99e1f..7899099 100644
--- a/clams-server/src/main/resources/templates/post.html
+++ b/clams-server/src/main/resources/templates/post.html
@@ -2,6 +2,7 @@
{% block content %}
<main class="main-container">
+ <a href="/blog" class="back-link-top">&larr; back</a>
<header class="post-header">
<span class="post-date">{{ published_date }}</span>
<span class="post-author">{{ author }}</span>