feat: sync blog content from external repo and rename /blog to /blogs
This commit is contained in:
parent
42a2aac730
commit
2fa13ed8b0
2
.gitignore
vendored
2
.gitignore
vendored
@ -19,7 +19,7 @@ public/dl-index/
|
||||
|
||||
# Contentlayer cache
|
||||
ui/docs/.contentlayer/
|
||||
src/content/blog/
|
||||
src/content/blogs/
|
||||
|
||||
# Lock files (如果不希望追踪)
|
||||
uyarn.lock
|
||||
|
||||
@ -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
34
scripts/sync-blog-content.sh
Executable 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."
|
||||
@ -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
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user