refactor: content collection config

This commit is contained in:
radishzzz 2025-03-18 01:19:24 +00:00
parent 473b13d0ab
commit 0dcd3a5838
2 changed files with 72 additions and 58 deletions

16
pnpm-lock.yaml generated
View file

@ -1158,8 +1158,8 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@vitest/eslint-plugin@1.1.37': '@vitest/eslint-plugin@1.1.38':
resolution: {integrity: sha512-cnlBV8zr0oaBu1Vk6ruqWzpPzFCtwY0yuwUQpNIeFOUl3UhXVwNUoOYfWkZzeToGuNBaXvIsr6/RgHrXiHXqXA==} resolution: {integrity: sha512-KcOTZyVz8RiM5HyriiDVrP1CyBGuhRxle+lBsmSs6NTJEO/8dKVAq+f5vQzHj1/Kc7bYXSDO6yBe62Zx0t5iaw==}
peerDependencies: peerDependencies:
'@typescript-eslint/utils': ^8.24.0 '@typescript-eslint/utils': ^8.24.0
eslint: '>= 8.57.0' eslint: '>= 8.57.0'
@ -1690,8 +1690,8 @@ packages:
duplexer@0.1.2: duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
electron-to-chromium@1.5.119: electron-to-chromium@1.5.120:
resolution: {integrity: sha512-Ku4NMzUjz3e3Vweh7PhApPrZSS4fyiCIbcIrG9eKrriYVLmbMepETR/v6SU7xPm98QTqMSYiCwfO89QNjXLkbQ==} resolution: {integrity: sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==}
emmet@2.4.11: emmet@2.4.11:
resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==}
@ -3852,7 +3852,7 @@ snapshots:
'@stylistic/eslint-plugin': 4.2.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) '@stylistic/eslint-plugin': 4.2.0(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
'@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
'@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/parser': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
'@vitest/eslint-plugin': 1.1.37(@typescript-eslint/utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) '@vitest/eslint-plugin': 1.1.38(@typescript-eslint/utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
ansis: 3.17.0 ansis: 3.17.0
cac: 6.7.14 cac: 6.7.14
eslint: 9.22.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
@ -4933,7 +4933,7 @@ snapshots:
'@unrs/rspack-resolver-binding-win32-x64-msvc@1.1.2': '@unrs/rspack-resolver-binding-win32-x64-msvc@1.1.2':
optional: true optional: true
'@vitest/eslint-plugin@1.1.37(@typescript-eslint/utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)': '@vitest/eslint-plugin@1.1.38(@typescript-eslint/utils@8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)':
dependencies: dependencies:
'@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/utils': 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2)
eslint: 9.22.0(jiti@2.4.2) eslint: 9.22.0(jiti@2.4.2)
@ -5349,7 +5349,7 @@ snapshots:
browserslist@4.24.4: browserslist@4.24.4:
dependencies: dependencies:
caniuse-lite: 1.0.30001705 caniuse-lite: 1.0.30001705
electron-to-chromium: 1.5.119 electron-to-chromium: 1.5.120
node-releases: 2.0.19 node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.4) update-browserslist-db: 1.1.3(browserslist@4.24.4)
@ -5608,7 +5608,7 @@ snapshots:
duplexer@0.1.2: {} duplexer@0.1.2: {}
electron-to-chromium@1.5.119: {} electron-to-chromium@1.5.120: {}
emmet@2.4.11: emmet@2.4.11:
dependencies: dependencies:

View file

