Files
Logan.Li bf377f3b55 refactor(homepage): 官网/文档/定价源码迁出程序 + 新增独立授权与激活页 /license
- 官网源码归拢到 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 隐藏页自身侧栏/上下页行为随之收敛)
2026-10-16 04:18:44 +08:00

229 lines
7.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}