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-upBash 使用 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;自动读取根 .envmake 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 端口。
本地将 .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/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)。
图片按 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。
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 和生产操作手册中的实际切换记录。
- 精选文章:标题与阅读全文跳转原文;正文是收录理由 / 编辑点评。
- 话题专区:只编排精选文章,推荐区与普通区共享一个有序清单。
- 学习随笔:原创 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 工具,不扩展生产入口,也不用于此次域名切换。 备份成功与恢复演练须分别记录;正式域名切换单独执行,不清空现有生产数据。
MIT。组件与字体来源见 Third-party notices。