From ea6dd5cf14ca974aafb95378deb16552ea9f87ea Mon Sep 17 00:00:00 2001 From: shenlan Date: Tue, 2 Dec 2025 09:50:58 +0800 Subject: [PATCH] Remove CMS extension system (#706) --- dashboard/next.config.mjs | 1 - dashboard/src/app/AppProviders.tsx | 15 +- dashboard/src/app/blog/page.tsx | 2 +- dashboard/src/app/layout.tsx | 15 +- .../src/components/home/CommunityFeed.tsx | 2 +- .../components/home/CommunityFeedServer.tsx | 2 +- .../src/components/home/ContactPanel.tsx | 2 +- .../components/home/ContactPanelClient.tsx | 2 +- dashboard/src/components/home/HeroBanner.tsx | 2 +- .../src/components/home/HeroBannerClient.tsx | 2 +- .../src/components/home/HeroProductTabs.tsx | 2 +- .../src/components/home/ProductMatrix.tsx | 2 +- .../components/home/ProductMatrixClient.tsx | 2 +- dashboard/src/components/home/SidebarCard.tsx | 2 +- dashboard/src/lib/cms/client.ts | 1 - dashboard/src/lib/cms/config.ts | 53 --- .../src/lib/cms/content/ContentContext.tsx | 26 -- .../src/lib/cms/content/getting-started.md | 3 - dashboard/src/lib/cms/content/homepage.ts | 359 ------------------ .../homepage/contact/items/github-star.md | 10 - .../content/homepage/contact/items/support.md | 12 - .../homepage/contact/items/wechat-group.md | 9 - .../homepage/contact/items/wechat-official.md | 9 - .../lib/cms/content/homepage/contact/panel.md | 4 - .../src/lib/cms/content/homepage/hero.md | 16 - .../posts/2024-07-12-community-events.md | 11 - .../posts/2024-07-30-observability-insight.md | 12 - .../posts/2024-08-15-release-notes.md | 13 - .../cms/content/homepage/sidebar/community.md | 10 - .../cms/content/homepage/sidebar/resources.md | 8 - .../lib/cms/content/homepage/sidebar/tags.md | 12 - .../content/homepage/solutions/xcloudflow.md | 17 - .../content/homepage/solutions/xcontrol.md | 17 - .../content/homepage/solutions/xscopehub.md | 17 - .../cms/content/homepage/solutions/xstream.md | 17 - dashboard/src/lib/cms/content/index.ts | 1 - dashboard/src/lib/cms/content/source.ts | 45 --- dashboard/src/lib/cms/contentSources.ts | 10 - dashboard/src/lib/cms/extensionRuntime.ts | 54 --- dashboard/src/lib/cms/extensions/appShell.tsx | 25 -- .../src/lib/cms/extensions/markdownSync.tsx | 7 - dashboard/src/lib/cms/index.ts | 14 - dashboard/src/lib/cms/package.json | 7 - .../src/lib/cms/templates/default/index.tsx | 2 - .../src/lib/cms/themes/default/index.tsx | 16 - dashboard/src/lib/cms/types.ts | 24 -- dashboard/src/lib/marketingContent.ts | 269 +++++++++++++ dashboard/src/modules/templateRegistry.ts | 10 +- .../templates/default/config.ts | 6 +- .../src/modules/templates/default/index.tsx | 2 +- dashboard/tests/unit/templateRegistry.test.ts | 128 ------- dashboard/tests/unit/vitest.config.ts | 1 - dashboard/tsconfig.json | 3 - 53 files changed, 294 insertions(+), 1019 deletions(-) delete mode 100644 dashboard/src/lib/cms/client.ts delete mode 100644 dashboard/src/lib/cms/config.ts delete mode 100644 dashboard/src/lib/cms/content/ContentContext.tsx delete mode 100644 dashboard/src/lib/cms/content/getting-started.md delete mode 100644 dashboard/src/lib/cms/content/homepage.ts delete mode 100644 dashboard/src/lib/cms/content/homepage/contact/items/github-star.md delete mode 100644 dashboard/src/lib/cms/content/homepage/contact/items/support.md delete mode 100644 dashboard/src/lib/cms/content/homepage/contact/items/wechat-group.md delete mode 100644 dashboard/src/lib/cms/content/homepage/contact/items/wechat-official.md delete mode 100644 dashboard/src/lib/cms/content/homepage/contact/panel.md delete mode 100644 dashboard/src/lib/cms/content/homepage/hero.md delete mode 100644 dashboard/src/lib/cms/content/homepage/posts/2024-07-12-community-events.md delete mode 100644 dashboard/src/lib/cms/content/homepage/posts/2024-07-30-observability-insight.md delete mode 100644 dashboard/src/lib/cms/content/homepage/posts/2024-08-15-release-notes.md delete mode 100644 dashboard/src/lib/cms/content/homepage/sidebar/community.md delete mode 100644 dashboard/src/lib/cms/content/homepage/sidebar/resources.md delete mode 100644 dashboard/src/lib/cms/content/homepage/sidebar/tags.md delete mode 100644 dashboard/src/lib/cms/content/homepage/solutions/xcloudflow.md delete mode 100644 dashboard/src/lib/cms/content/homepage/solutions/xcontrol.md delete mode 100644 dashboard/src/lib/cms/content/homepage/solutions/xscopehub.md delete mode 100644 dashboard/src/lib/cms/content/homepage/solutions/xstream.md delete mode 100644 dashboard/src/lib/cms/content/index.ts delete mode 100644 dashboard/src/lib/cms/content/source.ts delete mode 100644 dashboard/src/lib/cms/contentSources.ts delete mode 100644 dashboard/src/lib/cms/extensionRuntime.ts delete mode 100644 dashboard/src/lib/cms/extensions/appShell.tsx delete mode 100644 dashboard/src/lib/cms/extensions/markdownSync.tsx delete mode 100644 dashboard/src/lib/cms/index.ts delete mode 100644 dashboard/src/lib/cms/package.json delete mode 100644 dashboard/src/lib/cms/templates/default/index.tsx delete mode 100644 dashboard/src/lib/cms/themes/default/index.tsx delete mode 100644 dashboard/src/lib/cms/types.ts create mode 100644 dashboard/src/lib/marketingContent.ts rename dashboard/src/{lib/cms => modules}/templates/default/config.ts (86%) delete mode 100644 dashboard/tests/unit/templateRegistry.test.ts 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/*"],