diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ae4f159 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +## Project Overview + +`datascape` is a minimal personal wiki where **the folder structure is the wiki**. +No database, no CMS, no abstraction layer — every folder is a page, and `index.md` +in a folder is that page's content. + +## Build & Deploy + +```bash +# Local build (host architecture) +go build . + +# Deploy to NAS +make deploy +``` + +## HTTP API Surface + +| Method | Path | Behaviour | +|--------|------|-----------| +| GET | `/{path}/` | If folder exists: render `index.md` + list contents. If not: show empty create prompt. | +| GET | `/{path}/?edit` | Mobile-friendly editor with `index.md` content in a textarea | +| POST | `/{path}` | Write `index.md` to disk; creates the folder if it does not exist yet | + +Non-existent paths without a trailing slash redirect to the slash form (GET only — POSTs +are not redirected because `path.Clean` strips the trailing slash from `PostURL` and the +content would be lost). + +Do not add new endpoints without a concrete stated need. + +## Code Structure + +When adding a new special folder type, create a new `.go` file. Do not add type-specific logic to `main.go` or `render.go`. + +Prefer separate, human-readable `.html` files over inlined HTML strings in Go. Embed them via `embed.FS` if needed. + + +## Architecture Rules + +- **Single binary** — no installer, no runtime dependencies, no Docker +- **Go stdlib `net/http`** only — no web framework +- **`goldmark`** for Markdown rendering — no other Markdown libraries +- **`embed.FS`** for all assets — no external serving, no CDN +- **No database** of any kind +- **No indexing or caching** unless explicitly requested and justified +- Keep dependencies to an absolute minimum; if stdlib can do it, use stdlib + +## Frontend Rules + +- Vanilla JS only — no frameworks, no build pipeline +- Each feature gets its own JS file; global behaviour goes in `global-shortcuts.js` +- Do not inline JS in templates or merge unrelated features into one file +- `ALT+SHIFT` is the modifier for all keyboard shortcuts — do not introduce others +- Editor toolbar buttons use `data-action` + `data-key`; adding `data-key` auto-registers the shortcut + +## Development Priorities + +When building features, apply this order: +1. Correctness on the filesystem — never corrupt or lose files +2. Mobile usability (primary editing device is Android over Wireguard VPN) +3. Simplicity of implementation, adhere to KISS +4. Performance + +## What to Avoid + +- Any parallel folder structure (e.g. a separate `media/` tree mirroring `pages/`) +- Over-engineering auth — Basic auth is sufficient for a personal VPN tool +- Heavy payloads or expensive rendering (target CPU: ARMv7 32-bit NAS) +- Suggesting Docker (plain binary is preferred) + +## Out of Scope (do not implement unless explicitly asked) + +- Full-text search +- Browser-based file upload +- Version history / git integration +- Multi-user support +- Tagging or metadata beyond `index.md` content diff --git a/assets/diary/diary-day.html b/assets/diary/diary-day.html new file mode 100644 index 0000000..834e260 --- /dev/null +++ b/assets/diary/diary-day.html @@ -0,0 +1,7 @@ +{{if .Photos}} +
+ {{range .Photos}} + {{.Name}} + {{end}} +
+{{end}} diff --git a/assets/diary/diary-month.html b/assets/diary/diary-month.html new file mode 100644 index 0000000..69fff0f --- /dev/null +++ b/assets/diary/diary-month.html @@ -0,0 +1,15 @@ +{{range .Days}} +
+

+ {{if .URL}}{{.Heading}}{{else}}{{.Heading}}{{end}} +

+ {{if .Content}}
{{.Content}}
{{end}} + {{if .Photos}} +
+ {{range .Photos}} + {{.Name}} + {{end}} +
+ {{end}} +
+{{end}} diff --git a/assets/diary/diary-year.html b/assets/diary/diary-year.html new file mode 100644 index 0000000..8e0cc17 --- /dev/null +++ b/assets/diary/diary-year.html @@ -0,0 +1,8 @@ +{{range .Months}} +
+

+ {{.Name}} + {{if .PhotoCount}}({{.PhotoCount}} photos){{end}} +

