feat: sync blog content from external repo and rename /blog to /blogs

This commit is contained in:
Haitao Pan 2026-01-23 17:03:28 +08:00
parent 42a2aac730
commit 2fa13ed8b0
13 changed files with 115 additions and 92 deletions

2
.gitignore vendored
View File

@ -19,7 +19,7 @@ public/dl-index/
# Contentlayer cache
ui/docs/.contentlayer/
src/content/blog/
src/content/blogs/
# Lock files (如果不希望追踪)
uyarn.lock

View File

@ -7,7 +7,7 @@
},
"scripts": {
"dev": "bash scripts/Dev-MCP-Server.sh && next dev --turbo",
"prebuild": "tsx scripts/generate-content.ts && node scripts/build-contentlayer.mjs",
"prebuild": "bash scripts/sync-blog-content.sh && tsx scripts/generate-content.ts && node scripts/build-contentlayer.mjs",
"build": "next build",
"build:static": "npm run prebuild && next build",
"start": "node ./scripts/start.js",
@ -100,4 +100,4 @@
"glob": "10.5.0"
},
"packageManager": "yarn@4.12.0"
}
}

34
scripts/sync-blog-content.sh Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
CONTENT_DIR="src/content/blogs"
REPO_URL="https://github.com/cloud-neutral-workshop/knowledge.git"
# Ensure we're in the project root
cd "$(dirname "$0")/.."
if [ -d "${CONTENT_DIR}/.git" ]; then
echo "Updating existing git repo in ${CONTENT_DIR}..."
git -C "${CONTENT_DIR}" fetch --depth=1 origin main
git -C "${CONTENT_DIR}" reset --hard origin/main
git -C "${CONTENT_DIR}" clean -fdx
exit 0
fi
echo "Syncing content from ${REPO_URL} to ${CONTENT_DIR}..."
TMP_DIR=$(mktemp -d)
trap 'rm -rf "${TMP_DIR}"' EXIT
git clone --depth=1 "${REPO_URL}" "${TMP_DIR}/repo"
mkdir -p "${CONTENT_DIR}"
# Remove existing content but keep the directory
# Find and delete all files and directories inside CONTENT_DIR, but ignore errors if empty
find "${CONTENT_DIR}" -mindepth 1 -delete 2>/dev/null || true
# Copy content from repo to content dir, excluding .git
tar -C "${TMP_DIR}/repo" --exclude='.git' -cf - . | tar -C "${CONTENT_DIR}" -xf -
echo "Content synced successfully."

View File

