Skip to content

feat(base-path): support mounting site under URL sub-path#1

Closed
NickWilde18 wants to merge 13 commits into
masterfrom
feat/base-path-support
Closed

feat(base-path): support mounting site under URL sub-path#1
NickWilde18 wants to merge 13 commits into
masterfrom
feat/base-path-support

Conversation

@NickWilde18
Copy link
Copy Markdown
Owner

Summary

Fork patch 集——给上游 pkgsite 加 -base-path flag,让站点能整体挂在 URL 子路径下(如 /gogodocs/)。这是反代 / 共享域名场景的硬需求,上游 pkg.go.dev 假设挂根没这能力。仅在 fork 内维护,不向上游提

无论挂在哪,本 patch 集对上游零差异(默认 -base-path="" 行为完全一致)。

用法

pkgsite -base-path=/gogodocs -http=:8080 .
# 或 docker:
docker run gogodocs -base-path=/docs -http=:8080 .

约束:base path 必须以 / 开头、不带尾斜杠,否则 validateBasePath fail-fast 启动失败。

改造分布(3 commits)

Commit 1:framework + 模板(48 文件)

  • cmd/pkgsite/main.go-base-path flag + validateBasePath
  • cmd/internal/pkgsite/server.go & internal/frontend/server.goServerConfig.BasePath / Server.basePath 透传
  • Server.Install:shadow handle 入参注入 prefixPattern,所有 mux pattern 自动带前缀,下面 ~35 处 handle(\"...\") 声明零改动
  • 内部 http.StripPrefix 5 处 + staticHandlers.basePath 让 file server 剥到正确剩余 path
  • internal/frontend/templates/templates.go:新增 abs / basepath template helper
  • internal/godoc/dochtml/{dochtml,template}.go:godoc cross-reference 链接(PackageURL func)拼 base path
  • static/**/*.{tmpl,md}:批量 sed 把硬编码绝对 path ~150 处改成 abs / basepath 调用

Commit 2:Go redirect 字面量(3 文件)

  • internal/frontend/server.go/cmd/cgo/v1/api×2 加 s.basePath
  • internal/frontend/fetchserver/{fetch,404}.goFetchServer.BasePath 字段 + 2 处 redirect 拼前缀

Commit 3:smoke test 暴露的 3 类问题(32 文件)

  • safehtml/template TrustedResourceURL 严格校验——{{abs}}?version={{.X}} 被拒。新增 asset helper 一气拼好 path + ?version=<v>
  • <script> 上下文 _sanitizeScript 要求 safehtml.Script 类型,4 处 loadScript(\"{{abs}}\") 改用 document.documentElement.dataset.basePath runtime 拼
  • internal/frontend/details.go:home page path 比较 / trailing-slash 规范化 / ExtractURLPathInfo / stdlibRedirectURL 全部 base-path-aware(不然 /gogodocs//gogodocs 301/307 死循环)
  • TS 端 ~10 处硬编码 path:新增 static/shared/base-path/base-path.tsplayground/carousel/about 用之;<html data-base-path=\"{{basepath}}\"> runtime source;esbuild 重生 bundle

Smoke test

/gogodocs/                                HTTP 200 (home)
/gogodocs/static/frontend/frontend.min.css HTTP 200
/gogodocs/about                           HTTP 200
/gogodocs/search?q=fmt                    HTTP 200
/gogodocs/std                             HTTP 200 (stdlib detail)
/gogodocs/chat.cuhksz                     HTTP 200 (本仓库 module)
/gogodocs                                 HTTP 307 → /gogodocs/(mux 标准)

view-source: HTML 检查:<html data-base-path=\"/gogodocs\"> 注入正确,所有 attribute 引用都是 /gogodocs/... 前缀。

维护策略(AI 维护)

  • 月度 rebase 上游 master:predicted conflict 集中在 internal/frontend/server.go 的 mux 列表和模板。conflict 易解
  • 新增 mux 路由:作者按上游惯例写 handle(\"GET /foo\", h) 即可,basePath 自动 prefix
  • 新增模板硬编码 path:<a href=\"{{basepath}}/dynamic\"><a href=\"{{abs \/static`}}">`,分动 / 静两种 helper

遗留 TODO(不阻塞 P0 docker-compose)

  • internal/middleware/godocredirect.go:godoc.org → pkg.go.dev 跳转,host 仍硬编码
  • internal/frontend/serrors/serrors.go:inline error template 里两处 <a href=\"/...\">
  • cmd/frontend/main.go:prod 二进制创建 FetchServer 没传 BasePath(cmd/pkgsite 路径不影响)

Test plan

  • go build ./cmd/pkgsite ./internal/frontend ./internal/frontend/templates ./internal/godoc/dochtml ./internal/frontend/fetchserver 通过
  • go test ./internal/frontend/templates/... ./internal/godoc/dochtml/... 全绿
  • go run ./devtools/cmd/static(esbuild 重生 bundle)通过且输出含 dataset.basePath runtime 注入
  • 本地起 pkgsite -base-path=/gogodocs -http=:8089 .,全部 smoke endpoint HTTP 200
  • 验证 HTML 输出:<html data-base-path=\"/gogodocs\">、所有 attribute path 前缀正确
  • docker-compose 集成(P0b 后续工作)
  • Gateway 反代验证(P4 后续工作)

NickWilde18 added 13 commits May 7, 2026 14:53
加 -base-path flag 让 pkgsite 整站可挂在 URL 子路径下(如
-base-path=/gogodocs,站点入口 http://host/gogodocs/)。空 = 默认挂根
路径,跟上游零差异。需求来自反代 / 子路径部署场景。

主要改动:
- cmd/pkgsite/main.go:加 -base-path flag + validateBasePath(/foo
  形式,不带尾斜杠);ServerConfig.BasePath 透传
- cmd/internal/pkgsite/server.go:ServerConfig + newServer 增加 BasePath,
  传给 frontend.NewServer
- internal/frontend/server.go:ServerConfig + Server 加 basePath 字段;
  Install() 内 shadow handle 入参注入 prefixPattern 自动给所有 mux
  pattern 加前缀("/static/" → "/gogodocs/static/"),下面的具体路由
  声明完全不动;staticHandler / 内部 StripPrefix(third_party / sitemap /
  files / detail-stats / search-stats)手动加 s.basePath 让 file server
  能剥到正确剩余 path
- internal/frontend/templates/templates.go:新增 abs / basepath template
  helper,funcsWithBasePath 在内置 funcs 上叠 closure 实现
- internal/godoc/dochtml/template.go:LoadTemplates 接受 basePath 参数,
  存包级 var;BasePath() 暴露给读取(dochtml 单进程使用,包级 state OK)
- internal/godoc/dochtml/dochtml.go:godoc cross-reference 链接生成
  (PackageURL func)拼上 basePath,让 std lib / 同模块 import 链接在
  反代环境正确路由
- internal/godoc/dochtml/{dochtml,symbol}_test.go:LoadTemplates 调用
  补空 basePath 参数
- static/**/*.{tmpl,md}:批量把 hardcoded 绝对路径改成 abs / basepath:
  - "/static/foo.svg" → "{{abs `/static/foo.svg`}}"
  - "/search?q=..." → "{{abs `/search`}}?q=..."
  - "/{{.Path}}" → "{{basepath}}/{{.Path}}"
  - "/" → "{{abs `/`}}"
  - "/vuln/{{.ID}}" → "{{basepath}}/vuln/{{.ID}}"
  约 150 处 mechanical 替换;空 basePath 时 abs / basepath 都返原样

后续 patch 还要做:Go 端 http.Redirect 5 处字面量 / inline HTML in
serrors.go / TS 端 ~10 处 + bundle rebuild。
5 处硬编码 redirect 接 base path——之前的 commit 处理了模板和 mux
但忽略了 Go 端字面量 redirect URL,反代环境下这些 redirect 会跳出
站点子路径丢失上下文。

- internal/frontend/server.go:/cmd/cgo(C 包重定向)/v1/api(×2)
  三处加 s.basePath 前缀
- internal/frontend/fetchserver/fetch.go:FetchServer struct 加 BasePath
  字段(caller 不传则空字符串 = 行为同上游)
- internal/frontend/fetchserver/404.go:stdlib shortcut redirect 和
  nested-modules 搜索 redirect 用 s.BasePath 拼前缀

未处理(暂留 TODO):
- internal/middleware/godocredirect.go:godoc.org → pkg.go.dev 跳转,
  fork 内网部署用不到,host 还是 hardcoded "pkg.go.dev"
- internal/frontend/serrors/serrors.go:inline error template
  里的 <a href="/search?q=..."> / <a href="/about#...">——
  这两个 template 自带 parse,没注入 abs func;fork 内网这两类
  错误页极少触发,待 smoke test 发现真在出现再补
- cmd/frontend/main.go:prod 二进制创建 FetchServer 没传 BasePath。
  cmd/pkgsite(dev 二进制 / docker-compose)不用 FetchServer,影响为零。
P1 收尾——让站点真正能在 /gogodocs/ 子路径下浏览(之前 commit 把
框架搭好,但 smoke test 暴露三类残留问题):

1. safehtml/template TrustedResourceURL 校验严格——abs 在 <link href>
   后接 dynamic ?version={{.AppVersionLabel}} 拼接被拒("?version="
   不是合法 URL prefix)。新增 [asset path version] template helper:
   一气拼好 base + path + ?version=<v> 返回 safehtml.TrustedResourceURL,
   单 segment 单类型让 safehtml 满意。批量 sed 把模板里
   `{{abs `path`}}?version={{.AppVersionLabel}}` 改成
   `{{asset `path` .AppVersionLabel}}` 共 ~15 处。

2. <script> 上下文里 abs 调用过不了 _sanitizeScript(要求
   safehtml.Script 类型;abs 返 string / TrustedResourceURL 都不行)。
   inline loadScript("...") 改成 runtime 从
   document.documentElement.dataset.basePath 拼,frontend.tmpl + unit/versions
   + fetch + worker/index 共 4 处。

3. detail handler (unit/std/module 详情页入口)原版假设挂根:
   - r.URL.Path == "/" 判 home → 加 basePath / basePath+"/" 覆盖
   - trailing-slash 规范化 redirect 把 "/gogodocs/" 去尾变 "/gogodocs",
     mux 又 307 反向加尾,301 ↔ 307 死循环。加 basePath self 例外
     避免去尾;
   - urlinfo.ExtractURLPathInfo 假设 "/<module>" 形式,要先 strip basePath
   - stdlibRedirectURL 返 "/std" 等挂根 path,redirect 时补 basePath

4. TS 端 ~10 处硬编码 path(playground.ts 3 处 fetch、carousel.ts 2 处
   svg src、about/index.ts 1 处 svg + 1 处 location 比较),新增
   static/shared/base-path/base-path.ts 提供 getBasePath() / abs()。
   静态模板 frontend.tmpl 给 <html> 注入 data-base-path="{{basepath}}"
   作 runtime source of truth。重跑 esbuild 把 base-path 模块编进所有
   bundle (frontend.js / unit/main.js / about/index.js)。

5. 模板 funcs return type 改回 plain string——尝试过返
   safehtml.TrustedResourceURL 让 link href 通过,但 <script> 内同时
   要 safehtml.Script,单类型不能两全;plain string 让 safehtml
   按 context 自动 escape 是上游惯例。

Smoke test (本地 docker-compose 还没起,先跑 cmd/pkgsite 二进制 -base-path=/gogodocs):
- /gogodocs/                            HTTP 200 (home)
- /gogodocs/static/frontend/frontend.min.css  HTTP 200 (static)
- /gogodocs/about                       HTTP 200
- /gogodocs/search?q=fmt                HTTP 200
- /gogodocs/std                         HTTP 200 (stdlib detail)
- /gogodocs/chat.cuhksz                 HTTP 200 (本仓库 module)
- /gogodocs                             HTTP 307 → /gogodocs/(mux 自动)

未处理(fork 内网用得少,遇到再补):
- godocredirect 中间件:godoc.org 跳 pkg.go.dev,host 仍硬编码
- serrors.go inline error template 里两处 <a href="/...">
- cmd/frontend prod 二进制创建 FetchServer 没传 BasePath
P2 patch 集——给 fork 加三个内网部署常用能力,跟 P1 base path 解耦:

1. Mermaid 渲染(rsc.io/markdown 已经把 fenced code 输出
   <pre><code class="language-X">,缺最后一公里):
   - internal/sanitizer/sanitizer.go:放行 <code>/<pre> 的 class 属性,
     用新加的 langClass 正则限定只能是 "language-X" 形态,防任意
     class 注入
   - static/frontend/frontend.tmpl:head 末尾加 <script type="module">,
     页面里有 code.language-mermaid 才动态 import mermaid(~500KB)渲染。
     CDN 走 jsdelivr——公司内网若 strict-CSP / 不通公网,本地放
     third_party/mermaid 后 swap import URL 即可

2. -show-unexported flag(fork 内网 godoc 常见诉求——自家代码完整
   展示比 public-only 视图更有用):
   - internal/godoc/render.go:包级 var IncludeUnexported;DocPackage
     在 noFiltering(stdlib builtin 特例)之外多识别这个开关 → 给
     doc.NewFromFiles 传 doc.AllDecls
   - cmd/pkgsite/main.go:-show-unexported flag 直接设全局 var,
     避开 ServerConfig → frontend → godoc 多层透传(pkgsite 单
     进程一种行为,包级 state OK)

3. internal/ 包浏览:默认就 work(cmd/pkgsite 用 fetchdatasource +
   UseListedMods),不需 patch

Smoke test:
- /gogodocs/chat.cuhksz/internal/service/im/weixin HTTP 200,
  HTML 含 pollLoop / dispatchMessage / reconcileOnce / userMonitor
  等未导出符号——AllDecls 生效
- 模板注释从中文改英文(中文注释里的 \`\`\` 三反引号被 safehtml
  误判为 ES6 template literal 起始,整页 render 失败)

未做:mermaid 渲染端到端 smoke——cmd/pkgsite local mode 不走 README,
只渲 godoc comment(rsc.io/markdown 不在主路径上)。等 P0b docker-compose
跑完整 worker → DB → README 渲染流程时再实测一段含 mermaid 的 README。
cmd/pkgsite local mode 跑 docker container 用:
- multi-stage build:golang:1.24 编 cmd/pkgsite 二进制,stage 2 复用
  golang:1.24 base(不能用 alpine——local mode 走 go/packages.Load,
  container 必须自带 Go toolchain 调 `go list` 解析 module 依赖)
- ARG GOPROXY 默认 goproxy.cn,build / runtime 都生效——内网 / 国内
  网络环境的常态
- /repos 约定 mount 目录,docker-compose 把每个 Go module 仓库挂进
  这个路径下;容器主进程是 pkgsite,命令行参数指定 base-path /
  show-unexported / -http 端口 / mount 进来的 module path
- ENTRYPOINT pkgsite + CMD -h:默认行为是打 usage(防止误启动空容器
  hang);docker-compose 通过 command: 覆盖

下一步:Chat 仓库 docker-compose.dev.yml 加 gogodocs service,
mount 仓库,base-path=/gogodocs。
P1 smoke test 没覆盖到 module overview / unit detail 页的子目录链接,
用户在浏览器实测时发现 chat.cuhksz module 页只显示 api / manifest 两个
目录(internal 被 pkg.go.dev 默认 "Show internal" toggle 隐藏),
点子目录链接还会跳出 base path 外 404——两个独立 bug:

1. internal/frontend/versions/versions.go: ConstructUnitURL 是 pkgsite
   内部所有 unit/package/module 详情页链接的核心 URL builder(subdir
   列表 / search / breadcrumb 都走它),它生成的绝对路径必须带 base
   path 前缀。加包级 var BasePath,cmd/pkgsite/main.go 跟 godoc.IncludeUnexported
   同模式设置——pkgsite 单进程一种行为,不必加新参数让上百 caller 都改

2. unit.tmpl / unit/main/main.tmpl / search/search.tmpl 三处
   `loadScript('/static/...')` 用**单引号**,P1 sed 只匹配双引号
   loadScript("...") 漏过去。改成 `(document.documentElement.dataset.basePath
   || "") + "/static/..."` runtime 拼前缀(同 frontend.tmpl 内 inline
   script 模式)。修不修这个的影响:unit.js / main.js bundle 加载 404 →
   main.ts 里 "data-local=true 时 auto-click Show internal toggle 让
   internal 目录默认展开" 这段没跑 → 用户感知 internal 目录"不存在"
…old / mermaid)

go/doc/comment parser 不识别 markdown 反引号 / **bold** / fenced code,
但用户写 doc.go 时这些 markdown 风格已经形成习惯(毕竟 .md 写得多)。
渲染产物里 raw \` 和 ** 字面输出体验差,mermaid 图块也没办法变 fenced
code class 让客户端 mermaid.js 渲。

新增 internal/godoc/dochtml/internal/render/markdown_ext.go:
post-process 渲染好的 HTML 加三条 inline 转换:

- \`text\` → <code>text</code>
- **text** → <strong>text</strong>
- <pre>\`\`\`mermaid\\n...\`\`\`</pre> → <pre><code class="language-mermaid">...</code></pre>
  让 frontend.tmpl 里的 mermaid lazy-load CDN 脚本能识别并渲

实现:walkOutsidePre 把 HTML 切成 <pre> 内 / 外两类 segment,<pre> 内
原样保留——godoc 把 markdown 4 空格缩进 code block 渲染成 <pre>,
里面字符按字面(如 Go struct tag literal `json:"..."` 保持不动)。

设计选型:post-process HTML 比 fork go/doc/comment parser 干净——
保留 godoc 原生 [Symbol] cross-reference / heading-id / 链接提取等
功能不变,只在表层加几条 inline 转换。代价是 regex on HTML 不优雅,
但 godoc 渲染的 HTML 结构有限,<pre> 边界清晰,实测没遇到 corner case。

linkify.go formatDocHTML 在最后调一次 applyDocMarkdownExt 包装。

Smoke test (/gogodocs/chat.cuhksz/internal/service/im/weixin):
- <code>: 40 个,含 <code>wx:monitor:lease:{upn}</code> 等用户报告的
  反引号字面量
- <strong>: 38 个,含 <strong>不同 binding</strong> 等 bold
- language-mermaid: 2 个——doc.go 里两张 mermaid 时序图都被识别,
  浏览器侧 mermaid.js 渲成 SVG
两个 bug 一起修:

1. -show-unexported 实际没效果——godoc.IncludeUnexported=true 只让
   doc.NewFromFiles 用 doc.AllDecls,但 fetchdatasource 在喂给它之前先
   调 godoc.Package.AddFile(removeNodes=true) 在 AST 阶段就剥光未导出
   FuncDecl,doc.NewFromFiles 看到的 AST 已经被剔光 → AllDecls 救不回。
   修:internal/fetch/load.go 在 godoc.IncludeUnexported=true 时也把
   removeNodes 设 false,跟 stdlib builtin 特例同模式保留全部 nodes。
   smoke:weixin 包页面 dispatchMessage / authedClient / buildHelpText
   等私有函数现作为 data-kind="function" declaration 渲染,不再只是
   raw text 引用。

2. view source 链接没带 /gogodocs/ 前缀——cmd/pkgsite local mode 下
   source.Info 模板生成 "/files/{path}" 让 file mux serve 源码(不
   拷代码到 pod,直接从 mount 进来的目录读),但这个 URL 没经过
   ConstructUnitURL 这条已 patched 的路径。修:godoc 加 BasePath
   包级 var(同 IncludeUnexported 模式),renderOptions 里加 localPrefix
   闭包识别 "/files/" 起头的 local URL 前置 BasePath。GitHub 远程 URL
   (http:// 起头)不受影响,跟上游一致。
   smoke:view source URL 现是 "/gogodocs/files/.../poll_loop.go" 且 HTTP 200
   能正常 serve 源码内容。
两个 follow-up bug fix + UX 增强:

1. bold 跨行漏识别:boldRe 旧 pattern \`\`\\*\\*([^*\\n]+)\\*\\*\`\` 排除 \\n
   导致跨行 bold 不匹配——godoc 段落渲染保留段内 \\n,bold 写多行
   是常见用法(如 weixin/poll_loop.go 里 \`\`**Redis cursor(wx:get_updates_buf:{upn})
   只在本批所有 handler 跑完后才推进**\`\` 这种)。改成 \`\`\\*\\*([^*<]{1,500})\\*\\*\`\`
   容许 \\n + 用 [^<] 防跨 HTML 段落标签 + 500 字符上限双重防御
   贪心跨段。smoke:\`\`<strong>Redis cursor(...)只在本批所有 handler
   跑完后才推进</strong>\`\` 现正确包标签

2. unexported 符号 toggle(用户报告"私有太多反而拖慢阅读,但有时
   要看"):static/frontend/unit/main/main.ts 末尾加 IIFE,按 id
   首字母大小写自动给私有 declaration / index 链接的 wrapper 加
   .Documentation-unexported class,默认 CSS 隐藏;Index header
   旁注入 Show / Hide unexported toggle button,状态写
   localStorage 跨页保留。method id "Type.method" 取最后段判私有。
   const / var 不动(数量少且 godoc 整组渲染共享 declaration block,
   单独 hide 个别名字会破坏视觉)。
之前只标 .Documentation-index* 中央列表,左侧边栏 .go-Tree outline 没
覆盖——侧边栏私有函数链接还是显示。selector 加 '.go-Tree a[href^="#"]'。

isUnexported 加头部 anchor 排除:pkg- / section- / hdr- 起头的是页面
导航锚点(pkg-overview / section-readme / hdr-... 等),全小写但不是
Go 符号,扫到会误标整段隐藏。
P1 改 dochtml.LoadTemplates 签名加 basePath 时只补了 dochtml 包内
test(dochtml_test.go / symbol_test.go),漏了:
- internal/godoc/render_test.go 30 / 94
- internal/fetch/fetch_test.go 43

加空 basePath 第二个参数。

注:跑 fork 全 test 套件还有一个失败:TestFetchModule/master_version_of_
stdlib_module/stdlibzip 期望 commit hash 89fb59e2e920 实际 2b6a2e56778f。
切回 pristine upstream/master 复现同样 fail——stdlib zip 内容随 Go 版本
变化但 fixture 写死,是上游历史问题,跟 fork patch 无关。
P1 改这两个函数签名加 basePath 时还有 5 处漏:
- internal/worker/server.go (prod worker 二进制;fork 主走 cmd/pkgsite
  但仍要 build 通过)
- internal/worker/server_test.go
- internal/fetchdatasource/fetchdatasource_test.go
- internal/tests/templates/templates_test.go (含 ParsePageTemplates)
- internal/testing/integration/integration_test.go

加空 basePath 第二参数。go vet ./... + go test ./... 全过(除 1 个
TestFetchModule/master_version_of_stdlib_module/stdlibzip——这是上游
fixture 漂移,pristine upstream/master 同样 fail,跟 fork patch 无关)。

golangci-lint 24 issues(errcheck / staticcheck)全是上游历史问题不动。
GitHub repo 已 rename NickWilde18/gogodocs → NickWilde18/pkgsitex;
本仓库内部 17 个文件里所有 \"gogodocs\" 字面量替换为 \"pkgsitex\",覆盖:

- cmd/pkgsite/main.go:-base-path flag 默认值文案 + comment
- cmd/internal/pkgsite/server.go:ServerConfig.BasePath 注释
- internal/frontend/server.go / versions/versions.go / templates/templates.go
  / fetchserver/fetch.go:godoc 注释里举例 URL 全换
- internal/godoc/render.go / dochtml/dochtml.go / dochtml/template.go:
  PackageURL 拼接逻辑注释 + IncludeUnexported 包级 var 注释
- internal/godoc/dochtml/internal/render/markdown_ext.go:注释举例
- static/frontend/unit/main/main.ts:localStorage key
  gogodocs:showUnexported → pkgsitex:showUnexported(影响:之前
  toggle 状态用户需要重新点一次切换)
- static/frontend/about/index.ts、static/shared/base-path/base-path.ts:
  注释里举例 base path
- Dockerfile:org.opencontainers.image.source label

esbuild 已重 build 静态 bundle 同步反映 main.ts / about.ts 改动。

不动 git commit history(之前的 commit message 历史保留 \"gogodocs\"
字面量)。fork 仓库 PR #1 标题 / description 待 update。

Smoke test:本地起 \`-base-path=/pkgsitex\`,主页 + chat.cuhksz module
HTTP 200,渲染 HTML 残留 \"gogodocs\" 引用 0 个。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant