diff options
Diffstat (limited to 'internal/handlers/docs.go')
| -rw-r--r-- | internal/handlers/docs.go | 106 |
1 files changed, 106 insertions, 0 deletions
diff --git a/internal/handlers/docs.go b/internal/handlers/docs.go new file mode 100644 index 0000000..24732cb --- /dev/null +++ b/internal/handlers/docs.go @@ -0,0 +1,106 @@ +package handlers + +import ( + "html/template" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/yuin/goldmark" +) + +// docsCategories are the only subdirectories served under /docs -- a +// whitelist, not a filesystem listing, so an unexpected file dropped into +// DocsDir doesn't unintentionally become servable. +var docsCategories = []string{"specs", "plans"} + +type docsEntry struct { + Name string // filename, e.g. "2026-07-14-foo-design.md" + URL string +} + +type docsCategoryListing struct { + Name string + Entries []docsEntry +} + +// HandleDocsIndex lists every markdown file under each whitelisted category +// subdirectory of cfg.DocsDir, newest first (filenames are date-prefixed, +// so reverse-alphabetical is reverse-chronological). +func (h *Handler) HandleDocsIndex(w http.ResponseWriter, r *http.Request) { + var categories []docsCategoryListing + for _, cat := range docsCategories { + files, err := os.ReadDir(filepath.Join(h.config.DocsDir, cat)) + if err != nil { + continue + } + var entries []docsEntry + for _, f := range files { + if f.IsDir() || !strings.HasSuffix(f.Name(), ".md") { + continue + } + entries = append(entries, docsEntry{ + Name: f.Name(), + URL: "/docs/" + cat + "/" + f.Name(), + }) + } + sort.Slice(entries, func(i, j int) bool { return entries[i].Name > entries[j].Name }) + categories = append(categories, docsCategoryListing{Name: cat, Entries: entries}) + } + + if err := h.renderer.Render(w, "docs-index.html", struct { + Categories []docsCategoryListing + }{categories}); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + } +} + +// HandleDocsView renders a single markdown file to HTML. category and +// filename both come from the URL and are validated against a whitelist / +// base-name check before touching the filesystem, so a request can't +// escape DocsDir via "..": chi's {filename} route param never contains a +// literal "/", and Go's filepath.Base further strips any residual path +// separators before the read. +func (h *Handler) HandleDocsView(w http.ResponseWriter, r *http.Request) { + category := chi.URLParam(r, "category") + if !isValidDocsCategory(category) { + http.NotFound(w, r) + return + } + filename := filepath.Base(chi.URLParam(r, "filename")) + if !strings.HasSuffix(filename, ".md") { + http.NotFound(w, r) + return + } + + raw, err := os.ReadFile(filepath.Join(h.config.DocsDir, category, filename)) + if err != nil { + http.NotFound(w, r) + return + } + + var body strings.Builder + if err := goldmark.Convert(raw, &body); err != nil { + http.Error(w, "Failed to render markdown", http.StatusInternalServerError) + return + } + + if err := h.renderer.Render(w, "docs-view.html", struct { + Title string + Body template.HTML + }{filename, template.HTML(body.String())}); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + } +} + +func isValidDocsCategory(category string) bool { + for _, c := range docsCategories { + if c == category { + return true + } + } + return false +} |
