diff --git a/dashboard/next.config.mjs b/dashboard/next.config.mjs
index 97e6975..be8cabb 100644
--- a/dashboard/next.config.mjs
+++ b/dashboard/next.config.mjs
@@ -33,7 +33,6 @@ const nextConfig = {
// 显式 alias,保证 Turbopack 也能解析
config.resolve.alias = {
...(config.resolve.alias ?? {}),
- "@cms": path.join(__dirname, "src", "lib", "cms"),
"@components": path.join(__dirname, "src", "components"),
"@i18n": path.join(__dirname, "src", "i18n"),
"@lib": path.join(__dirname, "src", "lib"),
diff --git a/dashboard/src/app/AppProviders.tsx b/dashboard/src/app/AppProviders.tsx
index db8bc6c..ccd872e 100644
--- a/dashboard/src/app/AppProviders.tsx
+++ b/dashboard/src/app/AppProviders.tsx
@@ -2,23 +2,16 @@
import type { ReactNode } from 'react'
-import { collectExtensionProviders } from '@cms/client'
import { LanguageProvider } from '@i18n/LanguageProvider'
import { UserProvider } from '@lib/userStore'
import { ThemeProvider } from '@components/theme'
-const extensionProviders = collectExtensionProviders()
-
export function AppProviders({ children }: { children: ReactNode }) {
- let tree: ReactNode = (
+ return (
- {children}
+
+ {children}
+
)
-
- for (const Provider of extensionProviders) {
- tree = {tree}
- }
-
- return {tree}
}
diff --git a/dashboard/src/app/blog/page.tsx b/dashboard/src/app/blog/page.tsx
index 21d9150..0126442 100644
--- a/dashboard/src/app/blog/page.tsx
+++ b/dashboard/src/app/blog/page.tsx
@@ -2,7 +2,7 @@ export const dynamic = 'force-dynamic'
import Link from 'next/link'
import SearchComponent from '@components/search'
-import { getHomepagePosts } from '@cms/content'
+import { getHomepagePosts } from '@lib/marketingContent'
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx
index 77e1e46..e2b104a 100644
--- a/dashboard/src/app/layout.tsx
+++ b/dashboard/src/app/layout.tsx
@@ -1,9 +1,6 @@
export const dynamic = 'error'
-import { Fragment } from 'react'
-
import './globals.css'
-import { applyExtensionLayouts, getActiveTheme } from '@cms'
import { AppProviders } from './AppProviders'
export const metadata = {
@@ -11,20 +8,14 @@ export const metadata = {
description: 'Unified tools for your cloud native stack',
}
-const theme = getActiveTheme()
-const ThemeProvider = theme.Provider ?? Fragment
-const htmlAttributes = theme.htmlAttributes ?? { lang: 'en' }
-const bodyClassName = theme.bodyClassName ?? 'bg-[var(--color-background)] text-[var(--color-text)]'
+const htmlAttributes = { lang: 'en' }
+const bodyClassName = 'bg-[var(--color-background)] text-[var(--color-text)]'
export default function RootLayout({ children }: { children: React.ReactNode }) {
- const content = applyExtensionLayouts(children)
-
return (
-
- {content}
-
+ {children}
)
diff --git a/dashboard/src/components/home/CommunityFeed.tsx b/dashboard/src/components/home/CommunityFeed.tsx
index 2d577a4..1860990 100644
--- a/dashboard/src/components/home/CommunityFeed.tsx
+++ b/dashboard/src/components/home/CommunityFeed.tsx
@@ -5,7 +5,7 @@ import Link from 'next/link'
import { useLanguage } from '../../i18n/LanguageProvider'
import { designTokens } from '@theme/designTokens'
-import type { HomepagePost } from '@cms/content'
+import type { HomepagePost } from '@lib/marketingContent'
const feed: Record<'zh' | 'en', { title: string; subtitle: string; cta: string }> = {
zh: {
diff --git a/dashboard/src/components/home/CommunityFeedServer.tsx b/dashboard/src/components/home/CommunityFeedServer.tsx
index 9bba88b..7995531 100644
--- a/dashboard/src/components/home/CommunityFeedServer.tsx
+++ b/dashboard/src/components/home/CommunityFeedServer.tsx
@@ -1,4 +1,4 @@
-import { getHomepagePosts } from '@cms/content'
+import { getHomepagePosts } from '@lib/marketingContent'
import CommunityFeed from './CommunityFeed'
export default async function CommunityFeedServer() {
diff --git a/dashboard/src/components/home/ContactPanel.tsx b/dashboard/src/components/home/ContactPanel.tsx
index d1c7436..6cb4d6c 100644
--- a/dashboard/src/components/home/ContactPanel.tsx
+++ b/dashboard/src/components/home/ContactPanel.tsx
@@ -1,6 +1,6 @@
import ContactPanelClient from './ContactPanelClient'
-import { getContactPanelContent } from '@cms/content'
+import { getContactPanelContent } from '@lib/marketingContent'
type ContactPanelProps = {
className?: string
diff --git a/dashboard/src/components/home/ContactPanelClient.tsx b/dashboard/src/components/home/ContactPanelClient.tsx
index dafe9fd..ad1621f 100644
--- a/dashboard/src/components/home/ContactPanelClient.tsx
+++ b/dashboard/src/components/home/ContactPanelClient.tsx
@@ -15,7 +15,7 @@ import {
import { useMemo, useState } from 'react'
import clsx from 'clsx'
-import type { ContactPanelContent, ContactItemContent } from '@cms/content'
+import type { ContactPanelContent, ContactItemContent } from '@lib/marketingContent'
import { useLanguage } from '@i18n/LanguageProvider'
import { translations } from '@i18n/translations'
diff --git a/dashboard/src/components/home/HeroBanner.tsx b/dashboard/src/components/home/HeroBanner.tsx
index d32c908..18d6aa5 100644
--- a/dashboard/src/components/home/HeroBanner.tsx
+++ b/dashboard/src/components/home/HeroBanner.tsx
@@ -1,4 +1,4 @@
-import { getHomepageHero, getHeroSolutions } from '@cms/content'
+import { getHomepageHero, getHeroSolutions } from '@lib/marketingContent'
import HeroBannerClient from './HeroBannerClient'
export default async function HeroBanner() {
diff --git a/dashboard/src/components/home/HeroBannerClient.tsx b/dashboard/src/components/home/HeroBannerClient.tsx
index 8531044..4e81045 100644
--- a/dashboard/src/components/home/HeroBannerClient.tsx
+++ b/dashboard/src/components/home/HeroBannerClient.tsx
@@ -3,7 +3,7 @@
import Link from 'next/link'
import { useMemo } from 'react'
-import type { HeroContent, HeroSolution } from '@cms/content'
+import type { HeroContent, HeroSolution } from '@lib/marketingContent'
import HeroProductTabs from './HeroProductTabs'
import { useLanguage } from '@i18n/LanguageProvider'
import { translations } from '@i18n/translations'
diff --git a/dashboard/src/components/home/HeroProductTabs.tsx b/dashboard/src/components/home/HeroProductTabs.tsx
index 7579135..bdabca3 100644
--- a/dashboard/src/components/home/HeroProductTabs.tsx
+++ b/dashboard/src/components/home/HeroProductTabs.tsx
@@ -3,7 +3,7 @@
import Link from 'next/link'
import { useEffect, useId, useMemo, useState, type KeyboardEvent } from 'react'
-import type { HeroSolution } from '@cms/content'
+import type { HeroSolution } from '@lib/marketingContent'
import { useLanguage } from '@i18n/LanguageProvider'
import { translations } from '@i18n/translations'
diff --git a/dashboard/src/components/home/ProductMatrix.tsx b/dashboard/src/components/home/ProductMatrix.tsx
index 67b58fc..0be6fe2 100644
--- a/dashboard/src/components/home/ProductMatrix.tsx
+++ b/dashboard/src/components/home/ProductMatrix.tsx
@@ -1,6 +1,6 @@
import ProductMatrixClient from './ProductMatrixClient'
-import { getHeroSolutions } from '@cms/content'
+import { getHeroSolutions } from '@lib/marketingContent'
export default async function ProductMatrix() {
const solutions = await getHeroSolutions()
diff --git a/dashboard/src/components/home/ProductMatrixClient.tsx b/dashboard/src/components/home/ProductMatrixClient.tsx
index 43de68f..e41042b 100644
--- a/dashboard/src/components/home/ProductMatrixClient.tsx
+++ b/dashboard/src/components/home/ProductMatrixClient.tsx
@@ -4,7 +4,7 @@ import Link from 'next/link'
import { useMemo, useState } from 'react'
import clsx from 'clsx'
-import type { HeroSolution } from '@cms/content'
+import type { HeroSolution } from '@lib/marketingContent'
import { useLanguage } from '@i18n/LanguageProvider'
import { translations } from '@i18n/translations'
diff --git a/dashboard/src/components/home/SidebarCard.tsx b/dashboard/src/components/home/SidebarCard.tsx
index e2675bb..9881873 100644
--- a/dashboard/src/components/home/SidebarCard.tsx
+++ b/dashboard/src/components/home/SidebarCard.tsx
@@ -4,7 +4,7 @@ import Link from 'next/link'
import { useLanguage } from '@i18n/LanguageProvider'
import { translations } from '@i18n/translations'
-import type { SidebarSection } from '@cms/content'
+import type { SidebarSection } from '@lib/marketingContent'
interface SidebarCardProps {
section: SidebarSection
diff --git a/dashboard/src/lib/cms/client.ts b/dashboard/src/lib/cms/client.ts
deleted file mode 100644
index b4acd31..0000000
--- a/dashboard/src/lib/cms/client.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { collectExtensionProviders } from './extensionRuntime'
diff --git a/dashboard/src/lib/cms/config.ts b/dashboard/src/lib/cms/config.ts
deleted file mode 100644
index 0e71545..0000000
--- a/dashboard/src/lib/cms/config.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-export type TemplateName = 'default' | (string & {})
-export type ThemeName = 'default'
-export type ExtensionName = 'app-shell' | 'markdown-sync'
-
-export interface ContentSourceConfig {
- type: 'filesystem' | 'git'
- /**
- * Absolute path to the root directory that stores markdown content.
- * When `type` is `git`, the directory is expected to be hydrated by a
- * GitOps workflow ahead of time.
- */
- root: string
- /** Remote git repository used for GitOps synchronisation. */
- repository?: string
- /** Branch or tag to fetch when GitOps is enabled. */
- ref?: string
-}
-
-export interface CmsConfig {
- template: TemplateName
- theme: ThemeName
- extensions: ExtensionName[]
- content: {
- /** Default namespace used when a section does not override its source. */
- defaultNamespace: string
- sources: Record
- }
-}
-
-export const cmsConfig: CmsConfig = {
- template: 'default',
- theme: 'default',
- extensions: ['app-shell', 'markdown-sync'],
- content: {
- defaultNamespace: 'homepage',
- sources: {
- homepage: {
- type: 'filesystem',
- root: 'src/lib/cms/content/homepage',
- },
- docs: {
- type: 'filesystem',
- root: 'src/lib/cms/content/docs',
- },
- marketing: {
- type: 'git',
- root: 'cms/content/marketing',
- repository: 'git@github.com:example/xcontrol-marketing.git',
- ref: 'main',
- },
- },
- },
-}
diff --git a/dashboard/src/lib/cms/content/ContentContext.tsx b/dashboard/src/lib/cms/content/ContentContext.tsx
deleted file mode 100644
index 91e4017..0000000
--- a/dashboard/src/lib/cms/content/ContentContext.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-'use client'
-
-import { createContext, type ReactNode, useContext, useMemo } from 'react'
-
-import { listContentSourcesMetadata } from '../contentSources'
-
-type ContentSourceState = {
- namespace: string
- type: 'filesystem' | 'git'
- repository?: string
- ref?: string
-}
-
-const MarkdownContentContext = createContext([])
-
-export function MarkdownContentProvider({ children }: { children: ReactNode }) {
- const sources = useMemo(() => {
- return listContentSourcesMetadata()
- }, [])
-
- return {children}
-}
-
-export function useMarkdownSources() {
- return useContext(MarkdownContentContext)
-}
diff --git a/dashboard/src/lib/cms/content/getting-started.md b/dashboard/src/lib/cms/content/getting-started.md
deleted file mode 100644
index 33b1789..0000000
--- a/dashboard/src/lib/cms/content/getting-started.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Getting Started
-
-Welcome to the docs! This is a placeholder document.
diff --git a/dashboard/src/lib/cms/content/homepage.ts b/dashboard/src/lib/cms/content/homepage.ts
deleted file mode 100644
index 08b1328..0000000
--- a/dashboard/src/lib/cms/content/homepage.ts
+++ /dev/null
@@ -1,359 +0,0 @@
-import { promises as fs } from 'fs'
-import path from 'path'
-
-import { readMarkdownDirectory, readMarkdownFile } from '../../../lib/markdown'
-import { resolveContentSource } from './source'
-import { isFeatureEnabled } from '../../../lib/featureToggles'
-
-export interface HeroContent {
- eyebrow?: string
- title: string
- subtitle?: string
- primaryCtaLabel?: string
- primaryCtaHref?: string
- secondaryCtaLabel?: string
- secondaryCtaHref?: string
- highlights: string[]
- bodyHtml: string
-}
-
-export interface HeroSolution {
- slug: string
- title: string
- tagline?: string
- description?: string
- features: string[]
- bodyHtml: string
- primaryCtaLabel?: string
- primaryCtaHref?: string
- secondaryCtaLabel?: string
- secondaryCtaHref?: string
- tertiaryCtaLabel?: string
- tertiaryCtaHref?: string
-}
-
-export interface HomepagePost {
- slug: string
- title: string
- author?: string
- date?: string
- readingTime?: string
- tags: string[]
- excerpt: string
- contentHtml: string
-}
-
-export interface SidebarSection {
- slug: string
- title: string
- layout?: string
- tags: string[]
- bodyHtml: string
- ctaLabel?: string
- ctaHref?: string
- order?: number
-}
-
-export interface ContactItemContent {
- slug: string
- title: string
- type?: string
- description?: string
- bodyHtml: string
- qrValue?: string
- qrImage?: string
- icon?: string
- ctaLabel?: string
- ctaHref?: string
-}
-
-export interface ContactPanelContent {
- title: string
- subtitle?: string
- bodyHtml?: string
- items: ContactItemContent[]
-}
-
-const { absolutePath: HOMEPAGE_CONTENT_ROOT } = resolveContentSource('homepage')
-const CMS_HOMEPAGE_FLAG_PATH = '/homepage/dynamic'
-
-const FALLBACK_HERO: HeroContent = {
- title: '欢迎来到 XControl',
- highlights: [],
- bodyHtml: '',
-}
-
-const isCmsHomepageEnabled = () => isFeatureEnabled('cmsExperience', CMS_HOMEPAGE_FLAG_PATH)
-
-function ensureString(value: unknown): string | undefined {
- if (typeof value === 'string') {
- const trimmed = value.trim()
- return trimmed ? trimmed : undefined
- }
- if (Array.isArray(value)) {
- for (const item of value) {
- const stringItem = ensureString(item)
- if (stringItem) {
- return stringItem
- }
- }
- }
- return undefined
-}
-
-function ensureStringArray(value: unknown): string[] {
- if (Array.isArray(value)) {
- return value
- .map((item) => ensureString(item))
- .filter((item): item is string => Boolean(item && item.trim()))
- }
- const single = ensureString(value)
- return single ? [single] : []
-}
-
-function ensureNumber(value: unknown): number | undefined {
- const stringValue = ensureString(value)
- if (!stringValue) {
- return undefined
- }
- const parsed = Number(stringValue)
- return Number.isFinite(parsed) ? parsed : undefined
-}
-
-function extractExcerpt(markdown: string): string {
- const blocks = markdown.split(/\r?\n\s*\r?\n/)
- for (const block of blocks) {
- const trimmed = block.trim()
- if (!trimmed) continue
- const withoutFormatting = trimmed
- .replace(/^#+\s*/g, '')
- .replace(/[`*_>\[\]]/g, '')
- .replace(/\[(.*?)\]\((.*?)\)/g, '$1')
- if (withoutFormatting.trim()) {
- return withoutFormatting.trim()
- }
- }
- return ''
-}
-
-export async function getHomepageHero(): Promise {
- if (!isCmsHomepageEnabled()) {
- return FALLBACK_HERO
- }
-
- const hero = await readMarkdownFile('hero.md', { baseDir: HOMEPAGE_CONTENT_ROOT })
-
- return {
- eyebrow: ensureString(hero.metadata.eyebrow),
- title: ensureString(hero.metadata.title) ?? '欢迎来到 XControl',
- subtitle: ensureString(hero.metadata.subtitle),
- primaryCtaLabel: ensureString(hero.metadata.primaryCtaLabel),
- primaryCtaHref: ensureString(hero.metadata.primaryCtaHref),
- secondaryCtaLabel: ensureString(hero.metadata.secondaryCtaLabel),
- secondaryCtaHref: ensureString(hero.metadata.secondaryCtaHref),
- highlights: ensureStringArray(hero.metadata.highlights),
- bodyHtml: hero.html,
- }
-}
-
-export async function getHeroSolutions(): Promise {
- if (!isCmsHomepageEnabled()) {
- return []
- }
-
- let solutions: (HeroSolution & { order?: number })[] = []
- try {
- const files = await readMarkdownDirectory('solutions', { baseDir: HOMEPAGE_CONTENT_ROOT })
- solutions = files.map((file) => ({
- slug: file.slug,
- title: ensureString(file.metadata.title) ?? file.slug,
- tagline: ensureString(file.metadata.tagline),
- description: ensureString(file.metadata.description),
- features: ensureStringArray(file.metadata.features),
- bodyHtml: file.html,
- primaryCtaLabel: ensureString(file.metadata.primaryCtaLabel),
- primaryCtaHref: ensureString(file.metadata.primaryCtaHref),
- secondaryCtaLabel: ensureString(file.metadata.secondaryCtaLabel),
- secondaryCtaHref: ensureString(file.metadata.secondaryCtaHref),
- tertiaryCtaLabel: ensureString(file.metadata.tertiaryCtaLabel),
- tertiaryCtaHref: ensureString(file.metadata.tertiaryCtaHref),
- order: ensureNumber(file.metadata.order),
- }))
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
- throw error
- }
- }
-
- return solutions
- .sort((a, b) => {
- if (a.order !== undefined && b.order !== undefined) {
- return a.order - b.order
- }
- if (a.order !== undefined) return -1
- if (b.order !== undefined) return 1
- return a.title.localeCompare(b.title, 'zh-CN')
- })
- .map(({ order: _order, ...solution }) => solution)
-}
-
-export async function getHomepagePosts(): Promise {
- const blogContentRoot = path.join(process.cwd(), 'src', 'content', 'blog')
-
- let posts: HomepagePost[] = []
- try {
- const files = await readMarkdownDirectory('', { baseDir: blogContentRoot })
-
- posts = files.map((file) => {
- const title = ensureString(file.metadata.title) ?? file.slug
- const author = ensureString(file.metadata.author)
- const date = ensureString(file.metadata.date)
- const readingTime = ensureString(file.metadata.readingTime)
- const tags = ensureStringArray(file.metadata.tags)
- const excerptMetadata = ensureString(file.metadata.excerpt)
- const excerpt = excerptMetadata ?? extractExcerpt(file.content)
-
- return {
- slug: file.slug,
- title,
- author,
- date,
- readingTime,
- tags,
- excerpt,
- contentHtml: file.html,
- }
- })
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
- throw error
- }
- }
-
- const withParsedDates = posts.map((post) => ({
- ...post,
- dateValue: post.date ? new Date(post.date) : undefined,
- }))
-
- withParsedDates.sort((a, b) => {
- if (a.dateValue && b.dateValue) {
- return b.dateValue.getTime() - a.dateValue.getTime()
- }
- if (a.dateValue) return -1
- if (b.dateValue) return 1
- return a.title.localeCompare(b.title)
- })
-
- return withParsedDates.map(({ dateValue: _dateValue, ...post }) => post)
-}
-
-export async function getSidebarSections(): Promise {
- if (!isCmsHomepageEnabled()) {
- return []
- }
-
- const sidebarDir = path.join('sidebar')
- let sections: SidebarSection[] = []
- try {
- const files = await readMarkdownDirectory(sidebarDir, { baseDir: HOMEPAGE_CONTENT_ROOT })
- sections = files.map((file) => ({
- slug: file.slug,
- title: ensureString(file.metadata.title) ?? file.slug,
- layout: ensureString(file.metadata.layout),
- tags: ensureStringArray(file.metadata.tags),
- bodyHtml: file.html,
- ctaLabel: ensureString(file.metadata.ctaLabel),
- ctaHref: ensureString(file.metadata.ctaHref),
- order: ensureNumber(file.metadata.order),
- }))
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
- throw error
- }
- }
-
- return sections
- .sort((a, b) => {
- if (a.order !== undefined && b.order !== undefined) {
- return a.order - b.order
- }
- if (a.order !== undefined) return -1
- if (b.order !== undefined) return 1
- return a.title.localeCompare(b.title, 'zh-CN')
- })
- .map(({ order: _order, ...section }) => section)
-}
-
-export async function getContactPanelContent(): Promise {
- if (!isCmsHomepageEnabled()) {
- return undefined
- }
-
- let panelFile
- try {
- panelFile = await readMarkdownFile(path.join('contact', 'panel.md'), { baseDir: HOMEPAGE_CONTENT_ROOT })
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
- return undefined
- }
- throw error
- }
-
- let items: (ContactItemContent & { order?: number })[] = []
- try {
- const itemFiles = await readMarkdownDirectory(path.join('contact', 'items'), {
- baseDir: HOMEPAGE_CONTENT_ROOT,
- })
- items = itemFiles.map((file) => ({
- slug: file.slug,
- title: ensureString(file.metadata.title) ?? file.slug,
- type: ensureString(file.metadata.type),
- description: ensureString(file.metadata.description),
- bodyHtml: file.html,
- qrValue: ensureString(file.metadata.qrValue),
- qrImage: ensureString(file.metadata.qrImage),
- icon: ensureString(file.metadata.icon),
- ctaLabel: ensureString(file.metadata.ctaLabel),
- ctaHref: ensureString(file.metadata.ctaHref),
- order: ensureNumber(file.metadata.order),
- }))
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
- throw error
- }
- }
-
- const sortedItems = items
- .sort((a, b) => {
- if (a.order !== undefined && b.order !== undefined) {
- return a.order - b.order
- }
- if (a.order !== undefined) return -1
- if (b.order !== undefined) return 1
- return a.title.localeCompare(b.title, 'zh-CN')
- })
- .map(({ order: _order, ...item }) => item)
-
- return {
- title: ensureString(panelFile.metadata.title) ?? '保持联系',
- subtitle: ensureString(panelFile.metadata.subtitle),
- bodyHtml: panelFile.html,
- items: sortedItems,
- }
-}
-
-export async function getContentLastUpdated(): Promise {
- if (!isCmsHomepageEnabled()) {
- return undefined
- }
-
- try {
- const stats = await fs.stat(path.join(HOMEPAGE_CONTENT_ROOT, 'hero.md'))
- return stats.mtime.toISOString()
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
- throw error
- }
- }
- return undefined
-}
diff --git a/dashboard/src/lib/cms/content/homepage/contact/items/github-star.md b/dashboard/src/lib/cms/content/homepage/contact/items/github-star.md
deleted file mode 100644
index c8ee01f..0000000
--- a/dashboard/src/lib/cms/content/homepage/contact/items/github-star.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: 欢迎支持
-type: info
-order: 4
-icon: star
-description: 欢迎支持关注 Star
-ctaLabel: 访问 GitHub
-ctaHref: https://github.com/CloudNativeSuite
----
-点击链接访问 CloudNativeSuite GitHub,欢迎支持关注 Star,获取更多项目动态。
diff --git a/dashboard/src/lib/cms/content/homepage/contact/items/support.md b/dashboard/src/lib/cms/content/homepage/contact/items/support.md
deleted file mode 100644
index d2b5919..0000000
--- a/dashboard/src/lib/cms/content/homepage/contact/items/support.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-title: 获取支持
-type: info
-order: 3
-icon: life-buoy
-description: 了解使用反馈与社区支持
-ctaLabel: 联系我们
-ctaHref: https://github.com/svc-design/XControl/issues
----
-
-欢迎提交您的使用反馈或功能建议,我们会尽快回复。
-如需寻求技术帮助,可联系:**manbuzhe2008@gmail.com**
diff --git a/dashboard/src/lib/cms/content/homepage/contact/items/wechat-group.md b/dashboard/src/lib/cms/content/homepage/contact/items/wechat-group.md
deleted file mode 100644
index 36687b9..0000000
--- a/dashboard/src/lib/cms/content/homepage/contact/items/wechat-group.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: 加入微信群
-type: qr
-order: 2
-qrValue: https://xcontrol.cloud/contact/wechat-community
-qrImage: https://dl.svc.plus/images/contact/wechat-group.jpg
-description: 与产品团队和同行实时交流
----
-添加 XControl 社区小助手,获取最新活动信息并加入兴趣小组。
diff --git a/dashboard/src/lib/cms/content/homepage/contact/items/wechat-official.md b/dashboard/src/lib/cms/content/homepage/contact/items/wechat-official.md
deleted file mode 100644
index 3858f3a..0000000
--- a/dashboard/src/lib/cms/content/homepage/contact/items/wechat-official.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: 微信公众号
-type: qr
-order: 1
-qrValue: https://xcontrol.cloud/contact/wechat-official
-qrImage: https://dl.svc.plus/images/contact/wechat-official.jpg
-description: 了解商业产品和专业支持服务
----
-关注 XControl 官方公众号,解锁上云实践案例与专家分享。
diff --git a/dashboard/src/lib/cms/content/homepage/contact/panel.md b/dashboard/src/lib/cms/content/homepage/contact/panel.md
deleted file mode 100644
index 7cba45e..0000000
--- a/dashboard/src/lib/cms/content/homepage/contact/panel.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 保持联系
-subtitle: 扫码关注或加入社区,获取最新产品动态与支持。
----
diff --git a/dashboard/src/lib/cms/content/homepage/hero.md b/dashboard/src/lib/cms/content/homepage/hero.md
deleted file mode 100644
index 3e94bdd..0000000
--- a/dashboard/src/lib/cms/content/homepage/hero.md
+++ /dev/null
@@ -1,16 +0,0 @@
----
-eyebrow: 云原生套件
-title: 构建一体化的云原生工具集
-subtitle: 将资产管理、访问控制、可观测与自动化工作流整合到一个响应迅速的体验里,帮助团队高效落地治理策略。
-primaryCtaLabel: 立即体验
-primaryCtaHref: /register
-secondaryCtaLabel: 产品文档
-secondaryCtaHref: /docs
-highlights:
- - 跨集群纳管与多云环境统一治理
- - 以策略为核心的安全与合规编排
- - 数据驱动的可观测与成本分析
- - 场景化模板快速对接业务流程
----
-
-XControl 采用模块化架构设计,可在保持核心稳定的前提下按需引入观测、身份、编排等能力包。通过开放 API 与事件流,您可以轻松连接现有的 DevOps 工具链,让业务交付与平台治理协同运转。
diff --git a/dashboard/src/lib/cms/content/homepage/posts/2024-07-12-community-events.md b/dashboard/src/lib/cms/content/homepage/posts/2024-07-12-community-events.md
deleted file mode 100644
index ddfe603..0000000
--- a/dashboard/src/lib/cms/content/homepage/posts/2024-07-12-community-events.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-title: 社区巡回沙龙启动:实践分享与产品路线解读
-author: 社区团队
-date: 2024-07-12
-readingTime: 3 分钟
-tags:
- - 社区活动
- - 生态
----
-
-今年的巡回沙龙将覆盖八个城市,围绕平台工程、自动化安全与可观测进行实战分享。我们还准备了产品路线解读与开放问答,欢迎报名参与,与来自不同行业的伙伴一起交流经验。
diff --git a/dashboard/src/lib/cms/content/homepage/posts/2024-07-30-observability-insight.md b/dashboard/src/lib/cms/content/homepage/posts/2024-07-30-observability-insight.md
deleted file mode 100644
index ee1d95d..0000000
--- a/dashboard/src/lib/cms/content/homepage/posts/2024-07-30-observability-insight.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-title: 观测即服务:从日志到业务指标的一体化洞察
-author: 平台可观测团队
-date: 2024-07-30
-readingTime: 5 分钟
-tags:
- - 最佳实践
- - 观测
- - 数据分析
----
-
-针对多租户环境,我们重构了指标与日志的归档体系,实现了秒级可视化。通过业务域建模,团队可将技术指标与业务目标关联,快速定位影响用户体验的关键链路。
diff --git a/dashboard/src/lib/cms/content/homepage/posts/2024-08-15-release-notes.md b/dashboard/src/lib/cms/content/homepage/posts/2024-08-15-release-notes.md
deleted file mode 100644
index 68035b1..0000000
--- a/dashboard/src/lib/cms/content/homepage/posts/2024-08-15-release-notes.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-title: 版本 1.8 正式发布:策略联动与可观测双升级
-author: XControl 产品团队
-date: 2024-08-15
-readingTime: 8 分钟
-tags:
- - 发布公告
- - 策略中心
- - 可观测
-excerpt: 全新策略联动引擎与跨集群指标联邦能力上线,为大规模云原生团队带来统一的治理体验。
----
-
-我们针对复杂多集群环境推出了「策略联动」功能,可将身份、资源与安全策略编织成全局一致的执行链路。与此同时,指标联邦能力支持将多源观测数据按团队、业务或环境维度聚合分析,让治理效果实时可视化。
diff --git a/dashboard/src/lib/cms/content/homepage/sidebar/community.md b/dashboard/src/lib/cms/content/homepage/sidebar/community.md
deleted file mode 100644
index 40894d6..0000000
--- a/dashboard/src/lib/cms/content/homepage/sidebar/community.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: 社区热议
-ctaLabel: 加入社区
-ctaHref: https://example.com/community
-order: 1
----
-
-- [平台工程读书会](#) —— 每周聚焦云原生治理案例
-- [Slack 交流群](#) —— 与 2000+ 从业者实时交流
-- [GitHub Issues](#) —— 提交需求与反馈问题
diff --git a/dashboard/src/lib/cms/content/homepage/sidebar/resources.md b/dashboard/src/lib/cms/content/homepage/sidebar/resources.md
deleted file mode 100644
index 9003bc7..0000000
--- a/dashboard/src/lib/cms/content/homepage/sidebar/resources.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: 推荐资源
-order: 3
----
-
-1. [平台上线指南](#) — 分阶段部署 XControl 的最佳实践
-2. [安全策略手册](#) — 常见合规基线的策略模板
-3. [观测数据白皮书](#) — 如何构建统一的指标与日志视图
diff --git a/dashboard/src/lib/cms/content/homepage/sidebar/tags.md b/dashboard/src/lib/cms/content/homepage/sidebar/tags.md
deleted file mode 100644
index 3ec3549..0000000
--- a/dashboard/src/lib/cms/content/homepage/sidebar/tags.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-title: 热门标签
-layout: tags
-tags:
- - 策略驱动
- - 云原生安全
- - 平台工程
- - 观测洞察
- - 成本治理
- - 最佳实践
-order: 4
----
diff --git a/dashboard/src/lib/cms/content/homepage/solutions/xcloudflow.md b/dashboard/src/lib/cms/content/homepage/solutions/xcloudflow.md
deleted file mode 100644
index 906f453..0000000
--- a/dashboard/src/lib/cms/content/homepage/solutions/xcloudflow.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-title: XCloudFlow
-tagline: 多云 IaC
-order: 1
-description: 通过声明式模型统一编排多云基础设施,自动化落地资源策略与合规标准。
-primaryCtaLabel: 立刻体验
-primaryCtaHref: /demo?product=xcloudflow
-secondaryCtaLabel: 下载链接
-secondaryCtaHref: /download?product=xcloudflow
-tertiaryCtaLabel: 文档链接
-tertiaryCtaHref: /docs/xcloudflow
-features:
- - 跨云资源蓝图与参数化交付
- - GitOps 工作流驱动基础设施变更
- - 内置审批、审计保障合规
----
-XCloudFlow 将 Terraform、Pulumi 等主流 IaC 模型统一到一个工作台,为多云环境提供自助式交付与集中治理。
diff --git a/dashboard/src/lib/cms/content/homepage/solutions/xcontrol.md b/dashboard/src/lib/cms/content/homepage/solutions/xcontrol.md
deleted file mode 100644
index 86de632..0000000
--- a/dashboard/src/lib/cms/content/homepage/solutions/xcontrol.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-title: XControl 平台
-tagline: 云原生治理中枢
-order: 3
-description: 为多团队提供统一的权限、策略与工作流编排,让交付与治理协同无缝衔接。
-primaryCtaLabel: 立刻体验
-primaryCtaHref: /demo?product=xcontrol
-secondaryCtaLabel: 下载链接
-secondaryCtaHref: /download?product=xcontrol
-tertiaryCtaLabel: 文档链接
-tertiaryCtaHref: /docs/xcontrol
-features:
- - 一站式权限与合规策略中心
- - 工作流自动化驱动跨团队协作
- - 可扩展插件架构连接现有系统
----
-XControl 以策略即代码为核心,为云原生基础设施提供可观测、可治理、可审计的统一控制平面。
diff --git a/dashboard/src/lib/cms/content/homepage/solutions/xscopehub.md b/dashboard/src/lib/cms/content/homepage/solutions/xscopehub.md
deleted file mode 100644
index 6bbc611..0000000
--- a/dashboard/src/lib/cms/content/homepage/solutions/xscopehub.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-title: XScopeHub
-tagline: AI & 可观察性
-order: 2
-description: 利用 AI 驱动的分析工作台,统一日志、指标与追踪,快速定位异常并推荐修复路径。
-primaryCtaLabel: 立刻体验
-primaryCtaHref: /demo?product=xscopehub
-secondaryCtaLabel: 下载链接
-secondaryCtaHref: /download?product=xscopehub
-tertiaryCtaLabel: 文档链接
-tertiaryCtaHref: /docs/xscopehub
-features:
- - 全栈可观察性数据联邦检索
- - 智能告警关联与根因分析
- - 预置 AI 助手生成运维建议
----
-XScopeHub 通过语义化检索与时序分析,实现跨环境的可观察性汇聚与智能洞察。
diff --git a/dashboard/src/lib/cms/content/homepage/solutions/xstream.md b/dashboard/src/lib/cms/content/homepage/solutions/xstream.md
deleted file mode 100644
index 92e63f3..0000000
--- a/dashboard/src/lib/cms/content/homepage/solutions/xstream.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-title: XStream
-tagline: 网络加速器
-order: 4
-description: 按需构建全球传输网络,保障跨地域应用与数据同步的稳定低时延体验。
-primaryCtaLabel: 立刻体验
-primaryCtaHref: /demo?product=xstream
-secondaryCtaLabel: 下载链接
-secondaryCtaHref: /download?product=xstream
-tertiaryCtaLabel: 文档链接
-tertiaryCtaHref: /docs/xstream
-features:
- - 动态最优路径与带宽调度
- - 内置零信任安全与访问控制
- - 对接主流 CDN 与边缘节点
----
-XStream 通过软件定义的网络加速技术,为实时互动、音视频与数据分发提供稳定的全球链路。
diff --git a/dashboard/src/lib/cms/content/index.ts b/dashboard/src/lib/cms/content/index.ts
deleted file mode 100644
index 60685cb..0000000
--- a/dashboard/src/lib/cms/content/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './homepage'
diff --git a/dashboard/src/lib/cms/content/source.ts b/dashboard/src/lib/cms/content/source.ts
deleted file mode 100644
index 5831cbb..0000000
--- a/dashboard/src/lib/cms/content/source.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import path from 'path'
-
-import { cmsConfig } from '../config'
-
-export interface ResolvedContentSource {
- namespace: string
- absolutePath: string
- type: 'filesystem' | 'git'
- repository?: string
- ref?: string
-}
-
-export function resolveContentSource(namespace?: string): ResolvedContentSource {
- if (typeof window !== 'undefined') {
- throw new Error('resolveContentSource can only be called on the server')
- }
-
- const targetNamespace = namespace ?? cmsConfig.content.defaultNamespace
- const source = cmsConfig.content.sources[targetNamespace]
- if (!source) {
- const fallbackRoot = path.join(
- process.cwd(),
- 'src',
- 'lib',
- 'cms',
- 'content',
- targetNamespace
- )
- return {
- namespace: targetNamespace,
- absolutePath: fallbackRoot,
- type: 'filesystem',
- }
- }
-
- return {
- namespace: targetNamespace,
- absolutePath: path.isAbsolute(source.root)
- ? source.root
- : path.join(process.cwd(), source.root),
- type: source.type,
- repository: source.repository,
- ref: source.ref,
- }
-}
diff --git a/dashboard/src/lib/cms/contentSources.ts b/dashboard/src/lib/cms/contentSources.ts
deleted file mode 100644
index 782bf17..0000000
--- a/dashboard/src/lib/cms/contentSources.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { cmsConfig } from './config'
-
-export function listContentSourcesMetadata() {
- return Object.entries(cmsConfig.content.sources).map(([namespace, source]) => ({
- namespace,
- type: source.type,
- repository: source.repository,
- ref: source.ref,
- }))
-}
diff --git a/dashboard/src/lib/cms/extensionRuntime.ts b/dashboard/src/lib/cms/extensionRuntime.ts
deleted file mode 100644
index 3a9fddb..0000000
--- a/dashboard/src/lib/cms/extensionRuntime.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { ComponentType, createElement, isValidElement, type ReactNode } from 'react'
-
-import { cmsConfig, type ExtensionName, type ThemeName } from './config'
-import type { CmsExtension, CmsTheme } from './types'
-import { appShellExtension } from './extensions/appShell'
-import { markdownSyncExtension } from './extensions/markdownSync'
-import defaultTheme from './themes/default'
-import { AppShellBypass } from '../appShellBypass'
-
-const themeRegistry: Record = {
- default: defaultTheme,
-}
-
-const extensionRegistry: Record = {
- 'app-shell': appShellExtension,
- 'markdown-sync': markdownSyncExtension,
-}
-
-export function getActiveTheme(): CmsTheme {
- return themeRegistry[cmsConfig.theme]
-}
-
-export function getActiveExtensions(): CmsExtension[] {
- return cmsConfig.extensions
- .map((extensionName) => extensionRegistry[extensionName])
- .filter(Boolean)
-}
-
-export function collectExtensionProviders(): ComponentType<{ children: ReactNode }>[] {
- return getActiveExtensions().flatMap((extension) => extension.providers ?? [])
-}
-
-export function applyExtensionLayouts(children: ReactNode): ReactNode {
- const { content, skipAppShell } = unwrapAppShellBypass(children)
-
- return getActiveExtensions().reduceRight((acc, extension) => {
- if (!extension.Layout) {
- return acc
- }
- if (skipAppShell && extension.name === 'app-shell') {
- return acc
- }
- const LayoutComponent = extension.Layout
- return createElement(LayoutComponent, null, acc)
- }, content)
-}
-
-function unwrapAppShellBypass(node: ReactNode): { content: ReactNode; skipAppShell: boolean } {
- if (isValidElement(node) && (node.type as any) === AppShellBypass) {
- return { content: (node.props as any).children, skipAppShell: true }
- }
-
- return { content: node, skipAppShell: false }
-}
diff --git a/dashboard/src/lib/cms/extensions/appShell.tsx b/dashboard/src/lib/cms/extensions/appShell.tsx
deleted file mode 100644
index 4e65852..0000000
--- a/dashboard/src/lib/cms/extensions/appShell.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { ReactNode } from 'react'
-
-import Footer from '@components/Footer'
-import Navbar from '@components/Navbar'
-import { AskAIButton } from '@components/AskAIButton'
-
-import type { CmsExtension } from '../types'
-
-function AppShellLayout({ children }: { children: ReactNode }) {
- return (
- <>
-
-
- {children}
-
-
-
- >
- )
-}
-
-export const appShellExtension: CmsExtension = {
- name: 'app-shell',
- Layout: AppShellLayout,
-}
diff --git a/dashboard/src/lib/cms/extensions/markdownSync.tsx b/dashboard/src/lib/cms/extensions/markdownSync.tsx
deleted file mode 100644
index 1ad9b9f..0000000
--- a/dashboard/src/lib/cms/extensions/markdownSync.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { CmsExtension } from '../types'
-import { MarkdownContentProvider } from '../content/ContentContext'
-
-export const markdownSyncExtension: CmsExtension = {
- name: 'markdown-sync',
- providers: [MarkdownContentProvider],
-}
diff --git a/dashboard/src/lib/cms/index.ts b/dashboard/src/lib/cms/index.ts
deleted file mode 100644
index f306acf..0000000
--- a/dashboard/src/lib/cms/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export {
- applyExtensionLayouts,
- collectExtensionProviders,
- getActiveExtensions,
- getActiveTheme,
-} from './extensionRuntime'
-
-export {
- getActiveTemplate,
- listRegisteredTemplateNames,
- registerTemplate,
- registerTemplateLoader,
-} from '@modules/templateRegistry'
-export { listContentSourcesMetadata } from './contentSources'
diff --git a/dashboard/src/lib/cms/package.json b/dashboard/src/lib/cms/package.json
deleted file mode 100644
index 480dfb4..0000000
--- a/dashboard/src/lib/cms/package.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "@cms",
- "version": "1.0.0",
- "type": "module",
- "main": "index.ts"
-}
-
diff --git a/dashboard/src/lib/cms/templates/default/index.tsx b/dashboard/src/lib/cms/templates/default/index.tsx
deleted file mode 100644
index 7f0e43e..0000000
--- a/dashboard/src/lib/cms/templates/default/index.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from '@modules/templates/default'
-export { defaultHomeLayoutConfig } from './config'
diff --git a/dashboard/src/lib/cms/themes/default/index.tsx b/dashboard/src/lib/cms/themes/default/index.tsx
deleted file mode 100644
index e9ba741..0000000
--- a/dashboard/src/lib/cms/themes/default/index.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { CmsTheme } from '../../types'
-
-function PassthroughThemeProvider({ children }: { children: React.ReactNode }) {
- return <>{children}>
-}
-
-const defaultTheme: CmsTheme = {
- name: 'default',
- htmlAttributes: {
- lang: 'en',
- },
- bodyClassName: 'bg-[var(--color-background)] text-[var(--color-text)]',
- Provider: PassthroughThemeProvider,
-}
-
-export default defaultTheme
diff --git a/dashboard/src/lib/cms/types.ts b/dashboard/src/lib/cms/types.ts
deleted file mode 100644
index 73e3c48..0000000
--- a/dashboard/src/lib/cms/types.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { ComponentType, ReactNode } from 'react'
-
-import type { HomePageTemplateSlots, TemplateRenderProps } from '@modules/templates/types'
-
-export interface CmsTemplate {
- name: string
- pages: {
- home: ComponentType>
- [key: string]: ComponentType | undefined
- }
-}
-
-export interface CmsTheme {
- name: string
- htmlAttributes?: Record
- bodyClassName?: string
- Provider?: ComponentType<{ children: ReactNode }>
-}
-
-export interface CmsExtension {
- name: string
- providers?: ComponentType<{ children: ReactNode }>[]
- Layout?: ComponentType<{ children: ReactNode }>
-}
diff --git a/dashboard/src/lib/marketingContent.ts b/dashboard/src/lib/marketingContent.ts
new file mode 100644
index 0000000..03e48d7
--- /dev/null
+++ b/dashboard/src/lib/marketingContent.ts
@@ -0,0 +1,269 @@
+import path from 'path'
+
+import { readMarkdownDirectory } from './markdown'
+
+export interface HeroContent {
+ eyebrow?: string
+ title: string
+ subtitle?: string
+ primaryCtaLabel?: string
+ primaryCtaHref?: string
+ secondaryCtaLabel?: string
+ secondaryCtaHref?: string
+ highlights: string[]
+ bodyHtml: string
+}
+
+export interface HeroSolution {
+ slug: string
+ title: string
+ tagline?: string
+ description?: string
+ features: string[]
+ bodyHtml: string
+ primaryCtaLabel?: string
+ primaryCtaHref?: string
+ secondaryCtaLabel?: string
+ secondaryCtaHref?: string
+ tertiaryCtaLabel?: string
+ tertiaryCtaHref?: string
+}
+
+export interface HomepagePost {
+ slug: string
+ title: string
+ author?: string
+ date?: string
+ readingTime?: string
+ tags: string[]
+ excerpt: string
+ contentHtml: string
+}
+
+export interface SidebarSection {
+ slug: string
+ title: string
+ layout?: string
+ tags: string[]
+ bodyHtml: string
+ ctaLabel?: string
+ ctaHref?: string
+ order?: number
+}
+
+export interface ContactItemContent {
+ slug: string
+ title: string
+ type?: string
+ description?: string
+ bodyHtml: string
+ qrValue?: string
+ qrImage?: string
+ icon?: string
+ ctaLabel?: string
+ ctaHref?: string
+}
+
+export interface ContactPanelContent {
+ title: string
+ subtitle?: string
+ bodyHtml?: string
+ items: ContactItemContent[]
+}
+
+const HERO_CONTENT: HeroContent = {
+ eyebrow: '云原生套件',
+ title: '构建一体化的云原生工具集',
+ subtitle:
+ '将资产管理、访问控制、可观测与自动化工作流整合到一个响应迅速的体验里,帮助团队高效落地治理策略。',
+ primaryCtaLabel: '立即体验',
+ primaryCtaHref: '/register',
+ secondaryCtaLabel: '产品文档',
+ secondaryCtaHref: '/docs',
+ highlights: [
+ '跨集群纳管与多云环境统一治理',
+ '以策略为核心的安全与合规编排',
+ '数据驱动的可观测与成本分析',
+ '场景化模板快速对接业务流程',
+ ],
+ bodyHtml:
+ 'XControl 采用模块化架构设计,可在保持核心稳定的前提下按需引入观测、身份、编排等能力包。通过开放 API 与事件流,您可以轻松连接现有的 DevOps 工具链,让业务交付与平台治理协同运转。
',
+}
+
+const HERO_SOLUTIONS: HeroSolution[] = [
+ {
+ slug: 'xcloudflow',
+ title: 'XCloudFlow',
+ tagline: '多云 IaC',
+ description: '通过声明式模型统一编排多云基础设施,自动化落地资源策略与合规标准。',
+ features: ['跨云资源蓝图与参数化交付', 'GitOps 工作流驱动基础设施变更', '内置审批、审计保障合规'],
+ bodyHtml:
+ 'XCloudFlow 将 Terraform、Pulumi 等主流 IaC 模型统一到一个工作台,为多云环境提供自助式交付与集中治理。
',
+ primaryCtaLabel: '立刻体验',
+ primaryCtaHref: '/demo?product=xcloudflow',
+ secondaryCtaLabel: '下载链接',
+ secondaryCtaHref: '/download?product=xcloudflow',
+ tertiaryCtaLabel: '文档链接',
+ tertiaryCtaHref: '/docs/xcloudflow',
+ },
+ {
+ slug: 'xscopehub',
+ title: 'XScopeHub',
+ tagline: 'AI & 可观察性',
+ description: '利用 AI 驱动的分析工作台,统一日志、指标与追踪,快速定位异常并推荐修复路径。',
+ features: ['全栈可观察性数据联邦检索', '智能告警关联与根因分析', '预置 AI 助手生成运维建议'],
+ bodyHtml:
+ 'XScopeHub 通过语义化检索与时序分析,实现跨环境的可观察性汇聚与智能洞察。
',
+ primaryCtaLabel: '立刻体验',
+ primaryCtaHref: '/demo?product=xscopehub',
+ secondaryCtaLabel: '下载链接',
+ secondaryCtaHref: '/download?product=xscopehub',
+ tertiaryCtaLabel: '文档链接',
+ tertiaryCtaHref: '/docs/xscopehub',
+ },
+ {
+ slug: 'xstream',
+ title: 'XStream',
+ tagline: '网络加速器',
+ description: '按需构建全球传输网络,保障跨地域应用与数据同步的稳定低时延体验。',
+ features: ['动态最优路径与带宽调度', '内置零信任安全与访问控制', '对接主流 CDN 与边缘节点'],
+ bodyHtml:
+ 'XStream 通过软件定义的网络加速技术,为实时互动、音视频与数据分发提供稳定的全球链路。
',
+ primaryCtaLabel: '立刻体验',
+ primaryCtaHref: '/demo?product=xstream',
+ secondaryCtaLabel: '下载链接',
+ secondaryCtaHref: '/download?product=xstream',
+ tertiaryCtaLabel: '文档链接',
+ tertiaryCtaHref: '/docs/xstream',
+ },
+]
+
+const CONTACT_PANEL: ContactPanelContent = {
+ title: '保持联系',
+ subtitle: '扫码关注或加入社区,获取最新产品动态与支持。',
+ items: [
+ {
+ slug: 'wechat-official',
+ title: '微信公众号',
+ type: 'qr',
+ description: '了解商业产品和专业支持服务',
+ bodyHtml: '关注 XControl 官方公众号,解锁上云实践案例与专家分享。',
+ qrValue: 'https://xcontrol.cloud/contact/wechat-official',
+ qrImage: 'https://dl.svc.plus/images/contact/wechat-official.jpg',
+ },
+ {
+ slug: 'wechat-group',
+ title: '加入微信群',
+ type: 'qr',
+ description: '与产品团队和同行实时交流',
+ bodyHtml: '添加 XControl 社区小助手,获取最新活动信息并加入兴趣小组。',
+ qrValue: 'https://xcontrol.cloud/contact/wechat-community',
+ qrImage: 'https://dl.svc.plus/images/contact/wechat-group.jpg',
+ },
+ {
+ slug: 'support',
+ title: '获取支持',
+ type: 'info',
+ description: '了解使用反馈与社区支持',
+ bodyHtml:
+ '欢迎提交您的使用反馈或功能建议,我们会尽快回复。
如需寻求技术帮助,可联系:manbuzhe2008@gmail.com
',
+ icon: 'life-buoy',
+ ctaLabel: '联系我们',
+ ctaHref: 'https://github.com/svc-design/XControl/issues',
+ },
+ {
+ slug: 'github-star',
+ title: '欢迎支持',
+ type: 'info',
+ description: '欢迎支持关注 Star',
+ bodyHtml: '点击链接访问 CloudNativeSuite GitHub,欢迎支持关注 Star,获取更多项目动态。',
+ icon: 'star',
+ ctaLabel: '访问 GitHub',
+ ctaHref: 'https://github.com/CloudNativeSuite',
+ },
+ ],
+}
+
+const BLOG_CONTENT_ROOT = path.join(process.cwd(), 'src', 'content', 'blog')
+
+function extractExcerpt(markdown: string): string {
+ const blocks = markdown.split(/\r?\n\s*\r?\n/)
+ for (const block of blocks) {
+ const trimmed = block.trim()
+ if (!trimmed) continue
+ const withoutFormatting = trimmed
+ .replace(/^#+\s*/g, '')
+ .replace(/[`*_>\[\]]/g, '')
+ .replace(/\[(.*?)\]\((.*?)\)/g, '$1')
+ if (withoutFormatting.trim()) {
+ return withoutFormatting.trim()
+ }
+ }
+ return ''
+}
+
+export async function getHomepageHero(): Promise {
+ return HERO_CONTENT
+}
+
+export async function getHeroSolutions(): Promise {
+ return HERO_SOLUTIONS
+}
+
+export async function getHomepagePosts(): Promise {
+ let posts: HomepagePost[] = []
+ try {
+ const files = await readMarkdownDirectory('', { baseDir: BLOG_CONTENT_ROOT })
+
+ posts = files.map((file) => {
+ const title = typeof file.metadata.title === 'string' ? file.metadata.title : file.slug
+ const author = typeof file.metadata.author === 'string' ? file.metadata.author : undefined
+ const date = typeof file.metadata.date === 'string' ? file.metadata.date : undefined
+ const readingTime =
+ typeof file.metadata.readingTime === 'string' ? file.metadata.readingTime : undefined
+ const tags = Array.isArray(file.metadata.tags)
+ ? file.metadata.tags.filter((tag): tag is string => typeof tag === 'string')
+ : []
+ const excerptMetadata = typeof file.metadata.excerpt === 'string' ? file.metadata.excerpt : undefined
+ const excerpt = excerptMetadata ?? extractExcerpt(file.content)
+
+ return {
+ slug: file.slug,
+ title,
+ author,
+ date,
+ readingTime,
+ tags,
+ excerpt,
+ contentHtml: file.html,
+ }
+ })
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
+ throw error
+ }
+ }
+
+ const withParsedDates = posts.map((post) => ({
+ ...post,
+ dateValue: post.date ? new Date(post.date) : undefined,
+ }))
+
+ withParsedDates.sort((a, b) => {
+ if (a.dateValue && b.dateValue) {
+ return b.dateValue.getTime() - a.dateValue.getTime()
+ }
+ if (a.dateValue) return -1
+ if (b.dateValue) return 1
+ return a.title.localeCompare(b.title)
+ })
+
+ return withParsedDates.map(({ dateValue: _dateValue, ...post }) => post)
+}
+
+export async function getContactPanelContent(): Promise {
+ if (!CONTACT_PANEL.items.length) {
+ return undefined
+ }
+ return CONTACT_PANEL
+}
diff --git a/dashboard/src/modules/templateRegistry.ts b/dashboard/src/modules/templateRegistry.ts
index ff0c60c..3264555 100644
--- a/dashboard/src/modules/templateRegistry.ts
+++ b/dashboard/src/modules/templateRegistry.ts
@@ -1,12 +1,11 @@
import fs from 'node:fs'
import path from 'node:path'
-import { cmsConfig } from '@cms/config'
-import type { CmsTemplate } from '@cms/types'
-
import defaultTemplate from './templates/default'
import type { TemplateDefinition } from './templates/types'
+type CmsTemplate = TemplateDefinition
+
const BUILTIN_TEMPLATES: Record = {
default: defaultTemplate,
}
@@ -64,8 +63,7 @@ export function resolveTemplateName(explicitName?: string, options?: TemplateSel
}
const envOverride =
- typeof process !== 'undefined' &&
- (process.env.NEXT_PUBLIC_TEMPLATE || process.env.CMS_TEMPLATE)
+ typeof process !== 'undefined' && (process.env.NEXT_PUBLIC_TEMPLATE || process.env.CMS_TEMPLATE)
if (envOverride) {
return envOverride
@@ -75,7 +73,7 @@ export function resolveTemplateName(explicitName?: string, options?: TemplateSel
throw new Error('Template name not provided and config fallback disabled.')
}
- return cmsConfig.template
+ return 'default'
}
export function getActiveTemplate(options?: TemplateSelectionOptions): CmsTemplate {
diff --git a/dashboard/src/lib/cms/templates/default/config.ts b/dashboard/src/modules/templates/default/config.ts
similarity index 86%
rename from dashboard/src/lib/cms/templates/default/config.ts
rename to dashboard/src/modules/templates/default/config.ts
index 802e4be..03a451e 100644
--- a/dashboard/src/lib/cms/templates/default/config.ts
+++ b/dashboard/src/modules/templates/default/config.ts
@@ -1,4 +1,4 @@
-import type { CommonHomeLayoutConfig } from '@templates/layouts/commonHome'
+import type { CommonHomeLayoutConfig } from '../layouts/commonHome'
export const defaultHomeLayoutConfig: CommonHomeLayoutConfig = {
rootClassName: 'relative min-h-screen bg-brand-surface text-brand-navy antialiased',
@@ -17,8 +17,6 @@ export const defaultHomeLayoutConfig: CommonHomeLayoutConfig = {
containerClassName: 'relative px-8',
contentClassName: 'mx-auto w-full max-w-6xl',
gridClassName: 'grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px] lg:items-start lg:gap-12',
- slots: [
- { key: 'CommunityFeed' },
- ],
+ slots: [{ key: 'CommunityFeed' }],
},
}
diff --git a/dashboard/src/modules/templates/default/index.tsx b/dashboard/src/modules/templates/default/index.tsx
index eed8715..ae5d3d7 100644
--- a/dashboard/src/modules/templates/default/index.tsx
+++ b/dashboard/src/modules/templates/default/index.tsx
@@ -1,7 +1,7 @@
import ProductMatrix from '@components/home/ProductMatrix'
import Sidebar from '@components/home/Sidebar'
-import { defaultHomeLayoutConfig } from '@cms/templates/default/config'
+import { defaultHomeLayoutConfig } from './config'
import { createCommonHomeTemplate } from '../layouts/commonHome'
import type { TemplateDefinition } from '../types'
diff --git a/dashboard/tests/unit/templateRegistry.test.ts b/dashboard/tests/unit/templateRegistry.test.ts
deleted file mode 100644
index 22b1338..0000000
--- a/dashboard/tests/unit/templateRegistry.test.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import fs from 'node:fs'
-import path from 'node:path'
-
-import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'
-
-import { cmsConfig } from '@cms/config'
-
-vi.mock('@cms/content/source', () => ({
- resolveContentSource: () => ({
- absolutePath: 'src/lib/cms/content',
- }),
-}))
-
-import {
- __resetTemplateRegistryForTests,
- getActiveTemplate,
- listRegisteredTemplateNames,
- registerTemplate,
- registerTemplateLoader,
- resolveTemplateName,
- type TemplateDefinition,
-} from '@modules/templateRegistry'
-
-const originalTemplateName = cmsConfig.template
-const templateFixturesPath = path.join(process.cwd(), 'src', 'modules', 'templates', 'fs-template')
-
-const runtimeTemplate: TemplateDefinition = {
- name: 'runtime',
- pages: {
- home: () => null,
- },
-}
-
-beforeEach(() => {
- __resetTemplateRegistryForTests()
- cmsConfig.template = 'default'
- delete process.env.NEXT_PUBLIC_TEMPLATE
- delete process.env.CMS_TEMPLATE
- fs.rmSync(templateFixturesPath, { recursive: true, force: true })
-})
-
-afterEach(() => {
- cmsConfig.template = originalTemplateName
- delete process.env.NEXT_PUBLIC_TEMPLATE
- delete process.env.CMS_TEMPLATE
- __resetTemplateRegistryForTests()
- const modulePath = path.join(templateFixturesPath, 'index.js')
- delete require.cache[modulePath]
- fs.rmSync(templateFixturesPath, { recursive: true, force: true })
-})
-
-describe('templateRegistry', () => {
- it('returns the default template from configuration', () => {
- const template = getActiveTemplate()
- expect(template.name).toBe('default')
- })
-
- it('prefers an explicit template name over configuration', () => {
- registerTemplate('runtime', runtimeTemplate)
- const template = getActiveTemplate({ name: 'runtime' })
- expect(template).toBe(runtimeTemplate)
- })
-
- it('allows environment variables to override the template', () => {
- registerTemplate('runtime', runtimeTemplate)
- process.env.NEXT_PUBLIC_TEMPLATE = 'runtime'
- const template = getActiveTemplate()
- expect(template).toBe(runtimeTemplate)
- })
-
- it('throws when config fallback is disabled and name is missing', () => {
- expect(() => resolveTemplateName(undefined, { fallbackToConfig: false })).toThrowError()
- })
-
- it('supports registering custom loaders', () => {
- const loaderTemplate: TemplateDefinition = {
- name: 'loader',
- pages: {
- home: () => null,
- },
- }
-
- registerTemplateLoader((name) => {
- return name === 'loader-template' ? loaderTemplate : null
- })
-
- const template = getActiveTemplate({ name: 'loader-template', fallbackToConfig: false })
- expect(template).toBe(loaderTemplate)
- })
-
- it('discovers templates from the filesystem', () => {
- fs.mkdirSync(templateFixturesPath, { recursive: true })
- const templateModulePath = path.join(templateFixturesPath, 'index.js')
- fs.writeFileSync(
- templateModulePath,
- "module.exports = { name: 'fs-template', pages: { home: () => null } };\n",
- 'utf8',
- )
-
- delete require.cache[templateModulePath]
- expect(fs.existsSync(templateModulePath)).toBe(true)
- expect(
- path.join(process.cwd(), 'src', 'modules', 'templates', 'fs-template', 'index.js')
- ).toBe(templateModulePath)
-
- cmsConfig.template = 'fs-template'
- const template = getActiveTemplate()
- expect(template.name).toBe('fs-template')
- })
-
- it('keeps the @cms facade compatible', async () => {
- const direct = getActiveTemplate()
-
- vi.stubGlobal('window', undefined)
- const { getActiveTemplate: legacyGetActiveTemplate } = await import('@cms')
- vi.unstubAllGlobals()
-
- const facade = legacyGetActiveTemplate()
- expect(facade).toEqual(direct)
- })
-
- it('lists registered template names including runtime additions', () => {
- registerTemplate('runtime', runtimeTemplate)
- const names = listRegisteredTemplateNames()
- expect(names).toContain('default')
- expect(names).toContain('runtime')
- })
-})
diff --git a/dashboard/tests/unit/vitest.config.ts b/dashboard/tests/unit/vitest.config.ts
index 5a7eb9c..3c1db85 100644
--- a/dashboard/tests/unit/vitest.config.ts
+++ b/dashboard/tests/unit/vitest.config.ts
@@ -21,7 +21,6 @@ export default defineConfig({
resolve: {
alias: {
'@components': path.resolve(__dirname, '..', '..', 'src', 'components'),
- '@cms': path.resolve(__dirname, '..', '..', 'src', 'lib', 'cms'),
'@i18n': path.resolve(__dirname, '..', '..', 'src', 'i18n'),
'@lib': path.resolve(__dirname, '..', '..', 'src', 'lib'),
'@types': path.resolve(__dirname, '..', '..', 'types'),
diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json
index 35f6602..7eec30b 100644
--- a/dashboard/tsconfig.json
+++ b/dashboard/tsconfig.json
@@ -17,9 +17,6 @@
"jsx": "react-jsx",
"baseUrl": ".", // 👈 根路径基准
"paths": {
- "@cms": ["src/lib/cms/index.ts"], // 👈 增加这行以兼容 import "@cms"
- "@cms/*": ["src/lib/cms/*"],
-
"@components/*": ["src/components/*"],
"@i18n/*": ["src/i18n/*"],
"@lib/*": ["src/lib/*"],