diff options
| author | Claude <noreply@anthropic.com> | 2026-03-31 12:44:10 +0200 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-03-31 12:44:10 +0200 |
| commit | aa23774abb90c168c9ba2559d6bf381bc9fc55ba (patch) | |
| tree | c4f4abfafb6e8e47a1ab505fe1253b951ccf77c9 /cmd/nebbet/main.go | |
| parent | 8d7cda6e578e684483c0b5c7391c48e5b9ac5192 (diff) | |
| download | nebbet.no-aa23774abb90c168c9ba2559d6bf381bc9fc55ba.tar.xz nebbet.no-aa23774abb90c168c9ba2559d6bf381bc9fc55ba.zip | |
Add post management admin UI and switch to modernc.org/sqlite
- Replace custom CGO sqlite driver with modernc.org/sqlite (registered as
"sqlite3"); a local compat shim under compat/sqlite/ provides the same
import path and WAL-mode behaviour using system libsqlite3 while network
access is unavailable — swapping to the real pure-Go package later only
requires removing the replace directive and running go get.
- Add internal/admin/server.go: HTTP handler for /admin/ that serves a
server-side-rendered post management UI (list, new, edit, delete). Posts
are stored as Markdown files under content/posts/ and rebuilt via the
existing Builder after every write. Basic auth is checked against the
.passwords file when it exists.
- Add cmd/nebbet/main.go: unified CLI with build, watch, serve (HTTP server
with admin + file server + watch), and user subcommands.
- Update builder.BuildAll to skip content/admin/ — admin pages are served
dynamically and must never appear in the static output directory.
- Mark content/admin/index.md as draft so the old static placeholder is
not built even if the admin skip logic is bypassed.
- Fix .gitignore: use /nebbet (root-only) so the pattern no longer
accidentally ignores the cmd/nebbet/ source directory.
https://claude.ai/code/session_01WLuSGxJhNs2cFM2zJzSsTx
Diffstat (limited to 'cmd/nebbet/main.go')
| -rw-r--r-- | cmd/nebbet/main.go | 229 |
1 files changed, 229 insertions, 0 deletions
diff --git a/cmd/nebbet/main.go b/cmd/nebbet/main.go new file mode 100644 index 0000000..1d8b3c0 --- /dev/null +++ b/cmd/nebbet/main.go @@ -0,0 +1,229 @@ +// Command nebbet is the CLI for the nebbet.no static site generator. +// +// Usage: +// +// nebbet build – build all pages once +// nebbet watch – watch for changes and rebuild +// nebbet serve [--port N] – serve with live admin UI + watch mode +// nebbet user add <name> – add a user to the passwords file +// nebbet user list – list users +// nebbet user delete <name> – remove a user +// nebbet user passwd <name> – change a user's password +package main + +import ( + "flag" + "fmt" + "net/http" + "os" + + "nebbet.no/internal/admin" + "nebbet.no/internal/auth" + "nebbet.no/internal/builder" + "nebbet.no/internal/db" +) + +const ( + contentDir = "content" + outputDir = "public" + postsDir = "content/posts" + dataDir = "data" + metaDBPath = "data/meta.db" + searchDBPath = "data/search.db" + passwordFile = ".passwords" +) + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(1) + } + switch os.Args[1] { + case "build": + cmdBuild(os.Args[2:]) + case "watch": + cmdWatch() + case "serve": + cmdServe(os.Args[2:]) + case "user": + cmdUser(os.Args[2:]) + default: + fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1]) + usage() + os.Exit(1) + } +} + +// ── build ───────────────────────────────────────────────────────────────────── + +func cmdBuild(args []string) { + fs := flag.NewFlagSet("build", flag.ExitOnError) + watch := fs.Bool("watch", false, "watch for changes and rebuild") + _ = fs.Parse(args) + + b := mustBuilder() + defer b.MetaDB.Close() + defer b.SearchDB.Close() + + if err := b.BuildAll(); err != nil { + fmt.Fprintln(os.Stderr, "build error:", err) + os.Exit(1) + } + if *watch { + if err := b.Watch(); err != nil { + fmt.Fprintln(os.Stderr, "watch error:", err) + os.Exit(1) + } + } +} + +// ── watch ───────────────────────────────────────────────────────────────────── + +func cmdWatch() { + b := mustBuilder() + defer b.MetaDB.Close() + defer b.SearchDB.Close() + if err := b.BuildAll(); err != nil { + fmt.Fprintln(os.Stderr, "build error:", err) + os.Exit(1) + } + if err := b.Watch(); err != nil { + fmt.Fprintln(os.Stderr, "watch error:", err) + os.Exit(1) + } +} + +// ── serve ───────────────────────────────────────────────────────────────────── + +func cmdServe(args []string) { + fs := flag.NewFlagSet("serve", flag.ExitOnError) + port := fs.String("port", "8080", "port to listen on") + _ = fs.Parse(args) + + b := mustBuilder() + defer b.MetaDB.Close() + defer b.SearchDB.Close() + + // Initial build. + if err := b.BuildAll(); err != nil { + fmt.Fprintln(os.Stderr, "build error:", err) + os.Exit(1) + } + + // Watch in background. + go func() { + if err := b.Watch(); err != nil { + fmt.Fprintln(os.Stderr, "watch error:", err) + } + }() + + adminSrv := &admin.Server{ + PostsDir: postsDir, + AuthFile: passwordFile, + Builder: b, + } + + mux := http.NewServeMux() + // Admin routes — handled dynamically, never from the static output dir. + mux.Handle("/admin/", http.StripPrefix("/admin", adminSrv)) + // Everything else — serve the pre-built static files. + mux.Handle("/", http.FileServer(http.Dir(outputDir))) + + addr := ":" + *port + fmt.Printf("listening on http://localhost%s\n", addr) + fmt.Printf(" public site: http://localhost%s/\n", addr) + fmt.Printf(" admin UI: http://localhost%s/admin/\n", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +// ── user ───────────────────────────────────────────────────────────────────── + +func cmdUser(args []string) { + if len(args) == 0 { + fmt.Fprintln(os.Stderr, "usage: nebbet user <add|list|delete|passwd> [username]") + os.Exit(1) + } + a := auth.New(passwordFile) + switch args[0] { + case "add": + requireArg(args, 1, "nebbet user add <username>") + if err := a.AddUser(args[1]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println("user added:", args[1]) + case "list": + users, err := a.ListUsers() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if len(users) == 0 { + fmt.Println("(no users)") + return + } + for _, u := range users { + fmt.Println(u) + } + case "delete": + requireArg(args, 1, "nebbet user delete <username>") + if err := a.DeleteUser(args[1]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println("user deleted:", args[1]) + case "passwd": + requireArg(args, 1, "nebbet user passwd <username>") + if err := a.ChangePassword(args[1]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println("password updated:", args[1]) + default: + fmt.Fprintf(os.Stderr, "unknown user subcommand %q\n", args[0]) + os.Exit(1) + } +} + +// ── helpers ─────────────────────────────────────────────────────────────────── + +func mustBuilder() *builder.Builder { + if err := os.MkdirAll(dataDir, 0755); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + meta, err := db.OpenMeta(metaDBPath) + if err != nil { + fmt.Fprintln(os.Stderr, "meta db:", err) + os.Exit(1) + } + search, err := db.OpenSearch(searchDBPath) + if err != nil { + fmt.Fprintln(os.Stderr, "search db:", err) + os.Exit(1) + } + return builder.New(contentDir, outputDir, meta, search) +} + +func requireArg(args []string, n int, usage string) { + if len(args) <= n { + fmt.Fprintln(os.Stderr, "usage:", usage) + os.Exit(1) + } +} + +func usage() { + fmt.Fprintln(os.Stderr, `nebbet — static site generator with admin + +Commands: + build [--watch] build all pages (optionally watch for changes) + watch watch for changes and rebuild + serve [--port N] serve site + admin UI (default port 8080) + user add <name> add user to password file + user list list users + user delete <name> remove user + user passwd <name> change user password`) +} |
