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:
commit
3f16702d08
@ -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 && \
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user