feat(docs): finish docs service frontend switch

This commit is contained in:
Haitao Pan 2026-03-20 00:11:44 +08:00
parent 9a822a5874
commit 986985a63d
8 changed files with 48 additions and 35 deletions

View File

@ -13,19 +13,20 @@
The frontend is built in GitHub Actions and shipped as a prebuilt `linux/amd64` image. The host only pulls the image and starts containers; it does not build locally.
`yarn prebuild` bundles the docs, blog, and static content needed by the console. During that phase the CI container runs `scripts/sync-doc-content.sh` (pulling docs from this repo plus `accounts.svc.plus`, `rag-server.svc.plus`, and `postgresql.svc.plus`) and `scripts/sync-blog-content.sh` (cloning `https://github.com/cloud-neutral-workshop/knowledge.git`), so the `knowledge/` directory and all documentation assets already live inside the image before the runtime stage begins.
`yarn prebuild` now generates only console-owned marketing artifacts. `/docs` and `/blogs` no longer bundle `knowledge/` or synced markdown content into the frontend image. Those routes fetch rendered content from `docs.svc.plus` at request time through the server-side `docsServiceClient`.
The stack is static-first:
- Caddy serves `/_next/static/*` and public assets from a shared read-only volume.
- The Next.js standalone container serves dynamic HTML, auth endpoints, and API proxy routes. Static assets and hashed CSS/JS files are extracted by the `frontend-assets` helper task, so the runtime no longer needs to compile anything on the single-node host.
- `knowledge/` and the synced docs/blog assets are copied into the image during the Docker build via the GitHub Actions workflow.
- `docs.svc.plus` is the source of truth for rendered docs/blog pages; the browser does not call it directly.
Releases are orchestrated through `.github/workflows/service_release_frontend-deploy.yml`. That workflow clones the knowledge repository, runs the Docker build/push sequence, renders `.env.runtime`, and ships `docker-compose.yml`, `Caddyfile`, and the runtime env file to the host. The control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` then updates Cloudflare DNS for the release domain (via `scripts/github-actions/update-release-dns.sh`) so `cn.svc.plus` and the redirected alias `cn.onwalk.net` point at the new environment.
Releases are orchestrated through `.github/workflows/service_release_frontend-deploy.yml`. That workflow builds/pushes the image, renders `.env.runtime` including `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL`, and ships `docker-compose.yml`, `Caddyfile`, and the runtime env file to the host. The control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` then updates Cloudflare DNS for the release domain (via `scripts/github-actions/update-release-dns.sh`) so `cn.svc.plus` and the redirected alias `cn.onwalk.net` point at the new environment.
This baseline is intentional for the weak-IO single-node host (47.120.61.35). No images are built on the target machine, keeping the deployment lightweight: the host only logs into GHCR, pulls the `dashboard` image, extracts assets into `frontend_static`, and starts `dashboard` plus `caddy` containers via `docker compose`.
If `docs.svc.plus` is later refactored into a dedicated API service, revisit this writeup (and the runbook) so the GitHub Actions pipeline only bundles the API payloads that belong to that new service.
`docs.svc.plus` is now the dedicated docs/blog service for the frontend delivery path.
## Related Docs

View File

