diff options
| -rw-r--r-- | SPRINT.md | 12 | ||||
| -rw-r--r-- | src/components/blog/PostNavigation.astro | 39 | ||||
| -rw-r--r-- | src/data/post.ts | 39 | ||||
| -rw-r--r-- | src/i18n/translations.ts | 8 | ||||
| -rw-r--r-- | src/layouts/BlogPost.astro | 6 | ||||
| -rw-r--r-- | src/pages/posts/[...slug].astro | 17 |
6 files changed, 107 insertions, 14 deletions
@@ -7,17 +7,17 @@ Goal: Initialize project tooling for Claude Code ## In Progress ## Backlog (Prioritized) -- [ ] **[FEATURE-002]** Import posts from old Mastodon instance - - Fetch posts from https://mastodon.com.pl/@knightdave using the same approach as the Pleroma loader - - Integrate into the existing content collection alongside Pleroma posts -- [ ] **[FEATURE-003]** Add next/previous post navigation on blog post pages - - Add links to the previous and next posts on each blog post page - - Allow readers to quickly navigate between posts in sequence - [ ] **[FEATURE-004]** Add page number pagination on tag pages - Show numbered page links (e.g. "1, 2, 3, ..., 11") on tag listing pages like `/tags/microblog/` - Let users see total page count and jump to specific pages ## Completed This Sprint +- [x] **[FEATURE-003]** Add next/previous post navigation on blog post pages + - Completed: 2026-01-30 + - Notes: Category-scoped navigation (regular/microblog/archived) with i18n support (commit 468d7c4) +- [x] **[FEATURE-002]** Import posts from old Mastodon instance + - Completed: 2026-01-30 + - Notes: Imported via Mastodon API with local media caching (commit 3850218) - [x] **[FEATURE-001]** Add caching for Pleroma post fetching - Completed: 2026-01-29 - Notes: Incremental fetching via Astro 5 MetaStore + Mastodon since_id API. Supports force refresh via env var, config option, and cache TTL. diff --git a/src/components/blog/PostNavigation.astro b/src/components/blog/PostNavigation.astro new file mode 100644 index 0000000..037836f --- /dev/null +++ b/src/components/blog/PostNavigation.astro @@ -0,0 +1,39 @@ +--- +import type { CollectionEntry } from "astro:content"; +import { t } from "@/i18n/translations"; + +interface Props { + prevPost: CollectionEntry<"post"> | null | undefined; + nextPost: CollectionEntry<"post"> | null | undefined; + language?: string | undefined; +} + +const { prevPost, nextPost, language } = Astro.props; +--- + +{ + (prevPost || nextPost) && ( + <nav aria-label="Post navigation" class="mt-8 flex items-center gap-x-4"> + {prevPost && ( + <a + class="hover:text-accent me-auto py-2" + data-astro-prefetch + href={`/posts/${prevPost.id}/`} + > + <span class="sr-only">{t(language, "newerPostSr")}</span> + {t(language, "newerPost")} + </a> + )} + {nextPost && ( + <a + class="hover:text-accent ms-auto py-2" + data-astro-prefetch + href={`/posts/${nextPost.id}/`} + > + <span class="sr-only">{t(language, "olderPostSr")}</span> + {t(language, "olderPost")} + </a> + )} + </nav> + ) +} diff --git a/src/data/post.ts b/src/data/post.ts index 85cc0d0..08a6678 100644 --- a/src/data/post.ts +++ b/src/data/post.ts @@ -1,4 +1,5 @@ import { type CollectionEntry, getCollection } from "astro:content"; +import { collectionDateSort } from "@/utils/date"; /** filter out draft posts based on the environment and optionally archived posts */ export async function getAllPosts(includeArchived = false): Promise<CollectionEntry<"post">[]> { @@ -62,3 +63,41 @@ export function getUniqueTagsWithCount(posts: CollectionEntry<"post">[]): [strin ), ].sort((a, b) => b[1] - a[1]); } + +export type PostCategory = "regular" | "microblog" | "archived"; + +/** Determine the category of a post based on its tags. "archived" takes precedence over "microblog". */ +export function getPostCategory(post: CollectionEntry<"post">): PostCategory { + const tags = post.data.tags; + if (tags.includes("archived")) return "archived"; + if (tags.includes("microblog")) return "microblog"; + return "regular"; +} + +export interface PostNavigation { + prevPost: CollectionEntry<"post"> | null; + nextPost: CollectionEntry<"post"> | null; +} + +/** Get previous (newer) and next (older) posts within the same category. + * Posts are sorted newest-first. prevPost = newer, nextPost = older. + */ +export function getPostNavigation( + allPosts: CollectionEntry<"post">[], + currentPost: CollectionEntry<"post">, +): PostNavigation { + const category = getPostCategory(currentPost); + const sameCategoryPosts = allPosts + .filter((p) => getPostCategory(p) === category) + .sort(collectionDateSort); + + const currentIndex = sameCategoryPosts.findIndex((p) => p.id === currentPost.id); + + return { + prevPost: currentIndex > 0 ? (sameCategoryPosts[currentIndex - 1] ?? null) : null, + nextPost: + currentIndex < sameCategoryPosts.length - 1 + ? (sameCategoryPosts[currentIndex + 1] ?? null) + : null, + }; +} diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 5659257..0cead23 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -4,12 +4,20 @@ export const translations = { backToTop: "Back to top", updated: "Updated:", viewMoreWithTag: "View more blogs with the tag", + newerPost: "← Newer post", + olderPost: "Older post →", + newerPostSr: "Go to newer post:", + olderPostSr: "Go to older post:", }, pl: { viewOriginalPost: "Zobacz oryginalny wpis na Fediversum →", backToTop: "Powrót na górę", updated: "Zaktualizowano:", viewMoreWithTag: "Zobacz więcej wpisów z tagiem", + newerPost: "← Następny wpis", + olderPost: "Poprzedni wpis →", + newerPostSr: "Przejdź do następnego wpisu:", + olderPostSr: "Przejdź do poprzedniego wpisu:", }, } as const; diff --git a/src/layouts/BlogPost.astro b/src/layouts/BlogPost.astro index 764cd80..73f994b 100644 --- a/src/layouts/BlogPost.astro +++ b/src/layouts/BlogPost.astro @@ -2,15 +2,18 @@ import type { CollectionEntry } from "astro:content"; import Masthead from "@/components/blog/Masthead.astro"; +import PostNavigation from "@/components/blog/PostNavigation.astro"; import { t } from "@/i18n/translations"; import BaseLayout from "./Base.astro"; interface Props { post: CollectionEntry<"post">; + prevPost: CollectionEntry<"post"> | null | undefined; + nextPost: CollectionEntry<"post"> | null | undefined; } -const { post } = Astro.props; +const { post, prevPost, nextPost } = Astro.props; const { ogImage, title, description, updatedDate, publishDate, language } = post.data; const socialImage = ogImage ?? `/og-image/${post.id}.png`; const articleDate = updatedDate?.toISOString() ?? publishDate.toISOString(); @@ -49,6 +52,7 @@ const articleDate = updatedDate?.toISOString() ?? publishDate.toISOString(); } </div> </article> + <PostNavigation prevPost={prevPost} nextPost={nextPost} language={language} /> <button class="hover:border-link fixed end-4 bottom-8 z-90 flex h-10 w-10 translate-y-28 cursor-pointer items-center justify-center rounded-full border-2 border-transparent bg-zinc-200 text-3xl opacity-0 transition-all transition-discrete duration-300 data-[show=true]:translate-y-0 data-[show=true]:opacity-100 sm:end-8 sm:h-12 sm:w-12 dark:bg-zinc-700" data-show="false" diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro index 76e4f28..b66d911 100644 --- a/src/pages/posts/[...slug].astro +++ b/src/pages/posts/[...slug].astro @@ -1,24 +1,27 @@ --- import { render } from "astro:content"; import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; -import { getAllPosts } from "@/data/post"; +import { getAllPosts, getPostNavigation } from "@/data/post"; import PostLayout from "@/layouts/BlogPost.astro"; // if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr export const getStaticPaths = (async () => { const blogEntries = await getAllPosts(true); // Include archived posts for direct access - return blogEntries.map((post) => ({ - params: { slug: post.id }, - props: { post }, - })); + return blogEntries.map((post) => { + const { prevPost, nextPost } = getPostNavigation(blogEntries, post); + return { + params: { slug: post.id }, + props: { post, prevPost, nextPost }, + }; + }); }) satisfies GetStaticPaths; type Props = InferGetStaticPropsType<typeof getStaticPaths>; -const { post } = Astro.props; +const { post, prevPost, nextPost } = Astro.props; const { Content } = await render(post); --- -<PostLayout post={post}> +<PostLayout post={post} prevPost={prevPost} nextPost={nextPost}> <Content /> </PostLayout> |
