Remove CMS extension system (#706)

This commit is contained in:
shenlan 2025-12-02 09:50:58 +08:00 committed by GitHub
parent 1bdaa273e1
commit ea6dd5cf14
53 changed files with 294 additions and 1019 deletions

View File

@ -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"),

View File

@ -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 (
<LanguageProvider>
<UserProvider>{children}</UserProvider>
<UserProvider>
<ThemeProvider>{children}</ThemeProvider>
</UserProvider>
</LanguageProvider>
)
for (const Provider of extensionProviders) {
tree = <Provider>{tree}</Provider>
}
return <ThemeProvider>{tree}</ThemeProvider>
}

View File

@ -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'

View File

@ -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 (
<html {...htmlAttributes}>
<body className={bodyClassName}>
<ThemeProvider>
<AppProviders>{content}</AppProviders>
</ThemeProvider>
<AppProviders>{children}</AppProviders>
</body>
</html>
)

View File

@ -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: {

View File

@ -1,4 +1,4 @@
import { getHomepagePosts } from '@cms/content'
import { getHomepagePosts } from '@lib/marketingContent'
import CommunityFeed from './CommunityFeed'
export default async function CommunityFeedServer() {

View File

@ -1,6 +1,6 @@
import ContactPanelClient from './ContactPanelClient'
import { getContactPanelContent } from '@cms/content'
import { getContactPanelContent } from '@lib/marketingContent'
type ContactPanelProps = {
className?: string

View File

@ -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'

View File

@ -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() {

View File

@ -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'

View File

@ -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'

View File

@ -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()

View File

@ -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'

View File

@ -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

View File

@ -1 +0,0 @@
export { collectExtensionProviders } from './extensionRuntime'

View File

@ -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<string, ContentSourceConfig>
}
}
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',
},
},
},
}

View File

@ -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<ContentSourceState[]>([])
export function MarkdownContentProvider({ children }: { children: ReactNode }) {
const sources = useMemo(() => {
return listContentSourcesMetadata()
}, [])
return <MarkdownContentContext.Provider value={sources}>{children}</MarkdownContentContext.Provider>
}
export function useMarkdownSources() {
return useContext(MarkdownContentContext)
}

View File

@ -1,3 +0,0 @@
# Getting Started
Welcome to the docs! This is a placeholder document.

View File

@ -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<HeroContent> {
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<HeroSolution[]> {
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<HomepagePost[]> {
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<SidebarSection[]> {
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<ContactPanelContent | undefined> {
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<string | undefined> {
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
}

View File

@ -1,10 +0,0 @@
---
title: 欢迎支持
type: info
order: 4
icon: star
description: 欢迎支持关注 Star
ctaLabel: 访问 GitHub
ctaHref: https://github.com/CloudNativeSuite
---
点击链接访问 CloudNativeSuite GitHub欢迎支持关注 Star获取更多项目动态。

View File

@ -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**

View File

@ -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 社区小助手,获取最新活动信息并加入兴趣小组。

View File

@ -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 官方公众号,解锁上云实践案例与专家分享。

View File

@ -1,4 +0,0 @@
---
title: 保持联系
subtitle: 扫码关注或加入社区,获取最新产品动态与支持。
---

View File

@ -1,16 +0,0 @@
---
eyebrow: 云原生套件
title: 构建一体化的云原生工具集
subtitle: 将资产管理、访问控制、可观测与自动化工作流整合到一个响应迅速的体验里,帮助团队高效落地治理策略。
primaryCtaLabel: 立即体验
primaryCtaHref: /register
secondaryCtaLabel: 产品文档
secondaryCtaHref: /docs
highlights:
- 跨集群纳管与多云环境统一治理
- 以策略为核心的安全与合规编排
- 数据驱动的可观测与成本分析
- 场景化模板快速对接业务流程
---
XControl 采用模块化架构设计,可在保持核心稳定的前提下按需引入观测、身份、编排等能力包。通过开放 API 与事件流,您可以轻松连接现有的 DevOps 工具链,让业务交付与平台治理协同运转。

View File

@ -1,11 +0,0 @@
---
title: 社区巡回沙龙启动:实践分享与产品路线解读
author: 社区团队
date: 2024-07-12
readingTime: 3 分钟
tags:
- 社区活动
- 生态
---
今年的巡回沙龙将覆盖八个城市,围绕平台工程、自动化安全与可观测进行实战分享。我们还准备了产品路线解读与开放问答,欢迎报名参与,与来自不同行业的伙伴一起交流经验。

View File

@ -1,12 +0,0 @@
---
title: 观测即服务:从日志到业务指标的一体化洞察
author: 平台可观测团队
date: 2024-07-30
readingTime: 5 分钟
tags:
- 最佳实践
- 观测
- 数据分析
---
针对多租户环境,我们重构了指标与日志的归档体系,实现了秒级可视化。通过业务域建模,团队可将技术指标与业务目标关联,快速定位影响用户体验的关键链路。

View File

@ -1,13 +0,0 @@
---
title: 版本 1.8 正式发布:策略联动与可观测双升级
author: XControl 产品团队
date: 2024-08-15
readingTime: 8 分钟
tags:
- 发布公告
- 策略中心
- 可观测
excerpt: 全新策略联动引擎与跨集群指标联邦能力上线,为大规模云原生团队带来统一的治理体验。
---
我们针对复杂多集群环境推出了「策略联动」功能,可将身份、资源与安全策略编织成全局一致的执行链路。与此同时,指标联邦能力支持将多源观测数据按团队、业务或环境维度聚合分析,让治理效果实时可视化。

View File

@ -1,10 +0,0 @@
---
title: 社区热议
ctaLabel: 加入社区
ctaHref: https://example.com/community
order: 1
---
- [平台工程读书会](#) —— 每周聚焦云原生治理案例
- [Slack 交流群](#) —— 与 2000+ 从业者实时交流
- [GitHub Issues](#) —— 提交需求与反馈问题

View File

@ -1,8 +0,0 @@
---
title: 推荐资源
order: 3
---
1. [平台上线指南](#) — 分阶段部署 XControl 的最佳实践
2. [安全策略手册](#) — 常见合规基线的策略模板
3. [观测数据白皮书](#) — 如何构建统一的指标与日志视图

View File

@ -1,12 +0,0 @@
---
title: 热门标签
layout: tags
tags:
- 策略驱动
- 云原生安全
- 平台工程
- 观测洞察
- 成本治理
- 最佳实践
order: 4
---

View File

@ -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 模型统一到一个工作台,为多云环境提供自助式交付与集中治理。

View File

@ -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 以策略即代码为核心,为云原生基础设施提供可观测、可治理、可审计的统一控制平面。

View File

@ -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 通过语义化检索与时序分析,实现跨环境的可观察性汇聚与智能洞察。

View File

@ -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 通过软件定义的网络加速技术,为实时互动、音视频与数据分发提供稳定的全球链路。

View File

@ -1 +0,0 @@
export * from './homepage'

View File

@ -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,
}
}

View File

@ -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,
}))
}

View File

@ -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<ThemeName, CmsTheme> = {
default: defaultTheme,
}
const extensionRegistry: Record<ExtensionName, CmsExtension> = {
'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 }
}

View File

@ -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 (
<>
<Navbar />
<div className="pt-[var(--app-shell-nav-offset)]">
{children}
<Footer />
</div>
<AskAIButton />
</>
)
}
export const appShellExtension: CmsExtension = {
name: 'app-shell',
Layout: AppShellLayout,
}

View File

@ -1,7 +0,0 @@
import type { CmsExtension } from '../types'
import { MarkdownContentProvider } from '../content/ContentContext'
export const markdownSyncExtension: CmsExtension = {
name: 'markdown-sync',
providers: [MarkdownContentProvider],
}

View File

@ -1,14 +0,0 @@
export {
applyExtensionLayouts,
collectExtensionProviders,
getActiveExtensions,
getActiveTheme,
} from './extensionRuntime'
export {
getActiveTemplate,
listRegisteredTemplateNames,
registerTemplate,
registerTemplateLoader,
} from '@modules/templateRegistry'
export { listContentSourcesMetadata } from './contentSources'

View File

@ -1,7 +0,0 @@
{
"name": "@cms",
"version": "1.0.0",
"type": "module",
"main": "index.ts"
}

View File

@ -1,2 +0,0 @@
export { default } from '@modules/templates/default'
export { defaultHomeLayoutConfig } from './config'

View File

@ -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

View File

@ -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<TemplateRenderProps<HomePageTemplateSlots>>
[key: string]: ComponentType<any> | undefined
}
}
export interface CmsTheme {
name: string
htmlAttributes?: Record<string, string>
bodyClassName?: string
Provider?: ComponentType<{ children: ReactNode }>
}
export interface CmsExtension {
name: string
providers?: ComponentType<{ children: ReactNode }>[]
Layout?: ComponentType<{ children: ReactNode }>
}

View File

@ -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:
'<p>XControl 采用模块化架构设计,可在保持核心稳定的前提下按需引入观测、身份、编排等能力包。通过开放 API 与事件流,您可以轻松连接现有的 DevOps 工具链,让业务交付与平台治理协同运转。</p>',
}
const HERO_SOLUTIONS: HeroSolution[] = [
{
slug: 'xcloudflow',
title: 'XCloudFlow',
tagline: '多云 IaC',
description: '通过声明式模型统一编排多云基础设施,自动化落地资源策略与合规标准。',
features: ['跨云资源蓝图与参数化交付', 'GitOps 工作流驱动基础设施变更', '内置审批、审计保障合规'],
bodyHtml:
'<p>XCloudFlow 将 Terraform、Pulumi 等主流 IaC 模型统一到一个工作台,为多云环境提供自助式交付与集中治理。</p>',
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:
'<p>XScopeHub 通过语义化检索与时序分析,实现跨环境的可观察性汇聚与智能洞察。</p>',
primaryCtaLabel: '立刻体验',
primaryCtaHref: '/demo?product=xscopehub',
secondaryCtaLabel: '下载链接',
secondaryCtaHref: '/download?product=xscopehub',
tertiaryCtaLabel: '文档链接',
tertiaryCtaHref: '/docs/xscopehub',
},
{
slug: 'xstream',
title: 'XStream',
tagline: '网络加速器',
description: '按需构建全球传输网络,保障跨地域应用与数据同步的稳定低时延体验。',
features: ['动态最优路径与带宽调度', '内置零信任安全与访问控制', '对接主流 CDN 与边缘节点'],
bodyHtml:
'<p>XStream 通过软件定义的网络加速技术,为实时互动、音视频与数据分发提供稳定的全球链路。</p>',
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:
'<p>欢迎提交您的使用反馈或功能建议,我们会尽快回复。<br/>如需寻求技术帮助,可联系:<strong>manbuzhe2008@gmail.com</strong></p>',
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<HeroContent> {
return HERO_CONTENT
}
export async function getHeroSolutions(): Promise<HeroSolution[]> {
return HERO_SOLUTIONS
}
export async function getHomepagePosts(): Promise<HomepagePost[]> {
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<ContactPanelContent | undefined> {
if (!CONTACT_PANEL.items.length) {
return undefined
}
return CONTACT_PANEL
}

View File

@ -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<string, TemplateDefinition> = {
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 {

View File

@ -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' }],
},
}

View File

@ -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'

View File

@ -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')
})
})

View File

@ -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'),

View File

@ -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/*"],