@ -17,19 +17,15 @@ The production frontend is deployed as a prebuilt container image from GitHub Ac
- The target host does not build images locally.
- The workflow builds an `linux/amd64` image and pushes it to `ghcr.io/<owner>/dashboard:<sha>`.
- The host only performs `docker login`, `docker compose pull`, static asset extraction, and `docker compose up`.
- `knowledge/` is cloned during CI build (via `scripts/sync-blog-content.sh`) and synced with other docs (via `scripts/sync-doc-content.sh`) before being packed into the image.
- `/docs` and `/blogs` fetch their content from `docs.svc.plus` at runtime; the frontend image no longer packs `knowledge/` or synced markdown payloads.
- Static assets are extracted from the image into a shared Docker volume so Caddy can serve `/_next/static/*` and checked-in public files directly.
This is intentionally static-first for the current weak-IO single-node host. Dynamic HTML, auth routes, and API proxy routes still run through the Next.js container. When `docs.svc.plus` is later split into an API/service, revisit this runbook and remove docs content from the frontend image.
This is intentionally static-first for the current weak-IO single-node host. Dynamic HTML, auth routes, and API proxy routes still run through the Next.js container, but docs/blog content delivery is now delegated to `docs.svc.plus`.
## Control Plane & DNS Stage
The control repo (`github-org-x-evor`) tracks `console.svc.plus` through `console.svc.plus.code-workspace` and keeps the `subrepos/accounts.svc.plus` pointer in sync via `skills/cross-repo-upstream-submodule-sync`. Releases resolve metadata with that workspace and the `config/single-node-release` manifests. After `.github/workflows/service_release_frontend-deploy.yml` finishes pushing the new image, the control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` calls `scripts/github-actions/update-release-dns.sh` to update Cloudflare DNS so the new endpoint is reachable under `cn.svc.plus` and `cn.onwalk.net`.
## Future Docs Strategy
Because the frontend currently ships docs content directly (knowledge/blog + rendered markdown), any future split where `docs.svc.plus` becomes an API-backed service should include a repo-level migration plan: stop syncing docs into the frontend image, move documentation storage/serving into the dedicated API, and adjust the runbook/workflow notes above accordingly.
## Runtime Layout
Remote directory:
@ -83,6 +79,8 @@ Repository/environment variables recommended:
- `NEXT_PUBLIC_SERVER_SERVICE_URL`
- `RUNTIME_HOSTNAME`
- `DEPLOYMENT_HOSTNAME`
- `DOCS_SERVICE_URL`
- `DOCS_SERVICE_INTERNAL_URL`
- `NEXT_PUBLIC_RUNTIME_ENVIRONMENT`
- `NEXT_PUBLIC_RUNTIME_REGION`
- `NEXT_PUBLIC_GISCUS_*`
@ -94,14 +92,13 @@ Repository/environment variables recommended:
## Release Flow
1. GitHub Actions checks out the repo.
2. GitHub Actions clones `knowledge/`.
3. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time.
4. The image is pushed to GHCR.
5. The workflow runs a matrix DNS stage, updating one public domain per job.
6. The workflow renders `.env.runtime`.
7. The workflow uploads `docker-compose.yml`, `Caddyfile`, and `.env.runtime` to the host.
8. The host pulls the new image, refreshes the static asset volume, and starts `dashboard + caddy`.
9. The workflow verifies `cn.svc.plus` and `cn.onwalk.net`.
2. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time.
3. The image is pushed to GHCR.
4. The workflow runs a matrix DNS stage, updating one public domain per job.
5. The workflow renders `.env.runtime`, including docs service runtime endpoints.
6. The workflow uploads `docker-compose.yml`, `Caddyfile`, and `.env.runtime` to the host.
7. The host pulls the new image, refreshes the static asset volume, and starts `dashboard + caddy`.
8. The workflow verifies `cn.svc.plus` and `cn.onwalk.net`.
## Verification Commands

View File

@ -13,19 +13,19 @@
前端镜像在 GitHub Actions 中完成构建并推送到镜像仓库,目标主机只负责拉取镜像和启动容器,不在机器上本地构建。
`yarn prebuild` 会同步 docs、博客和其它静态内容。CI 在该阶段执行 `scripts/sync-doc-content.sh`(从 `console.svc.plus`、`accounts.svc.plus`、`rag-server.svc.plus` 和 `postgresql.svc.plus` 拉取文档)以及 `scripts/sync-blog-content.sh`(克隆 `https://github.com/cloud-neutral-workshop/knowledge.git`),因此 `knowledge/` 目录和所有文档/博客资产在构建镜像时就已存在
`yarn prebuild` 现在只生成 console 自己的营销内容工件。`/docs` 与 `/blogs` 不再把 `knowledge/` 或 Markdown 文档打进前端镜像,而是在请求时通过服务端 `docsServiceClient``docs.svc.plus` 拉取渲染后的内容
当前方案尽量以静态模式运行:
- Caddy 直接服务 `/_next/static/*``public/` 里的静态资源,并配合 `frontend_static` 共享卷。
- Next.js standalone 容器只承接动态页面、认证接口和代理接口,`frontend-assets` 任务会把所有静态文件(包括哈希后的 CSS/JS拷贝到 `frontend_static`
- `knowledge/` 与同步的文档/博客内容在 GitHub Actions 的 Docker 构建阶段就被写入镜像
- `docs.svc.plus` 是 docs/blog 的运行时内容源,浏览器不会直接访问它
发布由 `.github/workflows/service_release_frontend-deploy.yml` 驱动CI 构建/推送镜像、渲染 `.env.runtime`,然后将 `docker-compose.yml`、`Caddyfile` 与运行时环境文件发送到主机。随后控制平面工作流 `.github/workflows/service_release_apiserver-deploy.yml` 通过 `scripts/github-actions/update-release-dns.sh` 更新 Cloudflare DNS使 `cn.svc.plus` 与别名 `cn.onwalk.net` 指向更新后的环境。
发布由 `.github/workflows/service_release_frontend-deploy.yml` 驱动CI 构建/推送镜像、渲染包含 `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL` `.env.runtime`,然后将 `docker-compose.yml`、`Caddyfile` 与运行时环境文件发送到主机。随后控制平面工作流 `.github/workflows/service_release_apiserver-deploy.yml` 通过 `scripts/github-actions/update-release-dns.sh` 更新 Cloudflare DNS使 `cn.svc.plus` 与别名 `cn.onwalk.net` 指向更新后的环境。
这是针对弱 IO 单机主机 `47.120.61.35` 的部署权衡:主机不会在本地构建镜像,只需登录 GHCR、拉取 `dashboard` 镜像、解包静态资源到 `frontend_static`,再通过 `docker compose` 启动 `dashboard``caddy`
未来如果 `docs.svc.plus` 被拆分成独立的 API 服务,必须同步更新这份说明(以及运行手册),让 GitHub Actions 只打包属于新服务的内容
`docs.svc.plus` 已经是前端 docs/blog 内容的独立服务
## 相关文档

View File

@ -1,10 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
KNOWLEDGE_DIR="${REPO_ROOT}/knowledge"
KNOWLEDGE_REPO="${KNOWLEDGE_REPO:-https://github.com/Cloud-Neutral-Workshop/knowledge.git}"
rm -rf "${KNOWLEDGE_DIR}"
git clone --depth=1 "${KNOWLEDGE_REPO}" "${KNOWLEDGE_DIR}"
echo "frontend build context no longer syncs docs/blog content; using docs.svc.plus at runtime."

View File

@ -50,6 +50,8 @@ append_env NEXT_PUBLIC_ACCOUNT_SERVICE_URL "${NEXT_PUBLIC_ACCOUNT_SERVICE_URL:-$
append_env SERVER_SERVICE_URL "${SERVER_SERVICE_URL:-https://api.svc.plus}"
append_env NEXT_PUBLIC_SERVER_SERVICE_URL "${NEXT_PUBLIC_SERVER_SERVICE_URL:-${SERVER_SERVICE_URL:-https://api.svc.plus}}"
append_env SERVER_SERVICE_INTERNAL_URL "${SERVER_SERVICE_INTERNAL_URL-}"
append_env DOCS_SERVICE_URL "${DOCS_SERVICE_URL:-https://docs.svc.plus}"
append_env DOCS_SERVICE_INTERNAL_URL "${DOCS_SERVICE_INTERNAL_URL-}"
append_env OPENCLAW_GATEWAY_REMOTE_URL "${OPENCLAW_GATEWAY_REMOTE_URL-}"
append_env OPENCLAW_GATEWAY_TOKEN "${OPENCLAW_GATEWAY_TOKEN-}"
append_env VAULT_SERVER_URL "${VAULT_SERVER_URL-}"

View File

@ -11,12 +11,12 @@ echo "======================================"
echo "Starting prebuild process..."
echo "======================================"
# Step 1: Sync documentation from service repositories
# Step 1: Generate local marketing content artifacts
echo ""
echo "[1/2] Generating static content..."
echo "[1/2] Generating marketing content..."
npx tsx scripts/generate-content.ts
# Step 2: Build contentlayer
# Step 2: Build contentlayer artifacts used by non-doc pages
echo ""
echo "[2/2] Building contentlayer..."
node scripts/build-contentlayer.mjs

View File

@ -6,7 +6,6 @@ import { Suspense } from "react";
import BlogList from "@components/blog/BlogList";
import { PublicPageShell } from "@/components/public/PublicPageShell";
import type { BlogCategoryPayload, BlogPostPayload } from "@lib/docsServiceClient";
import { getBlogList } from "@lib/docsServiceClient";
export const metadata: Metadata = {
@ -17,9 +16,15 @@ export const metadata: Metadata = {
export default async function BlogPage() {
const listing = await getBlogList({ page: 1, pageSize: 200 });
const categories: BlogCategoryPayload[] = listing.categories;
const categories = listing.categories;
const postsWithoutContent = listing.posts.map(
({ html: _html, plaintext: _plaintext, sourcePath: _sourcePath, language: _language, ...post }: BlogPostPayload) => post,
({
html: _html,
plaintext: _plaintext,
sourcePath: _sourcePath,
language: _language,
...post
}) => post,
);
return (

View File

@ -9,7 +9,21 @@ import BrandCTA from "@components/BrandCTA";
import { PublicPageIntro } from "@/components/public/PublicPageShell";
import SearchComponent from "@components/search";
import { useLanguage } from "@i18n/LanguageProvider";
import type { BlogCategory, BlogPostSummary } from "@lib/blogContent";
type BlogCategory = {
key: string;
label: string;
};
type BlogPostSummary = {
slug: string;
title: string;
author?: string;
date?: string;
tags: string[];
excerpt: string;
category?: BlogCategory;
};
function formatDate(
dateStr: string | undefined,