- 官网源码归拢到 homepage/(index/pricing/license + docs.html + content/ + assets/ + docs-assets/),
删除程序内 go:embed 的 server/internal/{site,docs},后端只静态托管 homepage/dist 做本地预览
(dist 缺失时打 warn 日志,不再内置兜底;/home 别名保留供 GUI openExternal 使用)
- 文档 manifest 按现网口径重建:MCP 页标记 hidden(不再进侧栏与搜索)、补 快捷键 页、校正 updated 日期
- 新增独立静态页 /license:交付与激活四步(邮件交付授权码)、LIC- 在线 与 LICOFF- 离线 对比、
授权服务器 licence.rlwj.cn 通信范围说明、授权类型与价格、10 条授权 FAQ
- tools/homepage-build 支持 hidden 页与可选单页,产出 <name>.html 与 <name>/index.html
- 导航/页脚接入 /license,跨页锚点补 / 前缀,页脚底栏文案同步为静态站口径
- 产物零漂移:12 页文档与现网逐字节一致(仅 mcp 隐藏页自身侧栏/上下页行为随之收敛)
229 lines
7.6 KiB
Go
229 lines
7.6 KiB
Go
// homepage-build:把 homepage/ 源(落地页 + 定价页 + 文档 md)构建为纯静态站点 homepage/dist/。
|
||
// 产物可直接部署到任意静态托管(nginx/Pages)——官网与后端二进制彻底解耦。
|
||
// 用法: go run . [-src homepage] [-out homepage/dist]
|
||
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"html/template"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"github.com/yuin/goldmark"
|
||
"github.com/yuin/goldmark/extension"
|
||
gmhtml "github.com/yuin/goldmark/renderer/html"
|
||
)
|
||
|
||
type PageDef struct {
|
||
File string `json:"file"`
|
||
Slug string `json:"slug"`
|
||
Title string `json:"title"`
|
||
Updated string `json:"updated,omitempty"`
|
||
Hidden bool `json:"hidden,omitempty"`
|
||
}
|
||
type GroupDef struct {
|
||
Name string `json:"name"`
|
||
Pages []PageDef `json:"pages"`
|
||
}
|
||
type Manifest struct {
|
||
ProductVersion string `json:"productVersion"`
|
||
Groups []GroupDef `json:"groups"`
|
||
}
|
||
type TocItem struct {
|
||
ID string
|
||
Level int
|
||
Text string
|
||
}
|
||
type SearchEntry struct {
|
||
Slug, Title, Group, Text string
|
||
}
|
||
type NavLink struct {
|
||
Slug, Title string
|
||
}
|
||
type Page struct {
|
||
Slug, Title, Group, Updated string
|
||
HTML template.HTML
|
||
TOC []TocItem
|
||
Prev, Next *NavLink
|
||
}
|
||
|
||
var (
|
||
hRe = regexp.MustCompile(`(?s)<(h[23])>(.*?)</h[23]>`)
|
||
tagRe = regexp.MustCompile(`<[^>]+>`)
|
||
mdNoise = regexp.MustCompile("(?s)```.*?```|`[^`]*`|!\\[[^\\]]*\\]\\([^)]*\\)|\\[[^\\]]*\\]\\([^)]*\\)|[#>*_\\-|]+")
|
||
)
|
||
|
||
func injectHeadingIDs(html string) (string, []TocItem) {
|
||
var toc []TocItem
|
||
counter := 0
|
||
out := hRe.ReplaceAllStringFunc(html, func(m string) string {
|
||
sub := hRe.FindStringSubmatch(m)
|
||
tag, inner := sub[1], sub[2]
|
||
counter++
|
||
id := fmt.Sprintf("sec-%d", counter)
|
||
text := strings.TrimSpace(tagRe.ReplaceAllString(inner, ""))
|
||
level := 2
|
||
if tag == "h3" {
|
||
level = 3
|
||
}
|
||
toc = append(toc, TocItem{ID: id, Level: level, Text: text})
|
||
return fmt.Sprintf(`<%s id="%s">%s</%s>`, tag, id, inner, tag)
|
||
})
|
||
return out, toc
|
||
}
|
||
|
||
func plainText(src string) string {
|
||
s := mdNoise.ReplaceAllString(src, " ")
|
||
s = strings.Join(strings.Fields(s), " ")
|
||
if len(s) > 6000 {
|
||
s = s[:6000]
|
||
}
|
||
return s
|
||
}
|
||
|
||
func must(err error, what string) {
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "[homepage-build] %s: %v\n", what, err)
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
func writeFile(path string, content []byte) {
|
||
must(os.MkdirAll(filepath.Dir(path), 0o755), "mkdir "+path)
|
||
must(os.WriteFile(path, content, 0o644), "write "+path)
|
||
fmt.Println("[OK]", path)
|
||
}
|
||
|
||
// visibleManifest 返回去掉 hidden 页的分组清单:模板侧栏、上/下一页、搜索索引都只含可见页。
|
||
// Logan.Li
|
||
func visibleManifest(m Manifest) Manifest {
|
||
vm := Manifest{ProductVersion: m.ProductVersion}
|
||
for _, g := range m.Groups {
|
||
ng := GroupDef{Name: g.Name}
|
||
for _, p := range g.Pages {
|
||
if !p.Hidden {
|
||
ng.Pages = append(ng.Pages, p)
|
||
}
|
||
}
|
||
if len(ng.Pages) > 0 {
|
||
vm.Groups = append(vm.Groups, ng)
|
||
}
|
||
}
|
||
return vm
|
||
}
|
||
|
||
func main() {
|
||
src := flag.String("src", "homepage", "源目录")
|
||
out := flag.String("out", "homepage/dist", "产物目录")
|
||
flag.Parse()
|
||
|
||
// 幂等重建:清理上一轮生成的子目录(os.CopyFS 目标已存在会报错)
|
||
for _, d := range []string{"site", "docs", "pricing", "home"} {
|
||
must(os.RemoveAll(filepath.Join(*out, d)), "清理 "+d)
|
||
}
|
||
|
||
// 1) 落地页 + 别名 + 可选单页:纯 HTML 直接拷贝
|
||
raw, err := os.ReadFile(filepath.Join(*src, "index.html"))
|
||
must(err, "读 index.html")
|
||
writeFile(filepath.Join(*out, "index.html"), raw)
|
||
// /home 别名:对齐 nginx try_files $uri/index.html,GUI openExternal("/home") 依赖它
|
||
writeFile(filepath.Join(*out, "home", "index.html"), raw)
|
||
|
||
// 可选单页(pricing / terms / license):根 <name>.html + <name>/index.html
|
||
// (对齐 nginx location = /pricing|/license → 目录内 index;纯静态服务器上 /pricing/ 亦为 200)// Logan.Li
|
||
for _, name := range []string{"pricing.html", "terms.html", "license.html"} {
|
||
b, e := os.ReadFile(filepath.Join(*src, name))
|
||
if e != nil {
|
||
continue
|
||
}
|
||
writeFile(filepath.Join(*out, name), b)
|
||
writeFile(filepath.Join(*out, strings.TrimSuffix(name, ".html"), "index.html"), b)
|
||
}
|
||
// 站点资源:落地页/定价页引用 /site/assets/*
|
||
siteAssets := filepath.Join(*src, "assets")
|
||
if st, _ := os.Stat(siteAssets); st != nil {
|
||
must(os.CopyFS(filepath.Join(*out, "site", "assets"), os.DirFS(siteAssets)), "拷 site assets")
|
||
fmt.Println("[OK]", filepath.Join(*out, "site", "assets"))
|
||
}
|
||
|
||
// 2) 文档:manifest → goldmark 渲染 → docs.html 模板(Go html/template)
|
||
mraw, err := os.ReadFile(filepath.Join(*src, "content", "manifest.json"))
|
||
must(err, "读 manifest")
|
||
var manifest Manifest
|
||
must(json.Unmarshal(mraw, &manifest), "解析 manifest")
|
||
vis := visibleManifest(manifest)
|
||
|
||
tplRaw, err := os.ReadFile(filepath.Join(*src, "docs.html"))
|
||
must(err, "读 docs.html")
|
||
tpl, err := template.New("doc").Parse(string(tplRaw))
|
||
must(err, "解析 docs 模板")
|
||
md := goldmark.New(goldmark.WithExtensions(extension.Table, extension.Strikethrough, extension.TaskList), goldmark.WithRendererOptions(gmhtml.WithHardWraps()))
|
||
|
||
// 可见页顺序(侧栏 / 上下一页 / 搜索)
|
||
var order []string
|
||
pages := map[string]*Page{}
|
||
var search []SearchEntry
|
||
var prev *NavLink
|
||
for _, g := range vis.Groups {
|
||
for _, def := range g.Pages {
|
||
srcb, err := os.ReadFile(filepath.Join(*src, "content", def.File))
|
||
must(err, "读文档 "+def.File)
|
||
var buf strings.Builder
|
||
must(md.Convert(srcb, &buf), "渲染 "+def.File)
|
||
body, toc := injectHeadingIDs(buf.String())
|
||
p := &Page{Slug: def.Slug, Title: def.Title, Group: g.Name, Updated: def.Updated, HTML: template.HTML(body), TOC: toc, Prev: prev}
|
||
pages[def.Slug] = p
|
||
order = append(order, def.Slug)
|
||
prev = &NavLink{Slug: def.Slug, Title: def.Title}
|
||
search = append(search, SearchEntry{Slug: def.Slug, Title: def.Title, Group: g.Name, Text: plainText(string(srcb))})
|
||
}
|
||
}
|
||
for i, slug := range order {
|
||
if i+1 < len(order) {
|
||
n := pages[order[i+1]]
|
||
pages[slug].Next = &NavLink{Slug: n.Slug, Title: n.Title}
|
||
}
|
||
}
|
||
|
||
render := func(p *Page) []byte {
|
||
var sb strings.Builder
|
||
must(tpl.Execute(&sb, map[string]any{"Manifest": vis, "Page": p, "TOC": p.TOC, "Content": p.HTML}), "渲染模板 "+p.Slug)
|
||
return []byte(sb.String())
|
||
}
|
||
// /docs/ 首页 = 第一篇(静态站无重定向,直接渲染)
|
||
writeFile(filepath.Join(*out, "docs", "index.html"), render(pages[order[0]]))
|
||
for _, slug := range order {
|
||
writeFile(filepath.Join(*out, "docs", slug, "index.html"), render(pages[slug]))
|
||
}
|
||
// hidden 页:仍渲染为独立页面(不进侧栏/导航/搜索),供 /docs/<slug> 直达
|
||
for _, g := range manifest.Groups {
|
||
for _, def := range g.Pages {
|
||
if !def.Hidden {
|
||
continue
|
||
}
|
||
srcb, err := os.ReadFile(filepath.Join(*src, "content", def.File))
|
||
must(err, "读隐藏文档 "+def.File)
|
||
var buf strings.Builder
|
||
must(md.Convert(srcb, &buf), "渲染 "+def.File)
|
||
body, toc := injectHeadingIDs(buf.String())
|
||
p := &Page{Slug: def.Slug, Title: def.Title, Group: g.Name, Updated: def.Updated, HTML: template.HTML(body), TOC: toc}
|
||
writeFile(filepath.Join(*out, "docs", def.Slug, "index.html"), render(p))
|
||
}
|
||
}
|
||
sj, _ := json.Marshal(search)
|
||
writeFile(filepath.Join(*out, "docs", "search-data.json"), sj)
|
||
|
||
// docs assets(模板引用 /docs/assets/*)
|
||
da := filepath.Join(*src, "docs-assets")
|
||
if st, _ := os.Stat(da); st != nil {
|
||
must(os.CopyFS(filepath.Join(*out, "docs", "assets"), os.DirFS(da)), "拷 docs assets")
|
||
fmt.Println("[OK]", filepath.Join(*out, "docs", "assets"))
|
||
}
|
||
|
||
fmt.Printf("[DONE] 落地页 + %d 页文档(+隐藏页)→ %s(纯静态,可部署任意域名)\n", len(order), *out)
|
||
}
|