summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--SPRINT.md12
-rw-r--r--src/components/blog/PostNavigation.astro39
-rw-r--r--src/data/post.ts39
-rw-r--r--src/i18n/translations.ts8
-rw-r--r--src/layouts/BlogPost.astro6
-rw-r--r--src/pages/posts/[...slug].astro17
6 files changed, 107 insertions, 14 deletions
diff --git a/SPRINT.md b/SPRINT.md
index ba4c150..05d7ee1 100644
--- a/SPRINT.md
+++ b/SPRINT.md
@@ -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>