refactor: move docs site under ui and add make targets (#216)

This commit is contained in:
shenlan 2025-09-14 13:17:51 +08:00 committed by GitHub
parent 80c8913516
commit 6bfe8fbb3d
22 changed files with 484 additions and 6 deletions

View File

@ -7,9 +7,9 @@ NODE_MAJOR ?= 22
export PATH := $(GO_BIN):$(PATH)
.PHONY: install install-openresty install-redis install-postgresql install-pgvector install-zhparser init-db \
build build-server build-homepage build-panel build-dl \
start start-openresty start-server start-homepage start-panel start-dl \
stop stop-server stop-homepage stop-panel stop-dl stop-openresty restart
build build-server build-homepage build-panel build-dl build-docs \
start start-openresty start-server start-homepage start-panel start-dl start-docs \
stop stop-server stop-homepage stop-panel stop-dl stop-docs stop-openresty restart
# -----------------------------------------------------------------------------
# Dependency installation
@ -94,7 +94,7 @@ init-db:
# Build targets
# -----------------------------------------------------------------------------
build: build-cli build-server build-homepage build-panel build-dl
build: build-cli build-server build-homepage build-panel build-dl build-docs
build-cli:
$(MAKE) -C client build
@ -110,12 +110,15 @@ build-panel:
build-dl:
$(MAKE) -C ui/dl build
build-docs:
$(MAKE) -C ui/docs build
# -----------------------------------------------------------------------------
# Run targets
# -----------------------------------------------------------------------------
start: start-openresty start-server start-homepage start-panel start-dl
start: start-openresty start-server start-homepage start-panel start-dl start-docs
start-server:
$(MAKE) -C server start
@ -128,8 +131,11 @@ start-panel:
start-dl:
$(MAKE) -C ui/dl start
start-docs:
$(MAKE) -C ui/docs start
stop: stop-server stop-homepage stop-panel stop-dl stop-openresty
stop: stop-server stop-homepage stop-panel stop-dl stop-docs stop-openresty
stop-server:
$(MAKE) -C server stop
@ -142,6 +148,9 @@ stop-panel:
stop-dl:
$(MAKE) -C ui/dl stop
stop-docs:
$(MAKE) -C ui/docs stop
start-openresty:
ifeq ($(OS),Darwin)

View File

@ -0,0 +1,44 @@
name: Build and Deploy Docs
on:
push:
paths:
- 'docs/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install deps
run: cd docs && npm ci
- name: Import chapters
run: cd docs && node scripts/import-from-chapters.js
- name: Build site
run: cd docs && npm run build
- name: Generate PDFs
run: |
sudo apt-get update
sudo apt-get install -y pandoc wkhtmltopdf rsync
cd docs/out
mkdir -p pdf
find . -name index.html -type f | while read file; do
slug=$(dirname "$file")
[ "$slug" = "." ] && slug="index"
wkhtmltopdf "$file" "pdf/${slug}.pdf"
done
- name: Deploy
env:
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
SSH_KEY: ${{ secrets.SSH_KEY }}
WEB_ROOT: ${{ secrets.WEB_ROOT }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
rsync -avz -e "ssh -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no" docs/out/ $SSH_USER@$SSH_HOST:$WEB_ROOT

5
ui/docs/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.next
out
.DS_Store
*.log

88
ui/docs/Makefile Normal file
View File

@ -0,0 +1,88 @@
SHELL := /bin/bash
NODE_VERSION := $(shell node -v 2>/dev/null || echo "Not Found")
YARN := $(shell command -v yarn 2>/dev/null)
MAGICK := $(shell command -v magick 2>/dev/null || command -v convert 2>/dev/null)
OS := $(shell uname -s)
.PHONY: init dev build export clean info icon start stop restart test
icon:
@echo "🎨 Generating favicon and icon images..."
@if [ -z "$(MAGICK)" ]; then \
echo "❌ ImageMagick not found."; \
if [ "$(OS)" = "Darwin" ]; then \
echo "👉 Try: brew install imagemagick"; \
elif [ -f /etc/debian_version ]; then \
echo "👉 Try: sudo apt install imagemagick"; \
elif [ -f /etc/redhat-release ]; then \
echo "👉 Try: sudo dnf install imagemagick"; \
fi; \
exit 1; \
fi
@mkdir -p public/icons
@$(MAGICK) ../logo.png -resize 32x32 public/icons/cloudnative_32.png
@$(MAGICK) ../logo.png -resize 64x64 -background none -define icon:auto-resize=64,48,32,16 public/favicon.ico
@echo "✅ Icons generated successfully."
init:
@echo "🔧 Installing dependencies for docs..."
@if [ -z "$(YARN)" ]; then \
echo "⚠️ Yarn not found. Attempting to install..."; \
if [ "$(OS)" = "Darwin" ]; then \
if command -v brew >/dev/null 2>&1; then \
brew install yarn; \
else \
echo "❌ Homebrew not found. Please install Yarn manually."; exit 1; \
fi; \
elif [ -f /etc/debian_version ]; then \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list && \
sudo apt update && sudo apt install -y yarn; \
elif [ -f /etc/redhat-release ]; then \
curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo && \
sudo yum install -y yarn; \
else \
echo "❌ Unsupported OS. Please install Yarn manually."; exit 1; \
fi; \
fi
yarn install
dev:
@echo "🚀 Starting Next.js dev server (docs)..."
yarn next dev -p 3003
start:
@echo "🚀 Starting Next.js dev server (docs) in background..."
@nohup yarn next dev -p 3003 >/tmp/docs.log 2>&1 & echo $$! > docs.pid
stop:
@echo "🛑 Stopping Next.js dev server (docs)..."
@if [ -f docs.pid ]; then \
kill `cat docs.pid` >/dev/null 2>&1 || true; \
rm docs.pid; \
else \
echo "No running server"; \
fi
restart: stop start
test:
@echo "🔍 Running tests..."
@yarn test || echo "No tests configured"
build: init
yarn config set npmRegistryServer https://registry.npmmirror.com
@echo "🔨 Building docs..."
yarn next build
export:
@echo "📦 Exporting docs static site to ./out ..."
yarn next export
clean:
@echo "🧹 Cleaning .next and out directories..."
rm -rf .next out
info:
@echo "🧾 Node.js version: $(NODE_VERSION)"

12
ui/docs/README.md Normal file
View File

@ -0,0 +1,12 @@
# docs.svc.plus
Static documentation site built with Next.js, TailwindCSS and Contentlayer.
## Scripts
- `npm run build` build static site
- `node scripts/import-from-chapters.js` sync markdown from svc-design/documents
## Deployment
GitHub Actions builds the site, generates PDFs with pandoc/wkhtmltopdf and deploys via rsync. See `.github/workflows/build-deploy.yml`.
Nginx config: `nginx/docs.svc.plus.conf`

View File

@ -0,0 +1,33 @@
import { allDocs } from 'contentlayer/generated'
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { useReadingProgress } from '../../components/Progress'
export function generateStaticParams() {
return allDocs.map((doc) => ({ slug: doc.slug.split('/') }))
}
export default function DocPage({ params }: { params: { slug: string[] } }) {
const slug = params.slug.join('/')
const doc = allDocs.find((d) => d.slug === slug)
useReadingProgress(slug)
if (!doc) return notFound()
const docs = allDocs.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
const index = docs.findIndex((d) => d.slug === slug)
const prev = docs[index - 1]
const next = docs[index + 1]
const pdfUrl = `/pdf/${slug}.pdf`
return (
<article>
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.body.html }} />
<div className="mt-8 flex justify-between">
{prev ? <Link href={prev.url}> {prev.title}</Link> : <span />}
{next ? <Link href={next.url}>{next.title} </Link> : <span />}
</div>
<div className="mt-4">
<a href={pdfUrl} target="_blank" rel="noopener">Download PDF</a>
</div>
</article>
)
}

