Add next/prev links to bottom of blog posts

This commit is contained in:
Caleb Braaten 2026-01-09 00:13:23 -08:00
parent cc21c06641
commit 58fa014341
4 changed files with 151 additions and 6 deletions

View File

@ -6,7 +6,7 @@ import { Home } from "./src/frontend/pages/home";
import { NotFound } from "./src/frontend/pages/not-found"; import { NotFound } from "./src/frontend/pages/not-found";
import demo from "./temp/appshell.html"; import demo from "./temp/appshell.html";
import { Post } from "./src/frontend/pages/post"; 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) { async function blogPosts(hmr: boolean) {
const glob = new Bun.Glob("**/*.md"); const glob = new Bun.Glob("**/*.md");
@ -29,12 +29,17 @@ async function blogPosts(hmr: boolean) {
headers: { "Content-Type": "text/html" }, headers: { "Content-Type": "text/html" },
}); });
// Get adjacent posts for navigation
const { previousPost, nextPost } = getAdjacentPosts(post.path);
const data = { const data = {
title: post.title, title: post.title,
summary: post.summary, summary: post.summary,
date: new Date(post.date), date: new Date(post.date),
readingTime: post.reading_time, readingTime: post.reading_time,
tags: post.tags || [], tags: post.tags || [],
previousPost,
nextPost,
}; };
// AppShell is already loaded, just send the <main> content // AppShell is already loaded, just send the <main> content
@ -53,7 +58,10 @@ async function blogPosts(hmr: boolean) {
return new Response( return new Response(
renderToString( renderToString(
<AppShell> <AppShell>
<Post meta={data} children={post.content} /> <Post
meta={data}
children={post.content}
/>
</AppShell>, </AppShell>,
), ),
{ {

View File

@ -163,3 +163,49 @@ export function formatDate(dateString: string): string {
day: 'numeric' 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 };
}

View File

@ -1,5 +1,10 @@
import React from 'react'; import React from 'react';
interface NavigationPost {
title: string;
path: string;
}
interface PostProps { interface PostProps {
// HTML string for the blog post body // HTML string for the blog post body
children: string; children: string;
@ -7,10 +12,14 @@ interface PostProps {
title: string; title: string;
date: Date; date: Date;
readingTime: string; readingTime: string;
previousPost?: NavigationPost | null;
nextPost?: NavigationPost | null;
}; };
} }
export function Post({ children, meta }: PostProps) { export function Post({ children, meta }: PostProps) {
const { previousPost, nextPost } = meta;
return ( return (
<main> <main>
<article className="blog-post"> <article className="blog-post">
@ -39,6 +48,22 @@ export function Post({ children, meta }: PostProps) {
</div> </div>
</header> </header>
<div className="post-content" dangerouslySetInnerHTML={{ __html: children }} /> <div className="post-content" dangerouslySetInnerHTML={{ __html: children }} />
<footer className="post-navigation">
<div className="post-nav-links">
{previousPost && (
<a href={previousPost.path} className="post-nav-link prev-nav">
<span className="nav-direction"> Previous</span>
<span className="nav-title">{previousPost.title}</span>
</a>
)}
{nextPost && (
<a href={nextPost.path} className="post-nav-link next-nav">
<span className="nav-direction">Next </span>
<span className="nav-title">{nextPost.title}</span>
</a>
)}
</div>
</footer>
</article> </article>
</main> </main>
) )

View File

@ -914,6 +914,64 @@ h1 {
margin: 0; 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 */ /* Responsive adjustments */
@media (max-width: 1200px) { @media (max-width: 1200px) {
aside { aside {
@ -1025,16 +1083,24 @@ h1 {
} }
.post-content h3 { .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 { .tag-pills {
gap: 6px; display: flex;
} }
.tag-pill { .tag-pill {
font-size: 12px; padding: 6px 12px;
padding: 4px 10px;
} }
} }