feat(docsite): optional site_url and site_label link back to the main site

- site.yaml keys site_url and site_label, validated: http(s) URL with a host
  or a path starting with a single /; a label needs a URL
- docs:build and docs:serve flags --site-url and --site-label override them
  the way --base-url overrides base_url
- every page header, the 404 page included, links back with the explicit
  label, else the URL host, else Home; unset output is unchanged
- docs/console/utilities.md documents the keys and flags
This commit is contained in:
Jakub Zych
2026-10-01 16:09:40 +02:00
parent 7936234e8c
commit a494375db7
10 changed files with 207 additions and 10 deletions

View File

@@ -29,6 +29,8 @@ func docsBuildCommand() bonfire.Command {
{Name: "src", Description: "Docs source directory (default <root>/docs)"},
{Name: "out", Description: "Output directory (default <root>/site)"},
{Name: "base-url", Description: "Base URL for site links (overrides site.yaml base_url)"},
{Name: "site-url", Description: "Main site URL linked from the docs header (overrides site.yaml site_url)"},
{Name: "site-label", Description: "Label of the main site link (overrides site.yaml site_label; default: the URL host, or Home)"},
{Name: "check", Description: "Validate the docs and write nothing", Bare: true},
},
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
@@ -91,6 +93,8 @@ func docsServeCommand() bonfire.Command {
{Name: "root", Description: "Repository root; src= paths and modules/ resolve against it", Default: "."},
{Name: "src", Description: "Docs source directory (default <root>/docs)"},
{Name: "base-url", Description: "Base URL for site links (overrides site.yaml base_url)"},
{Name: "site-url", Description: "Main site URL linked from the docs header (overrides site.yaml site_url)"},
{Name: "site-label", Description: "Label of the main site link (overrides site.yaml site_label; default: the URL host, or Home)"},
{Name: "addr", Description: "Listen address; must be loopback unless --allow-remote", Default: docsite.DefaultServeAddr},
{Name: "allow-remote", Description: "Allow a non-loopback --addr (serves the docs on the network)", Bare: true},
},
@@ -126,6 +130,8 @@ func docsOptions(in bonfire.Input) docsite.Options {
opts.Src, _ = in.Flag("src")
opts.Out, _ = in.Flag("out")
opts.BaseURL, _ = in.Flag("base-url")
opts.SiteURL, _ = in.Flag("site-url")
opts.SiteLabel, _ = in.Flag("site-label")
return opts
}

View File

@@ -72,6 +72,40 @@ func TestDocsBuildRealTree(t *testing.T) {
}
}
// TestDocsBuildSiteFlags checks that --site-url and --site-label reach the
// header and that an unsafe --site-url is refused.
func TestDocsBuildSiteFlags(t *testing.T) {
out := filepath.Join(t.TempDir(), "site")
var buf bytes.Buffer
root, err := bonfire.NewRoot("summer", toolCommands(), &buf)
if err != nil {
t.Fatal(err)
}
root.SetArgs([]string{"docs:build", "--root", repoRoot, "--out", out, "--site-url", "/", "--site-label", "example.org"})
if err := root.Execute(); err != nil {
t.Fatalf("docs:build: %v\n%s", err, buf.String())
}
index, err := os.ReadFile(filepath.Join(out, "index.html"))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{`class="site-link" href="/"`, "example.org"} {
if !bytes.Contains(index, []byte(want)) {
t.Errorf("index.html lacks %s", want)
}
}
buf.Reset()
root, err = bonfire.NewRoot("summer", toolCommands(), &buf)
if err != nil {
t.Fatal(err)
}
root.SetArgs([]string{"docs:build", "--root", repoRoot, "--out", filepath.Join(t.TempDir(), "bad"), "--site-url", "javascript:alert(1)"})
if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "--site-url") {
t.Fatalf("docs:build --site-url javascript:alert(1): err = %v, want a --site-url error", err)
}
}
// requiredPages lists the guide pages the docs must keep. Each content plan
// appends the pages it writes.
var requiredPages = []string{