129 lines
3.5 KiB
TypeScript
129 lines
3.5 KiB
TypeScript
import type { CollectionEntry } from 'astro:content';
|
|
import { getCollection } from 'astro:content';
|
|
|
|
export type BlogPost = {
|
|
entry: CollectionEntry<'posts'>;
|
|
title: string;
|
|
slug: string;
|
|
date: Date;
|
|
dateText: string;
|
|
tags: string[];
|
|
categories: string[];
|
|
summary: string;
|
|
comments: boolean;
|
|
cover?: string;
|
|
href: string;
|
|
body: string;
|
|
};
|
|
|
|
const categorySlugMap: Record<string, string> = {
|
|
'技术': 'tech',
|
|
'建站手札': 'site-notes',
|
|
'学习': 'study',
|
|
'生活': 'life',
|
|
'杂谈': 'notes',
|
|
};
|
|
|
|
function asArray(value: unknown): string[] {
|
|
if (Array.isArray(value)) return value.map(String).filter(Boolean);
|
|
if (typeof value === 'string' && value.trim()) return [value.trim()];
|
|
return [];
|
|
}
|
|
|
|
function getCategories(entry: CollectionEntry<'posts'>): string[] {
|
|
const data = entry.data as Record<string, unknown>;
|
|
return [...asArray(data.categories), ...asArray(data.category)];
|
|
}
|
|
|
|
function toDate(value: unknown): Date {
|
|
if (value instanceof Date) return value;
|
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
const date = new Date(value);
|
|
if (!Number.isNaN(date.valueOf())) return date;
|
|
}
|
|
return new Date(0);
|
|
}
|
|
|
|
function formatDate(date: Date): string {
|
|
return new Intl.DateTimeFormat('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
}).format(date);
|
|
}
|
|
|
|
function fallbackSlug(id: string): string {
|
|
return id
|
|
.split('/')
|
|
.pop()
|
|
?.replace(/\.md$/, '')
|
|
.toLowerCase() ?? 'post';
|
|
}
|
|
|
|
function makeSummary(entry: CollectionEntry<'posts'>): string {
|
|
const explicit = entry.data.summary ?? entry.data.description;
|
|
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
|
|
|
|
return entry.body
|
|
.replace(/```[\s\S]*?```/g, '')
|
|
.replace(/<[^>]+>/g, '')
|
|
.replace(/[#>*_`~\-\[\]\(\)]/g, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.slice(0, 120);
|
|
}
|
|
|
|
export async function getAllPosts(): Promise<BlogPost[]> {
|
|
const entries = await getCollection('posts');
|
|
|
|
return entries
|
|
.map((entry) => {
|
|
const date = toDate(entry.data.date);
|
|
const slug = String(entry.data.abbrlink ?? fallbackSlug(entry.id));
|
|
const title = String(entry.data.title ?? slug);
|
|
|
|
return {
|
|
entry,
|
|
title,
|
|
slug,
|
|
date,
|
|
dateText: formatDate(date),
|
|
tags: asArray(entry.data.tags),
|
|
categories: getCategories(entry),
|
|
summary: makeSummary(entry),
|
|
comments: entry.data.comments !== false,
|
|
cover: typeof entry.data.cover === 'string' ? entry.data.cover : undefined,
|
|
href: `/posts/${slug}/`,
|
|
body: entry.body,
|
|
};
|
|
})
|
|
.sort((a, b) => b.date.valueOf() - a.date.valueOf());
|
|
}
|
|
|
|
export async function getAllTags(): Promise<string[]> {
|
|
return [...new Set((await getAllPosts()).flatMap((post) => post.tags))].sort((a, b) => a.localeCompare(b, 'zh-CN'));
|
|
}
|
|
|
|
export async function getAllCategories(): Promise<string[]> {
|
|
return [...new Set((await getAllPosts()).flatMap((post) => post.categories))].sort((a, b) => a.localeCompare(b, 'zh-CN'));
|
|
}
|
|
|
|
export function getCategorySlug(category: string): string {
|
|
const mapped = categorySlugMap[category];
|
|
if (mapped) return mapped;
|
|
|
|
const slug = category
|
|
.trim()
|
|
.toLowerCase()
|
|
.normalize('NFKD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '');
|
|
|
|
return slug || 'category';
|
|
}
|
|
|
|
export function getCategoryHref(category: string): string {
|
|
return `/categories/${getCategorySlug(category)}/`;
|
|
}
|