Skip to content

Latest commit

 

History

21 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GopherAtlas

Golang or go home? — 精选文章、话题专区与学习随笔。 为 Go 开发者整理值得反复阅读的外部文章、策展阅读路径与原创实践。

当前为 P0-5.5:Product Realignment & Legacy-Ready Rebuild 验收版本。 私有 CMS 提供内容表格、Markdown 编辑/安全预览、串行 autosave、审核工作区、 Revision History、Tags/Authors/Users/Audit 和 Monitor。Publish 仅表示 已在 CMS 发布; 已实现 Assets/R2、原子 generation/outbox、Snapshot v1、Worker/Hook 和公开构建 marker。 Public 从 snapshot v1 构建完整阅读页面、静态 redirects、RSS/sitemap/SEO 与 Pagefind 搜索。 Production CMS 已更新并验证登录、备份、generation 1 发布同步与 Worker 首页访问。 实操命令及验收边界见 生产操作手册。 首次 generation → snapshot → Hook → marker 已跑通;正式域名已接入新 Worker。 详见 阶段范围。

架构

当前私有 CMS:Go / Fiber v3 + 内嵌 React Admin → SQLite / sqlc / goose
发布链路:CMS → 私有 R2 快照 → Astro 静态构建 → Workers Static Assets
                    └── 不可变 R2 图片 ──────────────→ 公共读者

公共站不在请求时依赖 CMS;私有服务器离线不影响读者。源码留在 Git,内容 通过 CMS 编辑与发布;about/contribute 页面继续随仓库维护。

目录 当前职责
apps/web snapshot v1 三条产品线、Post 兼容详情、搜索、RSS/SEO/redirects
apps/admin 按 feature 拆分的 React/Vite 编辑工作台、Base UI、RHF、TanStack Table/Query
packages/markdown CommonMark/GFM 安全规则与共享 remark 插件
packages/api-client OpenAPI 生成类型、同源请求及 CSRF header
cmd/gopheratlas-cms 配置、共享 logger、SQLite、OAuth 服务的构造与关闭
internal/app, internal/http Fiber 中间件顺序、API、错误与 cookie 边界
internal/auth, internal/policy 身份事务、会话和集中权限判断
internal/content, internal/audit 内容工作流、路由、taxonomy 和同事务安全 Audit
internal/database, db SQLite 连接池、真实 goose migrations 和 sqlc queries
internal/adminui 编译进 Go 二进制的 Admin 生产静态文件

分支模型

初期集中开发按用户授权直接在 clean、已同步的 dev 提交和 push。 环境仅 Development / Production;main 保存生产发布快照,不直接开发,也不由本阶段同步。 CI 检查 push 与 PR 的 dev、main;仓库默认分支和 Ruleset 由维护者管理。

本地开始

需要 Git、GNU Make、Node 24.15.0、pnpm 12.3.4、Go 1.26.8。 版本由 .node-version、.go-version、根 packageManager 固定。 pnpm 的精确保存、严格 engine 检查、minimumReleaseAge: 0 与 esbuild/sharp 构建白名单在 pnpm-workspace.yaml;.npmrc 仅用于 registry/auth。 CI 使用 pnpm/action-setup@v6.1.0 读取根版本。

pnpm install --frozen-lockfile
go mod download
make check

本地初始化数据库必须显式选择路径。PowerShell 示例:

New-Item -ItemType Directory -Force data
$env:DATABASE_PATH = './data/gopheratlas.db'
make db-status
make db-up

Bash 使用 mkdir -p data 和 export DATABASE_PATH=./data/gopheratlas.db 后运行相同 make 命令。迁移脚本不隐式加载 .env;CMS 启动和 /readyz 也不会执行迁移。 已有数据库需显式运行 make db-up 至 00003_publication.sql。00001/00002 保持不变, readiness 要求 schema version 3、cover 列、assets/jobs 和 site_state singleton。

配置好根 .env 后,可在一个终端启动全部开发服务(Windows PowerShell、Linux/macOS 相同):

make dev