@ -61,7 +61,7 @@ export default async function BlogPostPage({ params }: PageProps) {
<main className="flex min-h-screen flex-col bg-slate-50">
<div className="mx-auto w-full max-w-4xl px-4 py-16">
<Link
href="/blog"
href="/blogs"
className="mb-8 inline-flex items-center text-sm font-semibold text-brand transition hover:text-brand-dark"
>
{post.date ? 'Back to Blog' : '返回博客'}
@ -98,7 +98,7 @@ export default async function BlogPostPage({ params }: PageProps) {
<footer className="mt-16 border-t border-slate-200 pt-8">
<Link
href="/blog"
href="/blogs"
className="inline-flex items-center text-sm font-semibold text-brand transition hover:text-brand-dark"
>
Back to Blog

View File

@ -13,7 +13,7 @@ const staticEntries: MetadataRoute.Sitemap = [
priority: 1,
},
{
url: `${baseUrl}/blog`,
url: `${baseUrl}/blogs`,
changeFrequency: 'weekly',
priority: 0.8,
},
@ -43,7 +43,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const [posts, collections] = await Promise.all([getBlogPosts(), getDocCollections()])
const blogEntries: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
url: `${baseUrl}/blogs/${post.slug}`,
lastModified: post.date ? new Date(post.date) : undefined,
changeFrequency: 'weekly',
priority: 0.7,

View File

@ -27,9 +27,9 @@ type NavSubItem = {
export default function Navbar() {
const pathname = usePathname()
const isHiddenRoute = pathname
? ['/login', '/register', '/xstream', '/xcloudflow', '/xscopehub', '/blog'].some((prefix) =>
pathname.startsWith(prefix),
)
? ['/login', '/register', '/xstream', '/xcloudflow', '/xscopehub', '/blogs'].some((prefix) =>
pathname.startsWith(prefix),
)
: false
const [menuOpen, setMenuOpen] = useState(false)
const [selectedChannels, setSelectedChannels] = useState<ReleaseChannel[]>(['stable'])
@ -98,48 +98,48 @@ export default function Navbar() {
const accountChildren: NavSubItem[] = user
? [
{
key: 'userCenter',
label: accountCopy.userCenter,
href: '/panel',
togglePath: '/panel',
},
...(user?.isAdmin || user?.isOperator
? [
{
key: 'management',
label: accountCopy.management,
href: '/panel/management',
togglePath: '/panel/management',
} satisfies NavSubItem,
]
: []),
{
key: 'logout',
label: accountCopy.logout,
href: '/logout',
},
]
{
key: 'userCenter',
label: accountCopy.userCenter,
href: '/panel',
togglePath: '/panel',
},
...(user?.isAdmin || user?.isOperator
? [
{
key: 'management',
label: accountCopy.management,
href: '/panel/management',
togglePath: '/panel/management',
} satisfies NavSubItem,
]
: []),
{
key: 'logout',
label: accountCopy.logout,
href: '/logout',
},
]
: [
{
key: 'register',
label: nav.account.register,
href: '/register',
togglePath: '/register',
},
{
key: 'login',
label: nav.account.login,
href: '/login',
togglePath: '/login',
},
{
key: 'demo',
label: nav.account.demo,
href: '/demo',
togglePath: '/demo',
},
]
{
key: 'register',
label: nav.account.register,
href: '/register',
togglePath: '/register',
},
{
key: 'login',
label: nav.account.login,
href: '/login',
togglePath: '/login',
},
{
key: 'demo',
label: nav.account.demo,
href: '/demo',
togglePath: '/demo',
},
]
const accountLabel = nav.account.title
@ -194,7 +194,7 @@ export default function Navbar() {
{ key: 'docs', label: labels.docs, href: '/docs' },
]
const downloadLink = { key: 'blog', label: labels.download, href: '/blog' }
const downloadLink = { key: 'blog', label: labels.download, href: '/blogs' }
const servicesLink = {
key: 'services',

View File

@ -118,20 +118,18 @@ export default function BlogList({ posts, categories }: BlogListProps) {
return (
<Link
key={tab.key}
href={`/blog${isActive ? '' : `?category=${tab.key}`}`}
className={`flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition ${
isActive
href={`/blogs${isActive ? '' : `?category=${tab.key}`}`}
className={`flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition ${isActive
? 'border-brand bg-brand text-white shadow-sm'
: 'border-slate-200 bg-white text-slate-700 hover:border-brand/60 hover:text-brand'
}`}
}`}
aria-current={isActive ? 'page' : undefined}
>
<span>{tab.label}</span>
{labelWithCount ? (
<span
className={`rounded-full px-2 py-0.5 text-xs font-bold ${
isActive ? 'bg-white/20 text-white' : 'bg-slate-100 text-slate-700'
}`}
className={`rounded-full px-2 py-0.5 text-xs font-bold ${isActive ? 'bg-white/20 text-white' : 'bg-slate-100 text-slate-700'
}`}
>
{labelWithCount}
</span>
@ -140,18 +138,16 @@ export default function BlogList({ posts, categories }: BlogListProps) {
)
})}
<Link
href="/blog"
className={`flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition ${
!selectedCategory
href="/blogs"
className={`flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition ${!selectedCategory
? 'border-brand bg-brand text-white shadow-sm'
: 'border-slate-200 bg-white text-slate-700 hover:border-brand/60 hover:text-brand'
}`}
}`}
>
<span
className={`rounded-full px-2 py-0.5 text-xs font-bold ${
!selectedCategory ? 'bg-white/20 text-white' : 'bg-slate-100 text-slate-700'
}`}
className={`rounded-full px-2 py-0.5 text-xs font-bold ${!selectedCategory ? 'bg-white/20 text-white' : 'bg-slate-100 text-slate-700'
}`}
>
{posts.length}
</span>
@ -191,7 +187,7 @@ export default function BlogList({ posts, categories }: BlogListProps) {
</div>
)}
<Link
href={`/blog/${post.slug}`}
href={`/blogs/${post.slug}`}
className="ml-auto text-sm font-semibold text-brand transition hover:text-brand-dark"
>
Read more
@ -204,12 +200,11 @@ export default function BlogList({ posts, categories }: BlogListProps) {
{totalPages > 1 && (
<nav className="mt-12 flex items-center justify-center gap-2">
<Link
href={`/blog?page=${Math.max(1, currentPage - 1)}${selectedCategory ? `&category=${selectedCategory}` : ''}`}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition ${
currentPage === 1
href={`/blogs?page=${Math.max(1, currentPage - 1)}${selectedCategory ? `&category=${selectedCategory}` : ''}`}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition ${currentPage === 1
? 'cursor-not-allowed text-slate-400'
: 'text-brand hover:bg-slate-100'
}`}
}`}
aria-disabled={currentPage === 1}
>
Previous
@ -218,26 +213,23 @@ export default function BlogList({ posts, categories }: BlogListProps) {
{Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNumber) => (
<Link
key={pageNumber}
href={`/blog?page=${pageNumber}${selectedCategory ? `&category=${selectedCategory}` : ''}`}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition ${
pageNumber === currentPage
href={`/blogs?page=${pageNumber}${selectedCategory ? `&category=${selectedCategory}` : ''}`}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition ${pageNumber === currentPage
? 'bg-brand text-white'
: 'text-slate-700 hover:bg-slate-100'
}`}
}`}
>
{pageNumber}
</Link>
))}
<Link
href={`/blog?page=${Math.min(totalPages, currentPage + 1)}${
selectedCategory ? `&category=${selectedCategory}` : ''
}`}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition ${
currentPage === totalPages
href={`/blogs?page=${Math.min(totalPages, currentPage + 1)}${selectedCategory ? `&category=${selectedCategory}` : ''
}`}
className={`px-4 py-2 text-sm font-semibold rounded-lg transition ${currentPage === totalPages
? 'cursor-not-allowed text-slate-400'
: 'text-brand hover:bg-slate-100'
}`}
}`}
aria-disabled={currentPage === totalPages}
>
Next

View File

@ -71,7 +71,7 @@ export default function CommunityFeed({ posts = [] }: CommunityFeedProps) {
</h2>
<p className="text-sm text-slate-600 sm:text-base">{data.subtitle}</p>
</div>
<Link href="/blog" className="text-sm font-semibold text-[#3467e9] hover:text-[#2957cf]">
<Link href="/blogs" className="text-sm font-semibold text-[#3467e9] hover:text-[#2957cf]">
{data.cta}
</Link>
</div>
@ -91,7 +91,7 @@ export default function CommunityFeed({ posts = [] }: CommunityFeedProps) {
<div className="mt-3 space-y-2">
<h3 className="text-lg font-semibold text-slate-900">
<Link
href={`/blog/${post.slug}`}
href={`/blogs/${post.slug}`}
className="hover:text-slate-900"
>
{post.title}
@ -105,7 +105,7 @@ export default function CommunityFeed({ posts = [] }: CommunityFeedProps) {
)}
</div>
<Link
href={`/blog/${post.slug}`}
href={`/blogs/${post.slug}`}
className="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-[#3467e9] hover:text-[#2957cf]"
>
{language === 'zh' ? '查看详情' : 'View details'}

View File

@ -189,8 +189,7 @@ const CONTACT_PANEL: ContactPanelContent = {
],
}
const BLOG_CONTENT_ROOT = path.join(process.cwd(), 'src', 'content', 'blog')
const KNOWLEDGE_CONTENT_ROOT = path.join(process.cwd(), 'content')
const BLOG_CONTENT_ROOT = path.join(process.cwd(), 'src', 'content', 'blogs', 'content')
const CATEGORY_MAP: { key: string; label: string; match: (segments: string[]) => boolean }[] = [
{ key: 'infra-cloud', label: 'Infra & Cloud', match: (segments) => segments[0] === '04-infra-platform' },
@ -198,6 +197,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: '资讯',
@ -211,9 +211,6 @@ const CATEGORY_MAP: { key: string; label: string; match: (segments: string[]) =>
]
export function resolveBlogContentRoot(): string {
if (fs.existsSync(KNOWLEDGE_CONTENT_ROOT)) {
return KNOWLEDGE_CONTENT_ROOT
}
return BLOG_CONTENT_ROOT
}

View File

@ -40,7 +40,7 @@ const xcloudflow: ProductConfig = {
docsQuickstart: 'https://www.svc.plus/xcloudflow/docs/quickstart',
docsApi: 'https://www.svc.plus/xcloudflow/docs/api',
docsIssues: 'https://github.com/Cloud-Neutral/XCloudFlow/issues',
blogUrl: 'https://www.svc.plus/blog/tags/xcloudflow',
blogUrl: 'https://www.svc.plus/blogs/tags/xcloudflow',
videosUrl: 'https://www.svc.plus/videos/xcloudflow',
downloadUrl: 'https://www.svc.plus/xcloudflow/downloads',
editions: {

View File

@ -40,7 +40,7 @@ const xscopehub: ProductConfig = {
docsQuickstart: 'https://www.svc.plus/xscopehub/docs/quickstart',
docsApi: 'https://www.svc.plus/xscopehub/docs/api',
docsIssues: 'https://github.com/Cloud-Neutral/XScopeHub/issues',
blogUrl: 'https://www.svc.plus/blog/tags/xscopehub',
blogUrl: 'https://www.svc.plus/blogs/tags/xscopehub',
videosUrl: 'https://www.svc.plus/videos/xscopehub',
downloadUrl: 'https://www.svc.plus/xscopehub/downloads',
editions: {

View File

@ -40,7 +40,7 @@ const xstream: ProductConfig = {
docsQuickstart: 'https://github.com/Cloud-Neutral/Xstream#readme',
docsApi: 'https://github.com/Cloud-Neutral/Xstream/tree/main/docs',
docsIssues: 'https://github.com/Cloud-Neutral/Xstream/issues',
blogUrl: 'https://www.svc.plus/blog',
blogUrl: 'https://www.svc.plus/blogs',
videosUrl: 'https://www.svc.plus/videos',
downloadUrl: 'https://github.com/Cloud-Neutral/Xstream/releases',
editions: {