diff --git a/Makefile b/Makefile index 4f8f914..36a771c 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/ui/docs/.github/workflows/build-deploy.yml b/ui/docs/.github/workflows/build-deploy.yml new file mode 100644 index 0000000..d9a4e5a --- /dev/null +++ b/ui/docs/.github/workflows/build-deploy.yml @@ -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 diff --git a/ui/docs/.gitignore b/ui/docs/.gitignore new file mode 100644 index 0000000..1c5ca90 --- /dev/null +++ b/ui/docs/.gitignore @@ -0,0 +1,5 @@ +node_modules +.next +out +.DS_Store +*.log diff --git a/ui/docs/Makefile b/ui/docs/Makefile new file mode 100644 index 0000000..39e858f --- /dev/null +++ b/ui/docs/Makefile @@ -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)" + diff --git a/ui/docs/README.md b/ui/docs/README.md new file mode 100644 index 0000000..1e5ea8b --- /dev/null +++ b/ui/docs/README.md @@ -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` diff --git a/ui/docs/app/[...slug]/page.tsx b/ui/docs/app/[...slug]/page.tsx new file mode 100644 index 0000000..808821a --- /dev/null +++ b/ui/docs/app/[...slug]/page.tsx @@ -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 ( +
+

{doc.title}

+
+
+ {prev ? โ† {prev.title} : } + {next ? {next.title} โ†’ : } +
+
+ Download PDF +
+
+ ) +} diff --git a/ui/docs/app/layout.tsx b/ui/docs/app/layout.tsx new file mode 100644 index 0000000..2413e30 --- /dev/null +++ b/ui/docs/app/layout.tsx @@ -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 ( + + + +
+ +
{children}
+
+
+ + + ) +} diff --git a/ui/docs/app/page.tsx b/ui/docs/app/page.tsx new file mode 100644 index 0000000..2631e6f --- /dev/null +++ b/ui/docs/app/page.tsx @@ -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 ( +
+

SVC Docs

+ +
+ ) +} diff --git a/ui/docs/components/Footer.tsx b/ui/docs/components/Footer.tsx new file mode 100644 index 0000000..d2d64da --- /dev/null +++ b/ui/docs/components/Footer.tsx @@ -0,0 +1,7 @@ +export function Footer() { + return ( + + ) +} diff --git a/ui/docs/components/Progress.ts b/ui/docs/components/Progress.ts new file mode 100644 index 0000000..c221857 --- /dev/null +++ b/ui/docs/components/Progress.ts @@ -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]) +} diff --git a/ui/docs/components/Sidebar.tsx b/ui/docs/components/Sidebar.tsx new file mode 100644 index 0000000..a6504d9 --- /dev/null +++ b/ui/docs/components/Sidebar.tsx @@ -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 ( + + ) +} diff --git a/ui/docs/components/TopScroller.tsx b/ui/docs/components/TopScroller.tsx new file mode 100644 index 0000000..747edae --- /dev/null +++ b/ui/docs/components/TopScroller.tsx @@ -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(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 ( +
+ {docs.map((doc) => ( + + {doc.title} + + ))} +
+ ) +} diff --git a/ui/docs/content/.gitkeep b/ui/docs/content/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ui/docs/contentlayer.config.ts b/ui/docs/contentlayer.config.ts new file mode 100644 index 0000000..f49c6b9 --- /dev/null +++ b/ui/docs/contentlayer.config.ts @@ -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' }]], + }, +}) diff --git a/ui/docs/next.config.js b/ui/docs/next.config.js new file mode 100644 index 0000000..7f4b7d6 --- /dev/null +++ b/ui/docs/next.config.js @@ -0,0 +1,6 @@ +const { withContentlayer } = require('next-contentlayer') + +module.exports = withContentlayer({ + output: 'export', + images: { unoptimized: true }, +}) diff --git a/ui/docs/nginx/docs.svc.plus.conf b/ui/docs/nginx/docs.svc.plus.conf new file mode 100644 index 0000000..af62600 --- /dev/null +++ b/ui/docs/nginx/docs.svc.plus.conf @@ -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; + } +} diff --git a/ui/docs/package.json b/ui/docs/package.json new file mode 100644 index 0000000..e3d0466 --- /dev/null +++ b/ui/docs/package.json @@ -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" + } +} diff --git a/ui/docs/postcss.config.js b/ui/docs/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/ui/docs/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/ui/docs/scripts/import-from-chapters.js b/ui/docs/scripts/import-from-chapters.js new file mode 100644 index 0000000..1a87b8d --- /dev/null +++ b/ui/docs/scripts/import-from-chapters.js @@ -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 }) diff --git a/ui/docs/styles/globals.css b/ui/docs/styles/globals.css new file mode 100644 index 0000000..2c2035c --- /dev/null +++ b/ui/docs/styles/globals.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-white text-gray-900; +} diff --git a/ui/docs/tailwind.config.ts b/ui/docs/tailwind.config.ts new file mode 100644 index 0000000..93b7b65 --- /dev/null +++ b/ui/docs/tailwind.config.ts @@ -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 diff --git a/ui/docs/tsconfig.json b/ui/docs/tsconfig.json new file mode 100644 index 0000000..d267e04 --- /dev/null +++ b/ui/docs/tsconfig.json @@ -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"] +}