该命令先编译开发 CMS、准备 Public 快照,再启动 CMS / Admin / Public。 Development 使用本地 SQLite 和 File ObjectStore,不连接 R2 或 Cloudflare。 Public 读取本地 data/storage/content/latest.json,检测到新 generation 时只重启 Astro,CMS/Admin 保持运行。 上传文件、内容和发布历史跨重启保留;启动不 seed 内容、不清理数据。 非预期的服务退出或启动失败会停止其余服务,Ctrl+C 统一退出;Linux/macOS 先发送终止信号, 超时后强制清理进程组,Windows 按本次启动的 PID 清理完整子进程树。 Windows 的子进程输出通过管道转发到当前终端,避免 PowerShell/Windows Terminal 中分离进程继承控制台句柄后静默退出;Ctrl+C 仍由启动器统一处理。 数据库仍需提前显式迁移,不会自动建业务 Schema。也可以分三个终端单独启动:

make dev-cms     # CMS: 127.0.0.1:46217
make dev-admin   # 浏览器: http://127.0.0.1:5173
make dev-web     # Public: http://127.0.0.1:4321;自动读取根 .env

make dev-cms 保持通过 Node 的 --env-file-if-exists=.env 读取根 .env; make dev-web 和 make dev 采用相同的进程变量优先规则,缺失文件无妨。 make dev 使用与 go run 相同的无内嵌 SPA 开发 CMS,Admin 由 Vite 提供。 直接运行 Go 二进制只读 process env。检查和测试不加载仓库的真实 .env, 所有测试数据库都位于临时目录。不要把真实配置、token、cookie 或 OAuth 参数写入 日志、fixture、提交或报告。

Admin Vite 将 /api、/ops、/healthz、/readyz 同源代理到 CMS_LISTEN_ADDR, 仅从根环境读取该非秘密代理地址。CMS 始终要求 loopback IP 和 1024–65535 端口。

GitHub 登录与身份

本地将 .env.example 复制为未跟踪的 .env,按 OAuth App 配置填写 GITHUB_OAUTH_CLIENT_ID、GITHUB_OAUTH_CLIENT_SECRET、 GITHUB_OAUTH_REDIRECT_URI、BOOTSTRAP_ADMIN_GITHUB_ID。开发浏览器 origin 默认 http://127.0.0.1:5173,回调应为该 origin 下的 /api/auth/github/callback,并匹配 GitHub OAuth App 注册设置。CMS_BASE_URL 必须与浏览器 origin 一致。

生产设置 APP_ENV=production、CMS_BASE_URL=https://<private-tailnet-host>.ts.net,回调为 https://<private-tailnet-host>.ts.net/api/auth/github/callback。生产必须提供完整 OAuth 配置和 正整数 bootstrap ID,并通过 Tailscale HTTPS 访问。OAuth token 仅用于 /user 身份解析,之后丢弃,不入库。测试只连接本地 fake provider。

  • GitHub numeric ID 是身份主键,login 可更新。作者 slug 为 github-<numeric-id>, 当前不可改名;显示名称、Markdown 简介和网站可编辑。
  • 首次未知用户为 editor/pending。无 active Admin 时,只有匹配 bootstrap ID 的 非 disabled 用户可成为第一个 Admin;存在 Admin 后由用户管理界面审批和授权。
  • Admin 管理用户及 Monitor;Reviewer 和 Editor 只能访问其服务器权限允许的界面。 最后一个 active Admin 不能被停用或降级;停用事务同时撤销该用户的所有会话。
  • OAuth state 一次性、短期、绑定发起浏览器;数据库只存 hash,原子消费防重放。
  • Session 与 CSRF 使用独立随机值,SQLite 只存 SHA-256;Session 默认 168h 绝对 有效期。生产 cookie 为 __Host-gopheratlas_session,Secure/HttpOnly/Lax/Path=/, 无 Domain;CSRF cookie 同样 host-only/Secure,但可被 JS 读取。
  • 客户端从 cookie 读取 CSRF 并发送 X-CSRF-Token,服务端同时验证 cookie、hash 和精确 Origin。刷新/多标签不需要 LocalStorage。退出撤销持久 Session 后清 cookie。 HTTP loopback 开发使用单独的 gopheratlas_dev_* cookie 名。

构建、健康检查与日志

make build-cms
# 输出 .cache/bin/gopheratlas-cms(Windows 为 .exe)

