refactor(docs): reimplement documentation page with Pigsty-style layout

- Replaced docs landing page with a direct redirect to content
- Implemented 3-column layout: Sticky Sidebar, Main Content, Right Metadata
- added DocsSidebar with collapsible categories
- Added Feedback component for user sentiment
- Updated sync script to pull from knowledge/docs
- Refactored doc version page styles
This commit is contained in:
Haitao Pan 2026-01-26 16:46:17 +08:00
parent 75af007c12
commit 4623324622
10 changed files with 333 additions and 157 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ models/
.DS_Store
ui/.DS_Store
ui/*/.DS_Store
src/content/
# Node.js dependencies
node_modules/

View File

@ -0,0 +1,63 @@
# Documentation Page Redesign Specification
## 1. Overview
Refactor the `/docs` section of `console.svc.plus` to strictly load content from `cloud-neutral-workshop/knowledge/docs`. The UI will be redesigned to match the [Pigsty Documentation](https://pigsty.cc/docs/) layout while maintaining the existing `console.svc.plus` visual identity (Tailwind tokens, fonts, and colors).
## 2. Layout Structure
The layout will consist of a fixed header and a three-column content area (on desktop).
### 2.1. Global Header (Modified)
- **Content**: Logo, Main Nav (Home, Docs, Blog, etc.), Search Bar, GitHub Link, User Profile.
- **Additions**:
- **Version Selector**: Dropdown to switch between documentation versions (if available).
- **Search**: Integrated Algolia/Command-K search bar.
### 2.2. Left Sidebar (Navigation)
- **Behavior**: Sticky, independently scrollable.
- **Content**:
- Tree-view navigation structure matching the directory structure of `knowledge/docs`.
- **Expandable/Collapsible**: Categories should be collapsible folders.
- **Active State**: clear visual indicator for current page.
### 2.3. Main Content Area (Center)
- **Typography**: Optimized for long-form reading (prose, decent line-height).
- **Elements**:
- Breadcrumbs at the top.
- H1 Title.
- Last updated timestamp.
- Content rendered via MDX/Markdown.
- **Feedback Section (Footer)**: "Is this page helpful?" (Yes/No buttons) similar to Pigsty.
- Prev/Next page navigation links.
### 2.4. Right Sidebar (Table of Contents & Meta)
- **Behavior**: Sticky, hidden on mobile.
- **Content**:
- **On this Page**: Auto-generated TOC from H2/H3 headers.
- **Metadata**:
- "Module": Tags or categorization pills.
- "Edit this page": Link to GitHub source.
- "Contributors": List of contributors (optional).
## 3. Visual Style & Theming
- **Colors**: Use existing `brand-*` and `surface-*` tokens from `console.svc.plus`.
- Sidebar Background: `bg-surface-muted` or `bg-background` with right border.
- Active Link: `text-primary` with `bg-primary/10` background.
- **Responsiveness**:
- **Mobile**: Hambergur menu to open Sidebar. TOC hidden or moved to top of content (Accordion).
## 4. Implementation Plan
### 4.1. Data Source
- Ensure `scripts/sync-doc-content.sh` pulls specifically from `knowledge/docs`.
- Update `contentlayer` or `next-mdx-remote` configuration to handle the nested structure of `docs/`.
### 4.2. Components
1. **`DocsLayout`**: Wrapper for the 3-column grid.
2. **`SidebarTree`**: Recursive component for navigation.
3. **`TOC`**: Component to parse headings and display right sidebar.
4. **`FeedbackWidget`**: Simple interactivity for user sentiment.
## 5. Reference
- **Inspiration**: [Pigsty Docs](https://pigsty.cc/docs/)
- **Theme**: Cloud-Neutral Toolkit (Dark/Light mode support).

39
scripts/sync-doc-content.sh Executable file
View File

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

View File

@ -0,0 +1,74 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
import { ChevronRight, ChevronDown, FileText } from 'lucide-react'
import type { DocCollection } from './types'
interface DocsSidebarProps {
collections: DocCollection[]
}
export default function DocsSidebar({ collections }: DocsSidebarProps) {
const pathname = usePathname()
// Sort collections by title or defined order if any
const sortedCollections = [...collections].sort((a, b) => a.title.localeCompare(b.title))
return (
<aside className="sticky top-[64px] hidden h-[calc(100vh-64px)] w-64 shrink-0 overflow-y-auto border-r border-surface-border bg-background py-6 pl-8 pr-4 lg:block">
<nav className="space-y-6">
{sortedCollections.map((collection) => (
<SidebarGroup
key={collection.slug}
collection={collection}
activePath={pathname}
/>
))}
</nav>
</aside>
)
}
function SidebarGroup({ collection, activePath }: { collection: DocCollection; activePath: string }) {
const [isOpen, setIsOpen] = useState(true)
// Check if any child is active to auto-expand (optional, defaulted to true for now)
const isActive = collection.versions.some(v => activePath === `/docs/${collection.slug}/${v.slug}`)
return (
<div className="space-y-2">
<button
onClick={() => setIsOpen(!isOpen)}
className="flex w-full items-center justify-between text-sm font-semibold text-heading transistion hover:text-primary"
>
<span>{collection.title}</span>
{isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</button>
{isOpen && (
<ul className="space-y-1 border-l border-surface-border pl-4">
{collection.versions.map((version) => {
const href = `/docs/${collection.slug}/${version.slug}`
const isPageActive = activePath === href
return (
<li key={version.slug}>
<Link
href={href}
className={`block rounded-md px-2 py-1.5 text-sm transition-colors ${isPageActive
? 'bg-primary/10 text-primary font-medium'
: 'text-text-muted hover:text-heading hover:bg-surface-muted'
}`}
>
{version.title}
</Link>
</li>
)
})}
</ul>
)}
</div>
)
}

36
src/app/docs/Feedback.tsx Normal file
View File

@ -0,0 +1,36 @@
'use client'
import { useState } from 'react'
import { ThumbsUp, ThumbsDown } from 'lucide-react'
export default function Feedback() {
const [voted, setVoted] = useState<'yes' | 'no' | null>(null)
return (
<div className="mt-16 border-t border-surface-border pt-8">
<div className="flex flex-col gap-4">
<h3 className="text-lg font-semibold text-heading">Is this page helpful?</h3>
{voted === null ? (
<div className="flex gap-3">
<button
onClick={() => setVoted('yes')}
className="flex items-center gap-2 rounded-md border border-surface-border bg-surface px-4 py-2 text-sm font-medium text-text transition hover:border-primary hover:text-primary"
>
<ThumbsUp className="h-4 w-4" />
Yes
</button>
<button
onClick={() => setVoted('no')}
className="flex items-center gap-2 rounded-md border border-surface-border bg-surface px-4 py-2 text-sm font-medium text-text transition hover:border-danger hover:text-danger"
>
<ThumbsDown className="h-4 w-4" />
No
</button>
</div>
) : (
<p className="text-sm text-text-muted">Thanks for your feedback!</p>
)}
</div>
</div>
)
}

View File

@ -4,27 +4,31 @@ export const revalidate = false
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
import Breadcrumbs, { type Crumb } from '../../../../components/download/Breadcrumbs'
import DocArticle from '@/components/doc/DocArticle'
import DocMetaPanel from '@/components/doc/DocMetaPanel'
import DocVersionSwitcher from '@/components/doc/DocVersionSwitcher'
import Feedback from '../../Feedback'
import { getDocVersionParams, getDocVersion } from '../../resources.server'
import { isFeatureEnabled } from '@lib/featureToggles'
import Link from 'next/link'
import { ChevronRight } from 'lucide-react'
function buildBreadcrumbs(
slug: string,
docTitle: string,
version?: { label: string; slug: string },
): Crumb[] {
const crumbs: Crumb[] = [
{ label: 'Docs', href: '/docs' },
{ label: docTitle, href: `/docs/${slug}` },
]
if (version) {
const versionSlug = version.slug
crumbs.push({ label: version.label, href: `/docs/${slug}/${versionSlug}` })
}
return crumbs
// Simple Breadcrumbs Component inline (or could be separate)
function DocsBreadcrumbs({ items }: { items: { label: string; href: string }[] }) {
return (
<nav className="flex items-center gap-2 text-sm text-text-muted mb-6">
{items.map((item, index) => (
<div key={item.href} className="flex items-center gap-2">
{index > 0 && <ChevronRight className="h-4 w-4" />}
<Link
href={item.href}
className={`transition hover:text-primary ${index === items.length - 1 ? 'font-medium text-text' : ''}`}
>
{item.label}
</Link>
</div>
))}
</nav>
)
}
export const generateStaticParams = async () => {
@ -37,8 +41,13 @@ export const generateStaticParams = async () => {
export const dynamicParams = false
export const metadata: Metadata = {
title: 'Documentation',
export async function generateMetadata({ params }: { params: { collection: string; version: string } }): Promise<Metadata> {
const doc = await getDocVersion(params.collection, params.version)
if (!doc) return {}
return {
title: `${doc.version.title} - ${doc.collection.title} | Documentation`,
description: doc.version.description,
}
}
export default async function DocVersionPage({
@ -56,39 +65,52 @@ export default async function DocVersionPage({
}
const { collection, version } = doc
const breadcrumbs = buildBreadcrumbs(collection.slug, collection.title, version)
const breadcrumbs = [
{ label: 'Documentation', href: '/docs' },
{ label: collection.title, href: `/docs/${collection.slug}` },
{ label: version.title, href: `/docs/${collection.slug}/${version.slug}` },
]
return (
<main className="px-4 py-8 md:px-8">
<div className="mx-auto flex max-w-6xl flex-col gap-6">
<Breadcrumbs items={breadcrumbs} />
<section className="rounded-3xl border border-gray-200 bg-white p-6 shadow-sm">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-purple-600">{collection.title}</p>
<h1 className="text-3xl font-bold text-gray-900 md:text-4xl">{version.title}</h1>
<p className="mt-2 text-sm text-gray-600">{collection.description}</p>
</div>
<div className="flex flex-col items-start gap-3 text-sm text-gray-500 md:items-end">
<DocVersionSwitcher
collectionSlug={collection.slug}
versions={collection.versions.map((item) => ({ slug: item.slug, label: item.label }))}
activeSlug={version.slug}
/>
{version.updatedAt && <span suppressHydrationWarning>Updated {version.updatedAt}</span>}
</div>
</div>
</section>
<div className="flex gap-12 xl:gap-16">
{/* Center Content */}
<article className="min-w-0 flex-1">
<DocsBreadcrumbs items={breadcrumbs} />
<div className="grid gap-6 lg:grid-cols-[minmax(0,240px)_1fr]">
<div className="rounded-3xl border border-gray-200 bg-white p-5 shadow-sm">
<DocMetaPanel description={version.description} updatedAt={version.updatedAt} tags={version.tags} />
</div>
<div className="rounded-3xl border border-gray-200 bg-white p-6 shadow-sm">
<DocArticle content={version.content} />
</div>
<header className="mb-10 border-b border-surface-border pb-8">
<h1 className="text-3xl font-bold tracking-tight text-heading sm:text-4xl">{version.title}</h1>
{version.description && <p className="mt-4 text-lg text-text-muted">{version.description}</p>}
</header>
<div className="prose prose-slate max-w-none dark:prose-invert prose-headings:scroll-mt-20 prose-headings:font-semibold prose-a:text-primary prose-a:no-underline hover:prose-a:underline">
<DocArticle content={version.content} />
</div>
</div>
</main>
<Feedback />
</article>
{/* Right Sidebar */}
<aside className="hidden w-64 shrink-0 lg:block xl:w-72">
<div className="sticky top-[100px] space-y-8 border-l border-surface-border pl-6">
<div>
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-text-subtle">Metadata</h3>
<DocMetaPanel
description={undefined} // Description already shown in header
updatedAt={version.updatedAt}
tags={version.tags}
/>
</div>
{/* We could add TOC here later */}
{/*
<div>
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-text-subtle">On This Page</h3>
<TOC content={version.content} />
</div>
*/}
</div>
</aside>
</div>
)
}