24
ui/docs/app/layout.tsx Normal file
View File

@ -0,0 +1,24 @@
import '../styles/globals.css'
import { Sidebar } from '../components/Sidebar'
import { TopScroller } from '../components/TopScroller'
import { Footer } from '../components/Footer'
export const metadata = {
title: 'SVC Docs',
description: 'svc.plus documents',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body className="flex">
<Sidebar />
<div className="flex-1 flex flex-col min-h-screen">
<TopScroller />
<main className="flex-1 w-full px-4 py-6 prose mx-auto">{children}</main>
<Footer />
</div>
</body>
</html>
)
}

18
ui/docs/app/page.tsx Normal file
View File

@ -0,0 +1,18 @@
import Link from 'next/link'
import { allDocs } from 'contentlayer/generated'
export default function Home() {
const docs = allDocs.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
return (
<div>
<h1>SVC Docs</h1>
<ul>
{docs.map((doc) => (
<li key={doc.slug}>
<Link href={doc.url}>{doc.title}</Link>
</li>
))}
</ul>
</div>
)
}

View File

@ -0,0 +1,7 @@
export function Footer() {
return (
<footer className="text-center py-4 text-sm text-gray-500">
Powered by <a href="https://svc.plus" className="underline">svc.plus</a> & AI <a href="https://docs.svc.plus" className="underline">https://docs.svc.plus</a>
</footer>
)
}

View File

@ -0,0 +1,17 @@
'use client'
import { useEffect } from 'react'
export function useReadingProgress(key: string) {
useEffect(() => {
const saved = localStorage.getItem(`progress:${key}`)
if (saved) {
window.scrollTo(0, parseInt(saved, 10))
}
const handler = () => {
localStorage.setItem(`progress:${key}`, String(window.scrollY))
}
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [key])
}

View File

@ -0,0 +1,31 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { allDocs } from 'contentlayer/generated'
export function Sidebar() {
const pathname = usePathname()
const docs = allDocs.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
return (
<aside className="w-60 border-r hidden md:block overflow-y-auto h-screen sticky top-0 p-4">
<nav>
<ul className="space-y-2">
{docs.map((doc) => {
const active = pathname === doc.url
return (
<li key={doc.slug}>
<Link
href={doc.url}
className={active ? 'text-blue-600 font-bold' : 'text-gray-700'}
>
{doc.title}
</Link>
</li>
)
})}
</ul>
</nav>
</aside>
)
}

View File

@ -0,0 +1,31 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { allDocs } from 'contentlayer/generated'
import { useRef, useEffect } from 'react'
export function TopScroller() {
const pathname = usePathname()
const ref = useRef<HTMLDivElement>(null)
const docs = allDocs.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
useEffect(() => {
const active = ref.current?.querySelector('a.active') as HTMLElement
active?.scrollIntoView({ inline: 'center' })
}, [pathname])
return (
<div ref={ref} className="flex overflow-x-auto space-x-4 p-2 border-b bg-white sticky top-0 z-10">
{docs.map((doc) => (
<Link
key={doc.slug}
href={doc.url}
className={`whitespace-nowrap px-2 ${pathname === doc.url ? 'active text-blue-600 font-semibold' : ''}`}
>
{doc.title}
</Link>
))}
</div>
)
}

0
ui/docs/content/.gitkeep Normal file
View File

View File

@ -0,0 +1,33 @@
import { defineDocumentType, makeSource } from 'contentlayer/source-files'
import rehypeAutolink from 'rehype-autolink-headings'
import rehypeSlug from 'rehype-slug'
import remarkGfm from 'remark-gfm'
export const Doc = defineDocumentType(() => ({
name: 'Doc',
filePathPattern: `**/*.md`,
contentType: 'markdown',
fields: {
title: { type: 'string', required: true },
order: { type: 'number', required: false },
},
computedFields: {
slug: {
type: 'string',
resolve: (doc) => doc._raw.flattenedPath,
},
url: {
type: 'string',
resolve: (doc) => `/${doc._raw.flattenedPath}`,
},
},
}))
export default makeSource({
contentDirPath: 'content',
documentTypes: [Doc],
markdown: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypeSlug, [rehypeAutolink, { behavior: 'wrap' }]],
},
})

6
ui/docs/next.config.js Normal file
View File

@ -0,0 +1,6 @@
const { withContentlayer } = require('next-contentlayer')
module.exports = withContentlayer({
output: 'export',
images: { unoptimized: true },
})

View File

@ -0,0 +1,14 @@
server {
listen 80;
server_name docs.svc.plus;
root /var/www/docs.svc.plus;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|pdf)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri $uri/ =404;
}
}

29
ui/docs/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "svc-docs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "contentlayer build && next build",
"start": "next start",
"lint": "eslint .",
"test": "echo 'no tests yet'"
},
"dependencies": {
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.16",
"contentlayer": "^0.3.4",
"next": "^14.1.0",
"next-contentlayer": "^0.3.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rehype-autolink-headings": "^7.1.1",
"rehype-slug": "^6.1.0",
"remark-gfm": "^3.0.1",
"tailwindcss": "^3.4.1"
},
"devDependencies": {
"postcss": "^8.4.31",
"typescript": "^5.3.3"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,27 @@
import { execSync } from 'child_process'
import { mkdtempSync, readdirSync, copyFileSync, existsSync, rmSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
const repo = 'https://github.com/svc-design/documents'
const include = process.env.INCLUDE ? process.env.INCLUDE.split(',') : []
const exclude = process.env.EXCLUDE ? process.env.EXCLUDE.split(',') : []
const tmp = mkdtempSync(join(tmpdir(), 'documents-'))
execSync(`git clone --depth 1 ${repo} ${tmp}`, { stdio: 'inherit' })
const targets = readdirSync(tmp)
for (const dir of targets) {
if (include.length && !include.includes(dir)) continue
if (exclude.includes(dir)) continue
const chapters = join(tmp, dir, 'CN', 'chapters')
if (!existsSync(chapters)) continue
const files = readdirSync(chapters).filter((f) => f.endsWith('.md'))
for (const file of files) {
const src = join(chapters, file)
const dest = join('content', dir, file)
execSync(`mkdir -p ${join('content', dir)}`)
copyFileSync(src, dest)
}
}
rmSync(tmp, { recursive: true, force: true })

View File

@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-white text-gray-900;
}

View File

@ -0,0 +1,12 @@
import type { Config } from 'tailwindcss'
import typography from '@tailwindcss/typography'
const config: Config = {
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './content/**/*.md'],
theme: {
extend: {},
},
plugins: [typography],
}
export default config

25
ui/docs/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".contentlayer/generated"
],
"exclude": ["node_modules"]
}