From 58fa0143415598842589ac31e6ff231a61f2d22b Mon Sep 17 00:00:00 2001 From: Caleb Braaten Date: Fri, 9 Jan 2026 00:13:23 -0800 Subject: [PATCH] Add next/prev links to bottom of blog posts --- index.tsx | 12 +++++- src/db/posts.ts | 46 +++++++++++++++++++++++ src/frontend/pages/post.tsx | 25 +++++++++++++ src/frontend/styles.css | 74 +++++++++++++++++++++++++++++++++++-- 4 files changed, 151 insertions(+), 6 deletions(-) diff --git a/index.tsx b/index.tsx index 1f5105b..02dbb97 100644 --- a/index.tsx +++ b/index.tsx @@ -6,7 +6,7 @@ import { Home } from "./src/frontend/pages/home"; import { NotFound } from "./src/frontend/pages/not-found"; import demo from "./temp/appshell.html"; import { Post } from "./src/frontend/pages/post"; -import { getPostWithTags } from "./src/db/index"; +import { getPostWithTags, getAdjacentPosts } from "./src/db/index"; async function blogPosts(hmr: boolean) { const glob = new Bun.Glob("**/*.md"); @@ -29,12 +29,17 @@ async function blogPosts(hmr: boolean) { headers: { "Content-Type": "text/html" }, }); + // Get adjacent posts for navigation + const { previousPost, nextPost } = getAdjacentPosts(post.path); + const data = { title: post.title, summary: post.summary, date: new Date(post.date), readingTime: post.reading_time, tags: post.tags || [], + previousPost, + nextPost, }; // AppShell is already loaded, just send the
content @@ -53,7 +58,10 @@ async function blogPosts(hmr: boolean) { return new Response( renderToString( - + , ), { diff --git a/src/db/posts.ts b/src/db/posts.ts index 8348aa3..6247a54 100644 --- a/src/db/posts.ts +++ b/src/db/posts.ts @@ -163,3 +163,49 @@ export function formatDate(dateString: string): string { day: 'numeric' }); } + +// Get adjacent posts (previous and next) chronologically for a given post +export function getAdjacentPosts(currentPostPath: string) { + const allPostsQuery = db.query(` + SELECT path, title, date + FROM posts + WHERE path NOT LIKE '%.md' + ORDER BY date ASC + `); + + const allPosts = allPostsQuery.all() as any[]; + + // Find the current post index + const currentIndex = allPosts.findIndex(post => post.path === currentPostPath); + + // If not found, return empty navigation + if (currentIndex === -1) { + return { previousPost: null, nextPost: null }; + } + + // Get previous post (newer date) + let previousPost = null; + if (currentIndex < allPosts.length - 1) { + const prevPost = allPosts[currentIndex + 1]; + // Clean up the path to match the URL structure + const cleanPath = prevPost.path.replace(/^.*\/content\//, '/').replace(/\.md$/, ''); + previousPost = { + title: prevPost.title, + path: cleanPath + }; + } + + // Get next post (older date) + let nextPost = null; + if (currentIndex > 0) { + const post = allPosts[currentIndex - 1]; + // Clean up the path to match the URL structure + const cleanPath = post.path.replace(/^.*\/content\//, '/').replace(/\.md$/, ''); + nextPost = { + title: post.title, + path: cleanPath + }; + } + + return { previousPost, nextPost }; +} diff --git a/src/frontend/pages/post.tsx b/src/frontend/pages/post.tsx index 8dc3ea1..d981cec 100644 --- a/src/frontend/pages/post.tsx +++ b/src/frontend/pages/post.tsx @@ -1,5 +1,10 @@ import React from 'react'; +interface NavigationPost { + title: string; + path: string; +} + interface PostProps { // HTML string for the blog post body children: string; @@ -7,10 +12,14 @@ interface PostProps { title: string; date: Date; readingTime: string; + previousPost?: NavigationPost | null; + nextPost?: NavigationPost | null; }; } export function Post({ children, meta }: PostProps) { + const { previousPost, nextPost } = meta; + return (
) diff --git a/src/frontend/styles.css b/src/frontend/styles.css index a48e366..341550e 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -914,6 +914,64 @@ h1 { margin: 0; } +.post-navigation { + margin-top: 48px; + padding-top: 24px; + border-top: 1px solid var(--border-color); +} + +.post-nav-links { + display: flex; + justify-content: space-between; + gap: 16px; +} + +.post-nav-link { + display: flex; + flex-direction: column; + padding: 12px 16px; + border-radius: 6px; + background-color: var(--sheet-background); + border: 1px solid var(--border-color); + text-decoration: none; + color: var(--text-primary); + transition: all 0.2s ease; + flex: 1; + max-width: 48%; +} + +.post-nav-link:hover { + background-color: var(--accent-color); + color: var(--accent-text-color); + transform: translateY(-2px); +} + +.prev-nav { + align-items: flex-start; +} + +.next-nav { + align-items: flex-end; + text-align: right; +} + +.nav-direction { + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 4px; + color: var(--accent-color); +} + +.post-nav-link:hover .nav-direction { + color: var(--accent-text-color); +} + +.nav-title { + font-size: 1rem; + font-weight: 500; + line-height: 1.4; +} + /* Responsive adjustments */ @media (max-width: 1200px) { aside { @@ -1025,16 +1083,24 @@ h1 { } .post-content h3 { - font-size: 20px; + font-size: 22px; + } + + .post-nav-link { + padding: 10px 12px; + font-size: 0.9rem; + } + + .nav-title { + font-size: 0.9rem; } .tag-pills { - gap: 6px; + display: flex; } .tag-pill { - font-size: 12px; - padding: 4px 10px; + padding: 6px 12px; } }