import { getCollection, type CollectionEntry } from 'astro:content'; export type PostEntry = CollectionEntry<'posts'>; export type TaxonomyItem = { name: string; count: number; }; export const POSTS_PER_PAGE = 8; export function getContentCacheKey(scope: string, entries: Array<{ id: string; body?: string; data: unknown; digest?: string | number }>): string { const fingerprints = entries .map((entry) => `${entry.id}:${entry.digest ?? JSON.stringify([entry.data, entry.body ?? ''])}`) .sort() .join('|'); return `${scope}:${fingerprints}`; } export async function getPublicPosts(): Promise { const posts = await getCollection('posts', ({ data }) => !data.draft); return posts.sort((left, right) => { const dateDifference = right.data.pubDate.getTime() - left.data.pubDate.getTime(); return dateDifference || left.id.localeCompare(right.id); }); } export function slugify(value: string): string { return value .normalize('NFKD') .trim() .toLowerCase() .replace(/[\u0300-\u036f]/g, '') .replace(/[^\p{Letter}\p{Number}]+/gu, '-') .replace(/^-+|-+$/g, ''); } export function getPostPath(post: PostEntry): string { const source = post.id.replace(/\\/g, '/').replace(/\.(md|mdx)$/i, ''); const slug = source.split('/').map(slugify).filter(Boolean).join('/'); return `/posts/${slug}/`; } export function getPostCardImage(post: PostEntry, defaultCovers: readonly string[], siteUrl: string): string | undefined { if (post.data.cover) return post.data.cover; if (post.data.hero) return post.data.hero; const source = post.body ?? ''; const markdownImage = /!\[[^\]]*\]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+["'][^"']*["'])?\s*\)/g; const htmlImage = /]*\bsrc\s*=\s*(?:["']([^"']+)["']|([^\s>]+))[^>]*>/gi; const markdownMatch = markdownImage.exec(source); const htmlMatch = htmlImage.exec(source); const firstMatch = !htmlMatch || (markdownMatch && markdownMatch.index < htmlMatch.index) ? markdownMatch : htmlMatch; const bodyImage = firstMatch?.[1] ?? firstMatch?.[2]; if (bodyImage) { const base = new URL(getPostPath(post), siteUrl); const resolved = new URL(bodyImage, base); return resolved.origin === base.origin ? `${resolved.pathname}${resolved.search}${resolved.hash}` : resolved.href; } if (!defaultCovers.length) return undefined; let hash = 0; for (const character of post.id) hash = (hash * 31 + character.charCodeAt(0)) >>> 0; return defaultCovers[hash % defaultCovers.length]; } export function getExcerpt(post: PostEntry, length = 180): string { if (post.data.description) return post.data.description; const text = (post.body ?? '').replace(/```[\s\S]*?```/g, '').replace(/[#*_>`~\[\]()]/g, '').replace(/\s+/g, ' ').trim(); return text.length > length ? `${text.slice(0, length).trim()}...` : text; } export function getPostTaxonomy(post: PostEntry): { categories: string[]; tags: string[] } { return { categories: [...new Set(post.data.categories)].sort((a, b) => a.localeCompare(b)), tags: [...new Set(post.data.tags)].sort((a, b) => a.localeCompare(b)) }; } export function deriveTaxonomy( posts: PostEntry[], field: 'categories' | 'tags' ): TaxonomyItem[] { const counts = new Map(); for (const post of posts) { for (const name of new Set(post.data[field])) { counts.set(name, (counts.get(name) ?? 0) + 1); } } return [...counts] .map(([name, count]) => ({ name, count })) .sort((left, right) => left.name.localeCompare(right.name)); } export function getTaxonomyPosts(posts: PostEntry[], field: 'categories' | 'tags', slug: string): PostEntry[] { return posts.filter((post) => post.data[field].some((name) => slugify(name) === slug)); } export function getArchiveYears(posts: PostEntry[]): Map> { const archives = new Map>(); for (const post of posts) { const year = post.data.pubDate.getFullYear(); const month = post.data.pubDate.getMonth() + 1; const months = archives.get(year) ?? new Map(); const entries = months.get(month) ?? []; entries.push(post); months.set(month, entries); archives.set(year, months); } return archives; }