# core.help Hugo Documentation Site — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Build a Hugo + Docsy documentation site at core.help that aggregates markdown from 39 repos via `core docs sync --target hugo`. **Architecture:** Hugo static site with Docsy theme, populated by extending `core docs sync` with a `--target hugo` flag that maps repo docs into Hugo's `content/` tree with auto-injected front matter. Deploy to BunnyCDN. **Tech Stack:** Hugo (Go SSG), Docsy theme (Hugo module), BunnyCDN, `core docs sync` CLI --- ## Context The docs sync command lives in `/Users/snider/Code/host-uk/cli/cmd/docs/`. The site will be scaffolded at `/Users/snider/Code/host-uk/docs-site/`. The registry at `/Users/snider/Code/host-uk/.core/repos.yaml` already contains all 39 repos (20 PHP + 18 Go + 1 CLI) with explicit paths for Go repos. Key files: - `/Users/snider/Code/host-uk/cli/cmd/docs/cmd_sync.go` — sync command (modify) - `/Users/snider/Code/host-uk/cli/cmd/docs/cmd_scan.go` — repo scanner (modify) - `/Users/snider/Code/host-uk/docs-site/` — Hugo site (create) ## Task 1: Scaffold Hugo + Docsy site **Files:** - Create: `/Users/snider/Code/host-uk/docs-site/hugo.toml` - Create: `/Users/snider/Code/host-uk/docs-site/go.mod` - Create: `/Users/snider/Code/host-uk/docs-site/content/_index.md` - Create: `/Users/snider/Code/host-uk/docs-site/content/getting-started/_index.md` - Create: `/Users/snider/Code/host-uk/docs-site/content/cli/_index.md` - Create: `/Users/snider/Code/host-uk/docs-site/content/go/_index.md` - Create: `/Users/snider/Code/host-uk/docs-site/content/mcp/_index.md` - Create: `/Users/snider/Code/host-uk/docs-site/content/php/_index.md` - Create: `/Users/snider/Code/host-uk/docs-site/content/kb/_index.md` This is the one-time Hugo scaffolding. No tests — just files. **`hugo.toml`:** ```toml baseURL = "https://core.help/" title = "Core Documentation" languageCode = "en" defaultContentLanguage = "en" enableRobotsTXT = true enableGitInfo = false [outputs] home = ["HTML", "JSON"] section = ["HTML"] [params] description = "Documentation for the Core CLI, Go packages, PHP modules, and MCP tools" copyright = "Host UK — EUPL-1.2" [params.ui] sidebar_menu_compact = true breadcrumb_disable = false sidebar_search_disable = false navbar_logo = false [params.ui.readingtime] enable = false [module] proxy = "direct" [module.hugoVersion] extended = true min = "0.120.0" [[module.imports]] path = "github.com/google/docsy" disable = false [markup.goldmark.renderer] unsafe = true [menu] [[menu.main]] name = "Getting Started" weight = 10 url = "/getting-started/" [[menu.main]] name = "CLI Reference" weight = 20 url = "/cli/" [[menu.main]] name = "Go Packages" weight = 30 url = "/go/" [[menu.main]] name = "MCP Tools" weight = 40 url = "/mcp/" [[menu.main]] name = "PHP Packages" weight = 50 url = "/php/" [[menu.main]] name = "Knowledge Base" weight = 60 url = "/kb/" ``` **`go.mod`:** ``` module github.com/host-uk/docs-site go 1.22 require github.com/google/docsy v0.11.0 ``` Note: Run `hugo mod get` after creating these files to populate `go.sum` and download Docsy. **Section `_index.md` files** — each needs Hugo front matter: `content/_index.md`: ```markdown --- title: "Core Documentation" description: "Documentation for the Core CLI, Go packages, PHP modules, and MCP tools" --- Welcome to the Core ecosystem documentation. ## Sections - [Getting Started](/getting-started/) — Installation, configuration, and first steps - [CLI Reference](/cli/) — Command reference for `core` CLI - [Go Packages](/go/) — Go ecosystem package documentation - [MCP Tools](/mcp/) — Model Context Protocol tool reference - [PHP Packages](/php/) — PHP module documentation - [Knowledge Base](/kb/) — Wiki articles and deep dives ``` `content/getting-started/_index.md`: ```markdown --- title: "Getting Started" linkTitle: "Getting Started" weight: 10 description: "Installation, configuration, and first steps with the Core CLI" --- ``` `content/cli/_index.md`: ```markdown --- title: "CLI Reference" linkTitle: "CLI Reference" weight: 20 description: "Command reference for the core CLI tool" --- ``` `content/go/_index.md`: ```markdown --- title: "Go Packages" linkTitle: "Go Packages" weight: 30 description: "Documentation for the Go ecosystem packages" --- ``` `content/mcp/_index.md`: ```markdown --- title: "MCP Tools" linkTitle: "MCP Tools" weight: 40 description: "Model Context Protocol tool reference — file operations, RAG, ML inference, process management" --- ``` `content/php/_index.md`: ```markdown --- title: "PHP Packages" linkTitle: "PHP Packages" weight: 50 description: "Documentation for the PHP module ecosystem" --- ``` `content/kb/_index.md`: ```markdown --- title: "Knowledge Base" linkTitle: "Knowledge Base" weight: 60 description: "Wiki articles, deep dives, and reference material" --- ``` **Verify:** After creating files, run from `/Users/snider/Code/host-uk/docs-site/`: ```bash hugo mod get hugo server ``` The site should start and show the landing page with Docsy theme at `localhost:1313`. **Commit:** ```bash cd /Users/snider/Code/host-uk/docs-site git init git add . git commit -m "feat: scaffold Hugo + Docsy documentation site" ``` --- ## Task 2: Extend scanRepoDocs to collect KB/ and README **Files:** - Modify: `/Users/snider/Code/host-uk/cli/cmd/docs/cmd_scan.go` Currently `scanRepoDocs` only collects files from `docs/`. For the Hugo target we also need: - `KB/**/*.md` files (wiki pages from go-mlx, go-i18n) - `README.md` content (becomes the package _index.md) Add a `KBFiles []string` field to `RepoDocInfo` and scan `KB/` alongside `docs/`: ```go type RepoDocInfo struct { Name string Path string HasDocs bool Readme string ClaudeMd string Changelog string DocsFiles []string // All files in docs/ directory (recursive) KBFiles []string // All files in KB/ directory (recursive) } ``` In `scanRepoDocs`, after the `docs/` walk, add a second walk for `KB/`: ```go // Recursively scan KB/ directory for .md files kbDir := filepath.Join(repo.Path, "KB") if _, err := io.Local.List(kbDir); err == nil { _ = filepath.WalkDir(kbDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return nil } if d.IsDir() || !strings.HasSuffix(d.Name(), ".md") { return nil } relPath, _ := filepath.Rel(kbDir, path) info.KBFiles = append(info.KBFiles, relPath) info.HasDocs = true return nil }) } ``` **Tests:** The existing tests should still pass. No new test file needed — this is a data-collection change. **Verify:** `cd /Users/snider/Code/host-uk/cli && GOWORK=off go build ./cmd/docs/...` **Commit:** ```bash git add cmd/docs/cmd_scan.go git commit -m "feat(docs): scan KB/ directory alongside docs/" ``` --- ## Task 3: Add `--target hugo` flag and Hugo sync logic **Files:** - Modify: `/Users/snider/Code/host-uk/cli/cmd/docs/cmd_sync.go` This is the main task. Add a `--target` flag (default `"php"`) and a new `runHugoSync` function that maps repos to Hugo's content tree. **Add flag variable and registration:** ```go var ( docsSyncRegistryPath string docsSyncDryRun bool docsSyncOutputDir string docsSyncTarget string ) func init() { docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", i18n.T("common.flag.registry")) docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, i18n.T("cmd.docs.sync.flag.dry_run")) docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", i18n.T("cmd.docs.sync.flag.output")) docsSyncCmd.Flags().StringVar(&docsSyncTarget, "target", "php", "Target format: php (default) or hugo") } ``` **Update RunE to pass target:** ```go RunE: func(cmd *cli.Command, args []string) error { return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun, docsSyncTarget) }, ``` **Update `runDocsSync` signature and add target dispatch:** ```go func runDocsSync(registryPath string, outputDir string, dryRun bool, target string) error { reg, basePath, err := loadRegistry(registryPath) if err != nil { return err } switch target { case "hugo": return runHugoSync(reg, basePath, outputDir, dryRun) default: return runPHPSync(reg, basePath, outputDir, dryRun) } } ``` **Rename current sync body to `runPHPSync`** — extract lines 67-159 of current `runDocsSync` into `runPHPSync(reg, basePath, outputDir string, dryRun bool) error`. This is a pure extract, no logic changes. **Add `hugoOutputName` mapping function:** ```go // hugoOutputName maps repo name to Hugo content section and folder. // Returns (section, folder) where section is the top-level content dir. func hugoOutputName(repoName string) (string, string) { // CLI guides if repoName == "cli" { return "getting-started", "" } // Core CLI command docs if repoName == "core" { return "cli", "" } // Go packages if strings.HasPrefix(repoName, "go-") { return "go", repoName } // PHP packages if strings.HasPrefix(repoName, "core-") { return "php", strings.TrimPrefix(repoName, "core-") } return "go", repoName } ``` **Add front matter injection helper:** ```go // injectFrontMatter prepends Hugo front matter to markdown content if missing. func injectFrontMatter(content []byte, title string, weight int) []byte { // Already has front matter if bytes.HasPrefix(bytes.TrimSpace(content), []byte("---")) { return content } fm := fmt.Sprintf("---\ntitle: %q\nweight: %d\n---\n\n", title, weight) return append([]byte(fm), content...) } // titleFromFilename derives a human-readable title from a filename. func titleFromFilename(filename string) string { name := strings.TrimSuffix(filepath.Base(filename), ".md") name = strings.ReplaceAll(name, "-", " ") name = strings.ReplaceAll(name, "_", " ") // Title case words := strings.Fields(name) for i, w := range words { if len(w) > 0 { words[i] = strings.ToUpper(w[:1]) + w[1:] } } return strings.Join(words, " ") } ``` **Add `runHugoSync` function:** ```go func runHugoSync(reg *repos.Registry, basePath string, outputDir string, dryRun bool) error { if outputDir == "" { outputDir = filepath.Join(basePath, "docs-site", "content") } // Scan all repos var docsInfo []RepoDocInfo for _, repo := range reg.List() { if repo.Name == "core-template" || repo.Name == "core-claude" { continue } info := scanRepoDocs(repo) if info.HasDocs { docsInfo = append(docsInfo, info) } } if len(docsInfo) == 0 { cli.Text("No documentation found") return nil } cli.Print("\n Hugo sync: %d repos with docs → %s\n\n", len(docsInfo), outputDir) // Show plan for _, info := range docsInfo { section, folder := hugoOutputName(info.Name) target := section if folder != "" { target = section + "/" + folder } fileCount := len(info.DocsFiles) + len(info.KBFiles) if info.Readme != "" { fileCount++ } cli.Print(" %s → %s/ (%d files)\n", repoNameStyle.Render(info.Name), target, fileCount) } if dryRun { cli.Print("\n Dry run — no files written\n") return nil } cli.Blank() if !confirm("Sync to Hugo content directory?") { cli.Text("Aborted") return nil } cli.Blank() var synced int for _, info := range docsInfo { section, folder := hugoOutputName(info.Name) // Build destination path destDir := filepath.Join(outputDir, section) if folder != "" { destDir = filepath.Join(destDir, folder) } // Copy docs/ files weight := 10 docsDir := filepath.Join(info.Path, "docs") for _, f := range info.DocsFiles { src := filepath.Join(docsDir, f) dst := filepath.Join(destDir, f) if err := copyWithFrontMatter(src, dst, weight); err != nil { cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err) continue } weight += 10 } // Copy README.md as _index.md (if not CLI/core which use their own index) if info.Readme != "" && folder != "" { dst := filepath.Join(destDir, "_index.md") if err := copyWithFrontMatter(info.Readme, dst, 1); err != nil { cli.Print(" %s README: %s\n", errorStyle.Render("✗"), err) } } // Copy KB/ files to kb/{suffix}/ if len(info.KBFiles) > 0 { // Extract suffix: go-mlx → mlx, go-i18n → i18n suffix := strings.TrimPrefix(info.Name, "go-") kbDestDir := filepath.Join(outputDir, "kb", suffix) kbDir := filepath.Join(info.Path, "KB") kbWeight := 10 for _, f := range info.KBFiles { src := filepath.Join(kbDir, f) dst := filepath.Join(kbDestDir, f) if err := copyWithFrontMatter(src, dst, kbWeight); err != nil { cli.Print(" %s KB/%s: %s\n", errorStyle.Render("✗"), f, err) continue } kbWeight += 10 } } cli.Print(" %s %s\n", successStyle.Render("✓"), info.Name) synced++ } cli.Print("\n Synced %d repos to Hugo content\n", synced) return nil } // copyWithFrontMatter copies a markdown file, injecting front matter if missing. func copyWithFrontMatter(src, dst string, weight int) error { if err := io.Local.EnsureDir(filepath.Dir(dst)); err != nil { return err } content, err := io.Local.Read(src) if err != nil { return err } title := titleFromFilename(src) result := injectFrontMatter([]byte(content), title, weight) return io.Local.Write(dst, string(result)) } ``` **Add imports** at top of file: ```go import ( "bytes" "fmt" "path/filepath" "strings" "forge.lthn.ai/core/go/pkg/cli" "forge.lthn.ai/core/go/pkg/i18n" "forge.lthn.ai/core/go/pkg/io" "forge.lthn.ai/core/go/pkg/repos" ) ``` **Verify:** `cd /Users/snider/Code/host-uk/cli && GOWORK=off go build ./cmd/docs/...` **Commit:** ```bash git add cmd/docs/cmd_sync.go git commit -m "feat(docs): add --target hugo sync mode for core.help" ``` --- ## Task 4: Test the full pipeline **No code changes.** Run the pipeline end-to-end. **Step 1:** Sync docs to Hugo: ```bash cd /Users/snider/Code/host-uk core docs sync --target hugo --dry-run ``` Verify all 39 repos appear with correct section mappings. **Step 2:** Run actual sync: ```bash core docs sync --target hugo ``` **Step 3:** Build and preview: ```bash cd /Users/snider/Code/host-uk/docs-site hugo server ``` Open `localhost:1313` and verify: - Landing page renders with section links - Getting Started section has CLI guides - CLI Reference section has command docs - Go Packages section has 18 packages with architecture/development/history - PHP Packages section has PHP module docs - Knowledge Base has MLX and i18n wiki pages - Navigation works, search works **Step 4:** Fix any issues found during preview. **Commit docs-site content:** ```bash cd /Users/snider/Code/host-uk/docs-site git add content/ git commit -m "feat: sync initial content from 39 repos" ``` --- ## Task 5: BunnyCDN deployment config **Files:** - Modify: `/Users/snider/Code/host-uk/docs-site/hugo.toml` Add deployment target: ```toml [deployment] [[deployment.targets]] name = "production" URL = "s3://core-help?endpoint=storage.bunnycdn.com®ion=auto" ``` Add a `Taskfile.yml` for convenience: **Create:** `/Users/snider/Code/host-uk/docs-site/Taskfile.yml` ```yaml version: '3' tasks: dev: desc: Start Hugo dev server cmds: - hugo server --buildDrafts build: desc: Build static site cmds: - hugo --minify sync: desc: Sync docs from all repos dir: .. cmds: - core docs sync --target hugo deploy: desc: Build and deploy to BunnyCDN cmds: - task: sync - task: build - hugo deploy --target production clean: desc: Remove generated content (keeps _index.md files) cmds: - find content -name "*.md" ! -name "_index.md" -delete ``` **Verify:** `task dev` starts the site. **Commit:** ```bash git add hugo.toml Taskfile.yml git commit -m "feat: add BunnyCDN deployment config and Taskfile" ``` --- ## Dependency Sequencing ``` Task 1 (Hugo scaffold) — independent, do first Task 2 (scan KB/) — independent, can parallel with Task 1 Task 3 (--target hugo) — depends on Task 2 Task 4 (test pipeline) — depends on Tasks 1 + 3 Task 5 (deploy config) — depends on Task 1 ``` ## Verification After all tasks: 1. `core docs sync --target hugo` populates `docs-site/content/` from all repos 2. `cd docs-site && hugo server` renders the full site 3. Navigation has 6 sections: Getting Started, CLI, Go, MCP, PHP, KB 4. All existing markdown renders correctly with auto-injected front matter 5. `hugo build` produces `public/` with no errors