构建先生成 apps/admin/dist 并复制到忽略的 internal/adminui/dist,再用 -tags=adminembed 编译。生产只运行该 Go 二进制,不需要 Node。直接 go test ./... 或 go run 无需已有前端产物;此模式的 SPA 返回 503,开发 UI 由 Vite 提供。 生产 tag 没有构建产物会在编译时失败,不会静默打包占位页面。

使用内嵌 Admin 本地验证时,将 CMS_BASE_URL 和 OAuth 回调改为实际 CMS 浏览器 origin(例如 http://127.0.0.1:46217)。构建产物不包含 .env 或 OAuth Secret。

/healthz 只表示进程存活;/readyz 检查 SQLite 和 P0-4 migration/列集合,缺失或 不兼容返回 503。GitHub、R2、Hook 和公开 marker 是否在线均不影响 readiness。/ops/monitor 只有 active Admin 能打开;一个 app-wide Monitor 包裹业务请求,Recover 在其后。

一个共享 Zap logger 同时写 JSON 到 stdout 和 Lumberjack JSONL 文件。默认 ./logs/cms.jsonl,100 MB / 10 份 / 30 天 / 压缩;LOG_* 可配置。contrib Zap 只记 latency/status/method/path/request_id,不记 query、body、IP、UA、cookie、Authorization 或错误原文。health/readiness/Monitor 不写 access log。反向代理也必须避免记录 OAuth query。生产 CMS 已由 systemd 管理,通过 Tailscale Serve 暴露私有 HTTPS;详见运维文档。

验证与生成

make generate        # OpenAPI → TS,真实 migrations/queries → sqlc
make check           # 最终统一门禁

门禁包括边界检查、gofmt/vet/staticcheck、Go 身份/内容/并发/数据库/HTTP 测试、前端 lint/ 类型/身份与编辑审核 UI 测试、OpenAPI 和生成漂移、goose/sqlc 隔离验证、Public 构建,以及 Admin 生产构建后的 embed 测试和最终 Go 二进制编译。Linux CI 另外执行 go test -race -tags=adminembed ./...。sqlc 输出与 TS schema 必须经生成,不手改。

设计与工程上下文

Public 延续旧站 Logo/视觉与暖纸/石墨主题,自托管 Sora/Plus Jakarta Sans,代码使用系统等宽字体, 不使用 shadcn;Admin 为紧凑的 Base UI 工作台。见 设计合同、 设计系统、AGENTS.md、架构 及 ADR。

内容 API 与 Admin 工作流

/api/admin/v1/content 创建/读取内容;Draft PUT 是包含 version 的完整快照, 同事务更新字段、Tags 和 Topic 顺序。旧版本保存返回 409 content_version_conflict。 正文上限 512 KiB UTF-8,普通 JSON 请求上限 1 MiB,Review comment 上限 16 KiB; 服务端验证 Markdown 和强类型 payload,拒绝未知 subtype 字段。

Owner/Admin 可提交、撤回及将历史 Revision 恢复到 Draft。Reviewer 只审核 exact pending Revision,不能审核自己 owned/byline 的内容;Admin 可以 bypass,也可 direct publish。发布后继续编辑只改变 Draft,已发布 Revision 指针保持独立。 Tag 写入、Topic 创建、featured 变更、unpublish/archive 仅 Admin 可操作。

Publish 表示 SQLite 选定 Revision 并原子排队 generation/job,并不表示公共网站已重建。路由历史永久 保留,重命名将旧路径转为按 Content identity 解析的 redirect;unpublish/archive 不释放路径。Audit 与重要 mutation 同事务,覆盖身份变更,但不记录 Draft autosave、 正文、Review comment、payload 或凭据。各 list 使用最多 100 条的 keyset 分页。

接口与请求/响应见 OpenAPI,领域约束见 Data contract 和 ADR 0006。 P0-3 增加 author summaries、immutable Review detail、列表筛选及 server action projection; 筛选不绕过 object policy,Reviewer 的历史详情也不返回他人的 Draft。 P0-4 publication 网络步骤独立于 SQLite 事务;正式域名切换独立执行。

编辑工作台

  • 后台只使用简体中文,不引入 i18n。208px 左侧导航可收起为图标栏,顶部提供面包屑、 搜索、新建、主题与账户菜单;窄屏使用抽屉导航。Phosphor 图标与 Base UI 控件统一交互。
  • Development 编辑器提供“博客预览”:先 flush 当前自动保存队列,再以 Public 的真实 ContentDetail/Layout/Markdown 样式打开当前 Draft。预览不要求发布,不推进 generation, 不写 R2 或预览文件;冲突/保存失败时停止。修改后再次点击即可预览最新保存内容。 预览数据只通过一次 POST 传递,不放 URL/浏览器存储。未填标题、路径或语言使用展示 占位值;专题条目优先使用本地已发布快照,没有快照的条目标注待发布。 样式/模板热更新可重载当前预览;直接打开预览 URL 不含 Draft 数据,需从后台进入。 Production 构建没有此入口。
  • 导航按工作台、内容、内容资源、编辑流程、成员、发布与运维分组,入口与动作仍由 server permissions/actions 控制。搜索和摘要使用现有有界 API,明确标注已加载范围。
  • Content 下四个类型入口复用同一张可筛选、cursor 分页的表格。创建后直接进入编辑器; Topic 仅 Admin 可创建,Tags 在独立管理页面创建/更新。
  • Source / Preview / Split 使用 UIW source 输入和 react-markdown/GFM 安全预览。 外部图片只显示 warning placeholder,不发起图片请求;无 H1/HTML 命令;Insert image 通过 Asset picker 和必填 alt 插入受控 URL。
  • 空闲 1.7 秒保存完整 Draft,始终只有一个 PUT 在途;后续输入合并并使用新 version。 Ctrl/Cmd+S、Submit 和 Direct Publish 复用同一个队列。409 后暂停自动重试,提供 Copy、Inspect 和显式 Reload;离开未保存页面会提示。Draft 从不写浏览器存储。
  • 审核始终显示 exact immutable Revision;反馈、resubmit、历史查看/恢复、归档和 直接发布均沿用 P0-2 服务。编辑下一版不改变已发布 pointer。
  • 跟随系统 / 浅色 / 深色与侧栏收起偏好可持久化。360px 下抽屉导航、内容属性堆叠。 Monitor 是普通 Admin 链接;Assets 对 active roles 开放,Publication 仅 Reviewer/Admin。

生产构建包含所有 lazy chunks,仍由单个 Go 二进制提供。Vite 构建门禁拒绝 raw HTML preview 或禁止的 editor/primitives 模块进入产物。工程决定见 ADR 0007。

新布局将正文置于视觉中心,右侧按基础/类型/搜索展示组织属性;版本与路径单独查看。 素材库复用图片选择器,标签用创建/编辑对话框,成员统一表格与资料表单;发布页先说明 同步状态,技术字段放入详情抽屉。UI 架构与约束见 ADR 0010。本次未改变 Public、API、 数据库或发布语义,P0-6 只做正式域名切换,不迁移旧内容(ADR 0015)。

Assets 与 Publication

图片按 bytes/header 检测 PNG/JPEG/WebP/GIF,最大 10 MiB、16384 px/边、100M 像素。 SHA-256 决定永久 key,同 bytes 去重、缓存一年 immutable,不保存原始本地文件名。 不剥离 EXIF/二进制 metadata。Admin soft delete 停止新选择,不删除 R2 对象。

Development 同样使用真实图片验证与 SHA 去重,文件写入本地 storage/assets。 CMS 的 /__dev/assets/* 仅在 Development 向 loopback 提供受控图片,Production 没有该路由。 Admin、封面、Markdown、博客预览与 Public 共用配置好的本地图片地址;生产仍仅接受正式素材域名。 新的 GitHub 登录仍需网络;已有有效持久 Session 后,编辑、上传、发布和本地 Public 可离线运行。 详见 本地持久化决定。 封面与 Markdown 插入均进入原有 autosave,历史 Revision/已发布封面保持不变。

Publish/Unpublish、原本已发布内容的 Archive、公开内容使用的 Author/Tag 更新,在 同事务内递增 generation 并插入 durable job。Worker 合并旧 generation,上传私有 snapshots/generation-N.json,再写 latest.json,再 POST Hook。Hook 为至少一次投递; 失败退避,六次自动尝试后保留 failed,由 Reviewer/Admin Retry。只支持一个 active CMS writer。

Development 总是启用本地 FileStore 与 snapshot worker,忽略旧 R2/Hook 配置; Production 仍必须配全八个 P0-4 变量,缺失时不能 fallback 到本地。参见 部署与恢复 和 Production 构建说明。

已有 Linux/systemd 生产安装更新时,以仓库所属用户执行:

cd /srv/gopheratlas/repo
git pull --ff-only origin main
make prod-update

该命令安装依赖、构建内嵌 Admin 的 CMS,随后自动 sudo 停机备份、安装、启动并检查健康状态。 单独备份使用 make prod-backup;备份保存在 /srv/gopheratlas/backups/,不会自动删除。 两个命令均不自动迁移、不读取或修改生产 Secret;不再需要维护服务器上的旧 update.sh。 详见运维说明与备份说明。

本地 make dev-web / make dev 自动读取根 .env,不需要手工导出或映射环境变量。 正常开发只有一条输入链路:本地 Admin → CMS → SQLite → 本地 full snapshot → Public。 默认目录如下,storage 总是位于 DATABASE_PATH 同目录:

data/
├── gopheratlas.db             # 及 WAL/SHM
└── storage/
    ├── assets/media/sha256/   # 真实上传的不可变图片
    └── content/
        ├── latest.json
        └── snapshots/generation-N.json

无需 R2、Hook 或 CONTENT_R2_*。旧值即使留在 .env 里也不会被 Development 使用。 若存在非空 CONTENT_SNAPSHOT_FILE,启动会明确拒绝并提示移除;该变量仅保留给测试/CI build。 发布仍走 Draft → immutable Revision → published pointer → generation/job → Exporter; 同一个 worker 写入 FileStore,并以现有终态完成任务,不请求外部 Hook。

make dev / make dev-web 每 750ms 检查本地 latest.json,generation 未变时不读完整快照或重启。 新 generation 通过原有 hash/schema/引用图/路由校验后,替换 .generated 输入并仅重启 Astro,更新 getStaticPaths;已打开的 Public 页面通过 Vite 重连刷新。保留三个服务运行, 在 Admin 发布后几秒内即可看到新页面与新路由,无需手工重启。轮询失败保留当前内容并重试。 初次本地目录尚无 latest.json 时明确显示等待状态,Public 使用空站点并继续等待首次发布; 这不是 fixture fallback。Production 缺失/损坏输入仍然 fail-closed。 Astro dev 不生成 Pagefind 索引,完整搜索应使用 fixture build 后的 preview。

Production / Cloudflare 的 pnpm --filter @gopheratlas/web build 行为不变:不读取根 .env、 不使用 CMS R2_* fallback,必须单独提供 CONTENT_R2_* read-only credential。 本地验证完整产物可运行 make check,它显式选择 fixture 并完成 build;随后运行 pnpm --filter @gopheratlas/web preview。不要为运行检查配置真实凭证。

构建校验 latest、SHA-256、Schema v1、Markdown 和引用图,输出忽略的 .generated 快照及 /.well-known/gopheratlas-build.json。make check 固定显式 fixture 并进行 synthetic secret-output scan。生产不配置 fixture fallback;内容桶始终私有,CMS RW 与 Web RO 凭据分离。实现与验证未使用真实 R2/Hook 或修改 Cloudflare/DNS。

Public 阅读站与 P0-6 边界

apps/web/src/lib/publication 在构建时验证/索引公开实体与引用,模板不读取 Draft 或 私有 API。详情直接使用 snapshot 的 canonicalPath;主流程为精选文章、话题专区、 学习随笔,Post 只保留兼容。文章筛选每页 12 条,Note 分组卡片每页 4 条,辅助集合 保持 24 条静态分页。另有 Tag、Author、about/contribute/search 和 404 页面。 Public 不需要运行中的 CMS 或 Node。Curated 展示来源与点评,标题和阅读全文跳转原文。

构建命令一次生成 HTML、_redirects、RSS/sitemap/robots、Pagefind 和 build marker。 历史路径输出直接 301,超过 2,000 条或单行 1,000 字符即失败。Pagefind 索引三类详情与 中英文 About/Contribute,统一中文分词索引兼顾英文词。Chrome、静态筛选、阅读控件和 Note 的 giscus 按页面加载浏览器代码;搜索页才加载 Pagefind。 正文复用共享 GFM/安全检查与 Shiki;SEO 使用覆盖值或 title/summary,语言保持真实。

本地构建后用 pnpm --filter @gopheratlas/web preview 查看完整产物(含 Pagefind)。 Astro preview 不模拟 Cloudflare _redirects;其内容由门禁检查,平台行为在发布验收 时确认。node scripts/check-public-build.mjs 检查核心 fixture 的实际页面与引用、 redirects/RSS/sitemap/search 索引、marker hash 和输出隐私,已接入 make check。

详见 ADR 0012。P0-5.5 已实现 Development plan/apply 与旧路由校验。用户已决定博客从头开始:不导入旧内容, 保留现有生产账号、建站记录和 generation,P0-6 只处理域名切换与新站验收。 见 ADR 0015 和生产操作手册中的实际切换记录。

三条产品线与 Legacy 导入

  • 精选文章:标题与阅读全文跳转原文;正文是收录理由 / 编辑点评。
  • 话题专区:只编排精选文章,推荐区与普通区共享一个有序清单。
  • 学习随笔:原创 Markdown 博客,按分组连续阅读。

Post 保留底层兼容路由/API,不再出现在正常创建菜单、主导航、RSS 和搜索中。 中文 Admin 保留现有 Shell、权限、自动保存与版本冲突处理。make dev 仍一次运行 CMS/Admin/Public;发布后 watcher 自动刷新 Public,“博客预览”仍复用真实 Public renderer,预览不会推进 generation 或写 R2。

本次收紧 pre-cutover snapshot v1。旧快照缺少 Note 分组字段、Topic 推荐数量,或包含 不符合新约束的内容时会明确拒绝加载;不会静默改写。升级已有 Development 数据时, 先单独启动 make dev-cms / make dev-admin,修正内容并通过正常发布生成新快照, 再运行 make dev。不要覆盖历史 generation 对象;未来生产 升级须同时部署匹配的 CMS exporter 与 Web consumer,详见 ADR 0012。

Legacy 导入先 plan 再 apply。必须显式指定当前 CMS 的 Admin owner 与 Note Author 映射,不猜测作者、不导入转载正文、不放宽图片安全规则。操作步骤、一次性导入边界、 失败恢复与旧路由统计见 Legacy 导入运维说明。 自动测试与浏览器演练使用可丢弃数据库;真实 Development apply 的目标与署名需明确。

Public 的 Logo、首页、配色、文章筛选和 Topic/Notes 结构延续旧 GopherAtlas。 /en/ 覆盖站点自有页面与全部内容、话题、随笔、标签、作者及分页的 chrome 别名。 语言切换保留当前页面、筛选条件与锚点;内容不自动翻译。详情别名共用原 canonical, 不重复进入 sitemap/Pagefind,SQLite 中的旧 URL 不变。Public 交互和断点以旧站为准。 Note 阅读提供 TOC、代码复制、脚注、宽表格滚动与组内上下篇。

Note 评论与 Reactions 使用 giscus。维护者已启用 GitHub Discussions,giscus 配置页 确认仓库满足条件;apps/web/src/config/discussion.ts 使用公开的「博客评论」分类 ID。 这些值不是 Secret。更换分类后需重新构建 Public;分类为空时显示未配置说明。 访客评论使用独立的 giscus GitHub 授权,不复用 CMS 登录。首次真实评论/回应仍需人工验收。 映射固定为 note:<groupSlug>/<slug>,标题变更不会新建 Discussion。

Legacy Importer 保留为 Development 工具,不扩展生产入口,也不用于此次域名切换。 备份成功与恢复演练须分别记录;正式域名切换单独执行,不清空现有生产数据。

License

MIT。组件与字体来源见 Third-party notices。

About

A curated Go knowledge atlas and multi-author Markdown publishing platform, built with Astro, React, Go, SQLite, and Cloudflare.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages