diff --git a/src/components/RelatedPosts.astro b/src/components/RelatedPosts.astro new file mode 100644 index 0000000..76c336b --- /dev/null +++ b/src/components/RelatedPosts.astro @@ -0,0 +1,157 @@ +--- +import type { CollectionEntry } from "astro:content"; + +interface Props { + posts: CollectionEntry<"posts">[]; +} + +const { posts } = Astro.props; + +const fmtDate = (d: Date) => + d.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); +--- + + + + diff --git a/src/components/TableOfContents.astro b/src/components/TableOfContents.astro new file mode 100644 index 0000000..accbcd6 --- /dev/null +++ b/src/components/TableOfContents.astro @@ -0,0 +1,88 @@ +--- +interface Props { + headings: { depth: number; slug: string; text: string }[]; +} + +const { headings } = Astro.props; +--- + +
+ + Table of Contents + + + +
+ + diff --git a/src/layouts/PostLayout.astro b/src/layouts/PostLayout.astro index 4a958f1..b67733f 100644 --- a/src/layouts/PostLayout.astro +++ b/src/layouts/PostLayout.astro @@ -1,5 +1,8 @@ --- import BaseLayout from "./BaseLayout.astro"; +import TableOfContents from "@/components/TableOfContents.astro"; +import RelatedPosts from "@/components/RelatedPosts.astro"; +import type { CollectionEntry } from "astro:content"; interface Props { title: string; @@ -10,6 +13,8 @@ interface Props { tags?: string[]; image?: string; readingTime: string; + headings?: { depth: number; slug: string; text: string }[]; + relatedPosts?: CollectionEntry<"posts">[]; } const { @@ -21,6 +26,8 @@ const { tags = [], image, readingTime, + headings = [], + relatedPosts = [], } = Astro.props; const fmtDate = (d: Date) => @@ -49,6 +56,7 @@ const blogPostingLd = { --- +
{image && (
@@ -83,9 +91,13 @@ const blogPostingLd = { )} + {headings.length > 0 && } +
+ + {relatedPosts.length > 0 && }
@@ -114,6 +126,7 @@ const blogPostingLd = { margin-bottom: var(--space-10); padding-bottom: var(--space-6); border-bottom: 1px solid var(--border); + text-align: center; } .post-category { @@ -136,6 +149,7 @@ const blogPostingLd = { .post-meta-row { display: flex; align-items: center; + justify-content: center; flex-wrap: wrap; gap: var(--space-2); font-size: var(--text-sm); @@ -153,6 +167,7 @@ const blogPostingLd = { .post-tags { display: flex; flex-wrap: wrap; + justify-content: center; gap: var(--space-2); margin-top: var(--space-3); } @@ -165,4 +180,30 @@ const blogPostingLd = { border-radius: var(--radius-sm); border: 1px solid var(--border); } + + .reading-progress { + position: fixed; + top: 0; + left: 0; + height: 3px; + width: 0%; + background-color: var(--accent); + z-index: 200; + pointer-events: none; + } + + diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro index 5e77ca1..de0df63 100644 --- a/src/pages/posts/[...slug].astro +++ b/src/pages/posts/[...slug].astro @@ -11,11 +11,26 @@ export async function getStaticPaths() { } const { post } = Astro.props; -const { Content } = await render(post); +const { Content, headings } = await render(post); +const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3); const wordCount = post.body?.split(/\s+/).length ?? 0; const minutes = Math.max(1, Math.round(wordCount / 220)); const readingTime = `${minutes} min read`; + +const allPosts = await getCollection("posts", ({ data }) => !data.draft); +const relatedPosts = allPosts + .filter((p) => p.id !== post.id) + .map((p) => { + let score = 0; + if (p.data.category === post.data.category) score += 10; + score += p.data.tags.filter((t) => post.data.tags.includes(t)).length * 3; + return { post: p, score }; + }) + .filter((s) => s.score > 0) + .sort((a, b) => b.score - a.score || b.post.data.date.getTime() - a.post.data.date.getTime()) + .slice(0, 3) + .map((s) => s.post); ---