import { getCollection, render, type CollectionEntry } from 'astro:content' import { readingTime, calculateWordCountFromHtml } from '@/lib/utils' export async function getAllAuthors(): Promise[]> { return await getCollection('authors') } export async function getAllPosts(): Promise[]> { const posts = await getCollection('blog') return posts .filter((post) => !post.data.draft && !isSubpost(post.id)) .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) } export async function getAllPostsAndSubposts(): Promise< CollectionEntry<'blog'>[] > { const posts = await getCollection('blog') return posts .filter((post) => !post.data.draft) .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) } export async function getAllProjects(): Promise[]> { const projects = await getCollection('projects') return projects.sort((a, b) => { const dateA = a.data.startDate?.getTime() || 0 const dateB = b.data.startDate?.getTime() || 0 return dateB - dateA }) } export async function getAllTags(): Promise> { const posts = await getAllPosts() return posts.reduce((acc, post) => { post.data.tags?.forEach((tag) => { acc.set(tag, (acc.get(tag) || 0) + 1) }) return acc }, new Map()) } export async function getAdjacentPosts(currentId: string): Promise<{ newer: CollectionEntry<'blog'> | null older: CollectionEntry<'blog'> | null parent: CollectionEntry<'blog'> | null }> { const allPosts = await getAllPosts() if (isSubpost(currentId)) { const parentId = getParentId(currentId) const allPosts = await getAllPosts() const parent = allPosts.find((post) => post.id === parentId) || null const posts = await getCollection('blog') const subposts = posts .filter( (post) => isSubpost(post.id) && getParentId(post.id) === parentId && !post.data.draft, ) .sort((a, b) => { const dateDiff = a.data.date.valueOf() - b.data.date.valueOf() if (dateDiff !== 0) return dateDiff const orderA = a.data.order ?? 0 const orderB = b.data.order ?? 0 return orderA - orderB }) const currentIndex = subposts.findIndex((post) => post.id === currentId) if (currentIndex === -1) { return { newer: null, older: null, parent } } return { newer: currentIndex < subposts.length - 1 ? subposts[currentIndex + 1] : null, older: currentIndex > 0 ? subposts[currentIndex - 1] : null, parent, } } const parentPosts = allPosts.filter((post) => !isSubpost(post.id)) const currentIndex = parentPosts.findIndex((post) => post.id === currentId) if (currentIndex === -1) { return { newer: null, older: null, parent: null } } return { newer: currentIndex > 0 ? parentPosts[currentIndex - 1] : null, older: currentIndex < parentPosts.length - 1 ? parentPosts[currentIndex + 1] : null, parent: null, } } export async function getPostsByAuthor( authorId: string, ): Promise[]> { const posts = await getAllPosts() return posts.filter((post) => post.data.authors?.includes(authorId)) } export async function getPostsByTag( tag: string, ): Promise[]> { const posts = await getAllPosts() return posts.filter((post) => post.data.tags?.includes(tag)) } export async function getRecentPosts( count: number, ): Promise[]> { const posts = await getAllPosts() return posts.slice(0, count) } export async function getSortedTags(): Promise< { tag: string; count: number }[] > { const tagCounts = await getAllTags() return [...tagCounts.entries()] .map(([tag, count]) => ({ tag, count })) .sort((a, b) => { const countDiff = b.count - a.count return countDiff !== 0 ? countDiff : a.tag.localeCompare(b.tag) }) } export function getParentId(subpostId: string): string { return subpostId.split('/')[0] } export async function getSubpostsForParent( parentId: string, ): Promise[]> { const posts = await getCollection('blog') return posts .filter( (post) => !post.data.draft && isSubpost(post.id) && getParentId(post.id) === parentId, ) .sort((a, b) => { const dateDiff = a.data.date.valueOf() - b.data.date.valueOf() if (dateDiff !== 0) return dateDiff const orderA = a.data.order ?? 0 const orderB = b.data.order ?? 0 return orderA - orderB }) } export function groupPostsByYear( posts: CollectionEntry<'blog'>[], ): Record[]> { return posts.reduce( (acc: Record[]>, post) => { const year = post.data.date.getFullYear().toString() ;(acc[year] ??= []).push(post) return acc }, {}, ) } export async function hasSubposts(postId: string): Promise { const subposts = await getSubpostsForParent(postId) return subposts.length > 0 } export function isSubpost(postId: string): boolean { return postId.includes('/') } export async function getParentPost( subpostId: string, ): Promise | null> { if (!isSubpost(subpostId)) { return null } const parentId = getParentId(subpostId) const allPosts = await getAllPosts() return allPosts.find((post) => post.id === parentId) || null } export async function parseAuthors(authorIds: string[] = []) { if (!authorIds.length) return [] const allAuthors = await getAllAuthors() const authorMap = new Map(allAuthors.map((author) => [author.id, author])) return authorIds.map((id) => { const author = authorMap.get(id) return { id, name: author?.data?.name || id, avatar: author?.data?.avatar || '/static/logo.png', isRegistered: !!author, } }) } export async function getPostById( postId: string, ): Promise | null> { const allPosts = await getAllPostsAndSubposts() return allPosts.find((post) => post.id === postId) || null } export async function getSubpostCount(parentId: string): Promise { const subposts = await getSubpostsForParent(parentId) return subposts.length } export async function getCombinedReadingTime(postId: string): Promise { const post = await getPostById(postId) if (!post) return readingTime(0) let totalWords = calculateWordCountFromHtml(post.body) if (!isSubpost(postId)) { const subposts = await getSubpostsForParent(postId) for (const subpost of subposts) { totalWords += calculateWordCountFromHtml(subpost.body) } } return readingTime(totalWords) } export async function getPostReadingTime(postId: string): Promise { const post = await getPostById(postId) if (!post) return readingTime(0) const wordCount = calculateWordCountFromHtml(post.body) return readingTime(wordCount) } export type TOCHeading = { slug: string text: string depth: number isSubpostTitle?: boolean } export type TOCSection = { type: 'parent' | 'subpost' title: string headings: TOCHeading[] subpostId?: string } export async function getTOCSections(postId: string): Promise { const post = await getPostById(postId) if (!post) return [] const parentId = isSubpost(postId) ? getParentId(postId) : postId const parentPost = isSubpost(postId) ? await getPostById(parentId) : post if (!parentPost) return [] const sections: TOCSection[] = [] const { headings: parentHeadings } = await render(parentPost) if (parentHeadings.length > 0) { sections.push({ type: 'parent', title: 'Overview', headings: parentHeadings.map((heading) => ({ slug: heading.slug, text: heading.text, depth: heading.depth, })), }) } const subposts = await getSubpostsForParent(parentId) for (const subpost of subposts) { const { headings: subpostHeadings } = await render(subpost) if (subpostHeadings.length > 0) { sections.push({ type: 'subpost', title: subpost.data.title, headings: subpostHeadings.map((heading, index) => ({ slug: heading.slug, text: heading.text, depth: heading.depth, isSubpostTitle: index === 0, })), subpostId: subpost.id, }) } } return sections }