Merge pull request #23 from cloud-neutral-toolkit/codex/fix-dynamic-loading-of-blog-md-files-i6k675

Trim blog list payload and load categories dynamically
This commit is contained in:
cloudneutral 2026-01-01 18:32:30 +08:00 committed by GitHub
commit 3f16702d08
4 changed files with 66 additions and 18 deletions

View File

@ -26,8 +26,6 @@ RUN apt-get update \
&& corepack prepare yarn@4.12.0 --activate
COPY . .
RUN rm -rf src/content/blog/* \
&& mkdir -p src/content/blog
RUN find . -name "package-lock.json" -delete
RUN yarn install --immutable && \
yarn prebuild && \

View File

@ -5,8 +5,8 @@ import type { Metadata } from 'next'
import { Suspense } from 'react'
import BlogList from '@components/blog/BlogList'
import type { BlogPostSummary } from '@lib/blogContent'
import { getBlogPosts } from '@lib/blogContent'
import type { BlogCategory, BlogPostSummary } from '@lib/blogContent'
import { getBlogCategories, getBlogPosts } from '@lib/blogContent'
export const metadata: Metadata = {
title: 'Blog | Cloud-Neutral',
@ -15,10 +15,11 @@ export const metadata: Metadata = {
export default async function BlogPage() {
const posts = await getBlogPosts()
const categories: BlogCategory[] = await getBlogCategories()
const postsWithoutContent: BlogPostSummary[] = posts.map(({ content: _content, ...post }) => post)
return (
<Suspense fallback={<div className="p-6 text-center">Loading blog content...</div>}>
<BlogList posts={postsWithoutContent} />
<BlogList posts={postsWithoutContent} categories={categories} />
</Suspense>
)
}

View File

@ -6,17 +6,7 @@ import { useSearchParams } from 'next/navigation'
import BrandCTA from '@components/BrandCTA'
import SearchComponent from '@components/search'
import type { BlogPostSummary } from '@lib/blogContent'
const CATEGORY_TABS: { key: string; label: string }[] = [
{ key: 'infra-cloud', label: 'Infra & Cloud' },
{ key: 'observability', label: 'Observability' },
{ key: 'identity', label: 'ID & Security' },
{ key: 'iac-devops', label: 'IaC & DevOps' },
{ key: 'data-ai', label: 'Data & AI' },
{ key: 'insight', label: '资讯' },
{ key: 'essays', label: '随笔&观察' },
]
import type { BlogCategory, BlogPostSummary } from '@lib/blogContent'
function formatDate(dateStr: string | undefined, language: 'zh' | 'en'): string {
if (!dateStr) return ''
@ -40,6 +30,7 @@ function formatDate(dateStr: string | undefined, language: 'zh' | 'en'): string
interface BlogListProps {
posts: BlogPostSummary[]
categories: BlogCategory[]
}
function buildCategoryCounts(posts: BlogPostSummary[]) {
@ -60,11 +51,22 @@ function detectLanguage(posts: BlogPostSummary[]): 'zh' | 'en' {
return 'en'
}
export default function BlogList({ posts }: BlogListProps) {
export default function BlogList({ posts, categories }: BlogListProps) {
const searchParams = useSearchParams()
const selectedCategory = searchParams.get('category')
const page = searchParams.get('page')
const categoryTabs = useMemo(() => {
const categoriesFromPosts = posts
.map((post) => post.category)
.filter((category): category is NonNullable<BlogPostSummary['category']> => Boolean(category))
.map((category) => ({ key: category.key, label: category.label ?? category.key }))
return [...categories, ...categoriesFromPosts].filter(
(category, index, self) => self.findIndex((item) => item.key === category.key) === index,
)
}, [categories, posts])
const categoryCounts = useMemo(() => buildCategoryCounts(posts), [posts])
const filteredPosts = useMemo(() => {
if (!selectedCategory) return posts
@ -109,7 +111,7 @@ export default function BlogList({ posts }: BlogListProps) {
</div>
<div className="mb-10 flex flex-wrap items-center gap-3">
{CATEGORY_TABS.map((tab) => {
{categoryTabs.map((tab) => {
const isActive = tab.key === selectedCategory
const labelWithCount = categoryCounts[tab.key]

View File

@ -1,3 +1,4 @@
import fs from 'fs/promises'
import { cache } from 'react'
import { readMdxDirectory, readMdxFile } from './mdx'
@ -17,6 +18,11 @@ export interface BlogPost {
}
}
export interface BlogCategory {
key: string
label: string
}
export type BlogPostSummary = Omit<BlogPost, 'content'>
const BLOG_EXTENSIONS = ['.md', '.mdx']
@ -27,6 +33,7 @@ const CATEGORY_MAP: { key: string; label: string; match: (segments: string[]) =>
{ key: 'identity', label: 'ID & Security', match: (segments) => segments[0] === '01-id-security' },
{ key: 'iac-devops', label: 'IaC & DevOps', match: (segments) => segments[0] === '02-iac-devops' },
{ key: 'data-ai', label: 'Data & AI', match: (segments) => segments[0] === '05-data-ai' },
{ key: 'workshops', label: 'Workshops', match: (segments) => segments[0] === '06-workshops' },
{
key: 'insight',
label: '资讯',
@ -39,6 +46,16 @@ const CATEGORY_MAP: { key: string; label: string; match: (segments: string[]) =>
},
]
const CATEGORY_DIRECTORIES: Record<string, { key: string; label: string }> = {
'04-infra-platform': { key: 'infra-cloud', label: 'Infra & Cloud' },
'03-observability': { key: 'observability', label: 'Observability' },
'01-id-security': { key: 'identity', label: 'ID & Security' },
'02-iac-devops': { key: 'iac-devops', label: 'IaC & DevOps' },
'05-data-ai': { key: 'data-ai', label: 'Data & AI' },
'00-global': { key: 'insight', label: '资讯' },
'06-workshops': { key: 'workshops', label: 'Workshops' },
}
const readBlogFiles = cache(async () =>
readMdxDirectory('', {
baseDir: resolveBlogContentRoot(),
@ -47,6 +64,36 @@ const readBlogFiles = cache(async () =>
}),
)
export const getBlogCategories = cache(async (): Promise<BlogCategory[]> => {
try {
const contentRoot = resolveBlogContentRoot()
const entries = await fs.readdir(contentRoot, { withFileTypes: true })
const directories = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name)
return directories
.sort()
.map((dirName) => {
const mapped = CATEGORY_DIRECTORIES[dirName]
if (mapped) return mapped
const withoutPrefix = dirName.replace(/^\d+-/, '')
const normalized = withoutPrefix
.split('-')
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
return { key: dirName, label: normalized || dirName }
})
.filter((category, index, self) => self.findIndex((item) => item.key === category.key) === index)
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error
}
return []
}
})
function resolveCategory(slug: string): { key: string; label: string } | undefined {
const segments = slug.split('/')
const matched = CATEGORY_MAP.find((category) => category.match(segments))