+
+{{end}} diff --git a/assets/icons/archive.svg b/assets/icons/archive.svg new file mode 100644 index 0000000..cd3d53b --- /dev/null +++ b/assets/icons/archive.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/assets/icons/audio.svg b/assets/icons/audio.svg new file mode 100644 index 0000000..75db0a1 --- /dev/null +++ b/assets/icons/audio.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/assets/icons/doc.svg b/assets/icons/doc.svg new file mode 100644 index 0000000..b00de2a --- /dev/null +++ b/assets/icons/doc.svg @@ -0,0 +1,4 @@ + + + diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg new file mode 100644 index 0000000..37c3a47 --- /dev/null +++ b/assets/icons/folder.svg @@ -0,0 +1,4 @@ + + + diff --git a/assets/icons/generic.svg b/assets/icons/generic.svg new file mode 100644 index 0000000..a505f9e --- /dev/null +++ b/assets/icons/generic.svg @@ -0,0 +1,4 @@ + + + diff --git a/assets/icons/image.svg b/assets/icons/image.svg new file mode 100644 index 0000000..5b264c7 --- /dev/null +++ b/assets/icons/image.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/assets/icons/video.svg b/assets/icons/video.svg new file mode 100644 index 0000000..74d0fe3 --- /dev/null +++ b/assets/icons/video.svg @@ -0,0 +1,5 @@ + + + + diff --git a/diary.go b/diary.go index a02d534..9958c66 100644 --- a/diary.go +++ b/diary.go @@ -85,17 +85,9 @@ type diaryYearData struct{ Months []diaryMonthSummary } type diaryMonthData struct{ Days []diaryDaySection } type diaryDayData struct{ Photos []diaryPhoto } -var diaryYearTmpl = template.Must(template.New("diary-year").Parse( - `{{range .Months}}

{{.Name}}{{if .PhotoCount}} ({{.PhotoCount}} photos){{end}}

{{end}}`, -)) - -var diaryMonthTmpl = template.Must(template.New("diary-month").Parse( - `{{range .Days}}

{{if .URL}}{{.Heading}}{{else}}{{.Heading}}{{end}}

{{if .Content}}
{{.Content}}
{{end}}{{if .Photos}}
{{range .Photos}}{{.Name}}{{end}}
{{end}}
{{end}}`, -)) - -var diaryDayTmpl = template.Must(template.New("diary-day").Parse( - `{{if .Photos}}
{{range .Photos}}{{.Name}}{{end}}
{{end}}`, -)) +var diaryYearTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-year.html")) +var diaryMonthTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-month.html")) +var diaryDayTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-day.html")) var photoExts = map[string]bool{ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, diff --git a/main.go b/main.go index 200b363..a6e96d6 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( "strings" ) -//go:embed assets/* +//go:embed assets var assets embed.FS var tmpl = template.Must(template.New("page.html").ParseFS(assets, "assets/page.html")) diff --git a/render.go b/render.go index cbc3c7d..b417060 100644 --- a/render.go +++ b/render.go @@ -44,6 +44,16 @@ type pageSettings struct { Type string } +var ( + iconFolder = readIcon("folder") + iconDoc = readIcon("doc") + iconImage = readIcon("image") + iconVideo = readIcon("video") + iconAudio = readIcon("audio") + iconArchive = readIcon("archive") + iconGeneric = readIcon("generic") +) + // renderMarkdown converts raw markdown to trusted HTML. func renderMarkdown(raw []byte) template.HTML { var buf bytes.Buffer @@ -133,16 +143,10 @@ func listEntries(fsPath, urlPath string) []entry { return append(folders, files...) } -// Pixel-art SVG icons — outlined, crispEdges, uses currentColor. -const ( - iconFolder template.HTML = `` - iconDoc template.HTML = `` - iconImage template.HTML = `` - iconVideo template.HTML = `` - iconAudio template.HTML = `` - iconArchive template.HTML = `` - iconGeneric template.HTML = `` -) +func readIcon(name string) template.HTML { + b, _ := assets.ReadFile("assets/icons/" + name + ".svg") + return template.HTML(strings.TrimSpace(string(b))) +} func fileIcon(name string) template.HTML { ext := strings.ToLower(path.Ext(name))