21
src/app/docs/layout.tsx Normal file
View File

@ -0,0 +1,21 @@
import { getDocCollections } from './resources.server'
import DocsSidebar from './DocsSidebar'
import Navbar from '@components/Navbar'
import Footer from '@components/Footer'
export default async function DocsLayout({ children }: { children: React.ReactNode }) {
const collections = await getDocCollections()
return (
<div className="flex min-h-screen flex-col bg-background text-text">
<Navbar />
<div className="mx-auto flex w-full max-w-[1536px] items-start">
<DocsSidebar collections={collections} />
<main className="min-h-[calc(100vh-64px)] flex-1 overflow-x-hidden py-8 px-4 sm:px-8 lg:px-10">
{children}
</main>
</div>
<Footer />
</div>
)
}

View File

@ -1,63 +1,37 @@
export const dynamic = 'error'
export const revalidate = false
import { notFound } from 'next/navigation'
import type { DocCollection } from './types'
import { getDocResources } from './resources.server'
import { isFeatureEnabled } from '@lib/featureToggles'
import DocCollectionCard from './DocCollectionCard'
function formatMeta(resource: DocCollection) {
const parts: string[] = []
if (resource.updatedAt) {
parts.push('Updated')
}
if (resource.versions.length > 1) {
parts.push(`${resource.versions.length} versions`)
}
return parts.join(' • ')
}
import { redirect, notFound } from 'next/navigation'
import { getDocCollections } from './resources.server'
export default async function DocsHome() {
if (!isFeatureEnabled('appModules', '/docs')) {
notFound()
const collections = await getDocCollections()
if (collections.length === 0) {
return (
<div className="flex h-64 flex-col items-center justify-center rounded-lg border border-dashed border-surface-border bg-surface p-8 text-center">
<h3 className="text-lg font-semibold text-heading">No Documentation Found</h3>
<p className="max-w-md text-sm text-text-muted mt-2">
We couldn't find any documentation files. Please ensure content is synced to <code>src/content/doc</code>.
</p>
</div>
)
}
const manifest = await getDocResources()
const resources = [...manifest].sort((a, b) => {
const aTime = a.updatedAt ? Date.parse(a.updatedAt) : 0
const bTime = b.updatedAt ? Date.parse(b.updatedAt) : 0
return bTime - aTime
// Try to find a collection named 'index', 'intro', 'home', 'docs' or similar to prioritize
const priorityKeys = ['index', 'intro', 'introduction', 'home', 'docs', 'overview']
const sorted = [...collections].sort((a, b) => {
const aIndex = priorityKeys.indexOf(a.slug.toLowerCase())
const bIndex = priorityKeys.indexOf(b.slug.toLowerCase())
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex
if (aIndex !== -1) return -1
if (bIndex !== -1) return 1
return 0
})
return (
<main className="bg-brand-surface px-6 py-12 sm:px-8">
<div className="mx-auto flex max-w-6xl flex-col gap-10">
<header className="space-y-4 text-brand-heading">
<p className="text-xs font-semibold uppercase tracking-[0.32em] text-brand">Knowledge Base</p>
<h1 className="text-[32px] font-bold text-brand md:text-[36px]">Documentation Library</h1>
<p className="max-w-3xl text-sm text-brand-heading/80 md:text-base">
Browse curated implementation guides, architecture notes, and runbooks from dl.svc.plus. Select a resource to open
the focused reading workspace.
</p>
</header>
const firstCollection = sorted[0]
const firstVersion = firstCollection.versions[0]
<section>
{resources.length === 0 ? (
<div className="rounded-2xl border border-dashed border-brand-border bg-white p-10 text-center text-sm text-brand-heading/70">
Documentation resources are not available at the moment. Please check back later.
</div>
) : (
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
{resources.map((resource) => {
const meta = formatMeta(resource)
return <DocCollectionCard key={resource.slug} collection={resource} meta={meta} />
})}
</div>
)}
</section>
</div>
</main>
)
if (firstCollection && firstVersion) {
redirect(`/docs/${firstCollection.slug}/${firstVersion.slug}`)
}
notFound()
}

View File

@ -1,29 +0,0 @@
---
title: Observability Baseline
description: Establish a consistent telemetry surface before onboarding workloads.
updatedAt: 2024-11-05
tags:
- tracing
- metrics
- dashboards
collection: observability
collectionLabel: Observability
version: "2024 Q4"
versionSlug: overview
---
## Why this matters
Reliable dashboards and alerts depend on predictable signals. This baseline locks in a minimal telemetry contract so new services inherit the same trace attributes, metric names, and log keys.
### Core checklist
- Emit request, dependency, and queue spans with shared trace IDs.
- Forward deployment, region, and tenant labels with every metric.
- Normalize structured logs with `severity`, `service`, and `component` fields.
### Rollout tips
1. Start with staging namespaces and mirror traffic where possible.
2. Validate alerts on canary services before expanding coverage.
3. Keep a changelog in the runbook so teams can replay the rollout.

View File

@ -1,25 +0,0 @@
---
title: Zero Downtime Dashboards
description: Use shadow pipelines to publish dashboards without interrupting operators.
updatedAt: 2024-12-12
tags:
- dashboards
- releases
collection: observability
collectionLabel: Observability
version: Preview
versionSlug: zero-downtime
format: mdx
---
<Callout title="Guardrails" tone="warning">
Always keep the stable dashboard folder intact. Publish experimental panels into a shadow folder first.
</Callout>
<Steps title="Release path">
<li>Create a new folder such as `dashboards/shadow` and sync it to staging only.</li>
<li>Attach the same data sources as production but pin to canary namespaces.</li>
<li>After validation, promote the folder and archive the previous release.</li>
</Steps>
Teams can safely add complex visualizations without losing historical parity. The same pattern works for alert rule experiments.