@ -1,34 +1,31 @@
import type { CollectionEntry } from 'astro:content' import type { CollectionEntry } from 'astro:content'
import themeConfig from '@/config' import { defaultLocale } from '@/config'
import { supportedLangs } from '@/i18n/config'
import { getCollection } from 'astro:content' import { getCollection } from 'astro:content'
// Type definitions // Type definitions
export type Post = CollectionEntry<'posts'> & { export type Post = CollectionEntry<'posts'> & {
remarkPluginFrontmatter?: { remarkPluginFrontmatter: {
minutes?: number minutes: number
} }
} }
export type PostData = Post['data']
export type PostsGroupByYear = Map<number, Post[]>
// Get post metadata including reading time // Add metadata including reading time to post
async function getPostMeta(post: CollectionEntry<'posts'>): Promise<Post> { async function addMetaToPost(post: CollectionEntry<'posts'>): Promise<Post> {
const { remarkPluginFrontmatter } = await post.render() const { remarkPluginFrontmatter } = await post.render()
return { ...post, remarkPluginFrontmatter } return { ...post, remarkPluginFrontmatter: remarkPluginFrontmatter as { minutes: number } }
} }
/** /**
* Check if the post slug is duplicated under the same language * Find duplicate post slugs within the same language
* @param posts Array of blog posts * @param posts Array of blog posts
* @returns Array of duplicate slugs with language information * @returns Array of descriptive error messages for duplicate slugs
*/ */
export async function checkPostSlugDuplication(posts: Post[]): Promise<string[]> { export async function checkPostSlugDuplication(posts: CollectionEntry<'posts'>[]): Promise<string[]> {
const slugMap = new Map<string, Set<string>>() const slugMap = new Map<string, Set<string>>()
const duplicates: string[] = [] const duplicates: string[] = []
posts.forEach((post) => { posts.forEach((post) => {
const lang = post.data.lang || '' const lang = post.data.lang
const slug = post.data.abbrlink || post.slug const slug = post.data.abbrlink || post.slug
if (!slugMap.has(lang)) { if (!slugMap.has(lang)) {
@ -52,36 +49,45 @@ export async function checkPostSlugDuplication(posts: Post[]): Promise<string[]>
return duplicates return duplicates
} }
// Get all posts except drafts (include pinned) /**
* Get all posts (including pinned ones, excluding drafts in production)
* @param lang Language code, optional, defaults to site's default language
* @returns Posts filtered by language, enhanced with metadata, sorted by date
*/
export async function getPosts(lang?: string) { export async function getPosts(lang?: string) {
const defaultLocale = themeConfig.global.locale
const currentLang = lang || defaultLocale const currentLang = lang || defaultLocale
const posts = await getCollection( const filteredPosts = await getCollection(
'posts', 'posts',
({ data }: Post) => { ({ data }: CollectionEntry<'posts'>) => {
// Show drafts in dev mode only
const shouldInclude = import.meta.env.DEV || !data.draft const shouldInclude = import.meta.env.DEV || !data.draft
if (!supportedLangs.includes(currentLang)) {
return shouldInclude && data.lang === ''
}
return shouldInclude && (data.lang === currentLang || data.lang === '') return shouldInclude && (data.lang === currentLang || data.lang === '')
}, },
) )
const postsWithMeta = await Promise.all(posts.map(getPostMeta)) const enhancedPosts = await Promise.all(filteredPosts.map(addMetaToPost))
return postsWithMeta.sort((a: Post, b: Post) => return enhancedPosts.sort((a: Post, b: Post) =>
b.data.published.valueOf() - a.data.published.valueOf(), b.data.published.valueOf() - a.data.published.valueOf(),
) )
} }
// Get all posts except drafts (not pinned) /**
* Get all non-pinned posts
* @param lang Language code, optional, defaults to site's default language
* @returns Regular posts (not pinned), already filtered by language and drafts
*/
export async function getRegularPosts(lang?: string) { export async function getRegularPosts(lang?: string) {
const posts = await getPosts(lang) const posts = await getPosts(lang)
return posts.filter(post => !post.data.pin || post.data.pin === 0) return posts.filter(post => !post.data.pin)
} }
// Get pinned posts /**
* Get pinned posts and sort by pin priority
* @param lang Language code, optional, defaults to site's default language
* @returns Pinned posts sorted by pin value in descending order
*/
export async function getPinnedPosts(lang?: string) { export async function getPinnedPosts(lang?: string) {
const posts = await getPosts(lang) const posts = await getPosts(lang)
return posts return posts
@ -89,8 +95,12 @@ export async function getPinnedPosts(lang?: string) {
.sort((a, b) => (b.data.pin || 0) - (a.data.pin || 0)) .sort((a, b) => (b.data.pin || 0) - (a.data.pin || 0))
} }
// Get posts grouped by year (not pinned) /**
export async function getPostsByYear(lang?: string): Promise<PostsGroupByYear> { * Group posts by year and sort within each year
* @param lang Language code, optional, defaults to site's default language
* @returns Map of posts grouped by year (descending), with posts in each year sorted by date (descending)
*/
export async function getPostsByYear(lang?: string): Promise<Map<number, Post[]>> {
const posts = await getRegularPosts(lang) const posts = await getRegularPosts(lang)
const yearMap = new Map<number, Post[]>() const yearMap = new Map<number, Post[]>()
@ -99,26 +109,25 @@ export async function getPostsByYear(lang?: string): Promise<PostsGroupByYear> {
if (!yearMap.has(year)) { if (!yearMap.has(year)) {
yearMap.set(year, []) yearMap.set(year, [])
} }
yearMap.get(year)?.push(post) yearMap.get(year)!.push(post)
}) })
yearMap.forEach((yearPosts) => { yearMap.forEach((yearPosts) => {
yearPosts.sort((a: Post, b: Post) => { yearPosts.sort((a, b) => {
const aDate = a.data.published const aDate = a.data.published
const bDate = b.data.published const bDate = b.data.published
const monthDiff = bDate.getMonth() - aDate.getMonth() return bDate.getMonth() - aDate.getMonth() || bDate.getDate() - aDate.getDate()
if (monthDiff !== 0) {
return monthDiff
}
return bDate.getDate() - aDate.getDate()
}) })
}) })
return new Map([...yearMap.entries()].sort((a, b) => b[0] - a[0])) return new Map([...yearMap.entries()].sort((a, b) => b[0] - a[0]))
} }
// Get all tags sorted by post count /**
* Get all tags sorted by post count
* @param lang Language code, optional, defaults to site's default language
* @returns Array of tags sorted by popularity (most posts first)
*/
export async function getAllTags(lang?: string) { export async function getAllTags(lang?: string) {
const tagMap = await getPostsGroupByTags(lang) const tagMap = await getPostsGroupByTags(lang)
const tagsWithCount = Array.from(tagMap.entries()) const tagsWithCount = Array.from(tagMap.entries())
@ -127,29 +136,34 @@ export async function getAllTags(lang?: string) {
return tagsWithCount.map(([tag]) => tag) return tagsWithCount.map(([tag]) => tag)
} }
// Get posts grouped by tags /**
* Group posts by their tags
* @param lang Language code, optional, defaults to site's default language
* @returns Map where keys are tag names and values are arrays of posts with that tag
*/
export async function getPostsGroupByTags(lang?: string) { export async function getPostsGroupByTags(lang?: string) {
const posts = await getPosts(lang) const posts = await getPosts(lang)
const tagMap = new Map<string, Post[]>() const tagMap = new Map<string, Post[]>()
posts.forEach((post: Post) => { posts.forEach((post: Post) => {
if (post.data.tags && post.data.tags.length > 0) { post.data.tags?.forEach((tag: string) => {
post.data.tags.forEach((tag: string) => { if (!tagMap.has(tag)) {
if (!tagMap.has(tag)) { tagMap.set(tag, [])
tagMap.set(tag, []) }
} tagMap.get(tag)!.push(post)
tagMap.get(tag)?.push(post) })
})
}
}) })
return tagMap return tagMap
} }
// Get all posts by specific tag /**
* Get all posts that contain a specific tag
* @param tag The tag name to filter posts by
* @param lang Language code, optional, defaults to site's default language
* @returns Array of posts that contain the specified tag
*/
export async function getPostsByTag(tag: string, lang?: string) { export async function getPostsByTag(tag: string, lang?: string) {
const posts = await getPosts(lang) const tagMap = await getPostsGroupByTags(lang)
return posts.filter((post: Post) => return tagMap.get(tag) || []
post.data.tags?.includes(tag),
)
} }