6.4 KiB
AGENTS.md — Personal Wiki (go-wiki)
Project Philosophy
This is a minimal personal wiki where the folder structure is the wiki. There is no database, no CMS, no abstraction layer between the user and their files. Every decision should reinforce this — if a proposed solution adds indirection between the filesystem and what the user sees, question it.
Core Concept
- Every folder is a page
index.mdin a folder is that page's content- All related files (PDFs, images, CAD files, etc.) live in the same folder as
index.md - Image links in
index.mdlikework because siblings are served at the same path - There are no "attachments" — files are just files in a folder
Target Environment
- Runs on a QNAP TS-431P3 NAS (Annapurna Labs AL-314, ARMv7 32-bit,
linux/arm) - All files live on the NAS and are mounted/accessed locally by the binary
- Users access via browser over Wireguard VPN from Windows, Linux, and Android
- Must cross-compile cleanly:
GOARCH=arm GOOS=linux GOARM=7 go build
Tech Constraints
- Language: Go
- Output: Single static binary, no installer, no runtime dependencies
- Markdown:
goldmarkfor server-side rendering — no other markdown libraries - Assets: Embedded via
embed.FS— no external asset serving or CDN - HTTP: stdlib
net/httponly — no web framework - Dependencies: Keep to an absolute minimum. If stdlib can do it, use stdlib.
HTTP Interface
The entire API surface should stay minimal:
| 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 endpoints beyond these without a concrete stated need.
UI Principles
- Mobile-first — the primary editing device is Android over Wireguard
- No JavaScript frameworks — vanilla JS only, and only when necessary
- No build pipeline for frontend assets — what is embedded is what is served
- Readable on small screens without zooming
- Fast on a low-power ARM CPU — no heavy rendering, no large payloads
Frontend Conventions
JS file scoping: each feature gets its own file. Global app behaviour goes in
global-shortcuts.js. Feature-specific logic gets its own file (e.g. editor.js).
Do not inline JS in the template or consolidate unrelated features into one file.
Keyboard shortcuts: ALT+SHIFT is the established modifier for all application
shortcuts — it avoids collisions with browser and OS bindings. Do not use other
modifiers for new shortcuts.
Editor toolbar: buttons use data-action (maps to a JS action function) and
data-key (the ALT+SHIFT+KEY shortcut letter). Adding a data-key to a button
automatically registers its shortcut — no extra wiring needed.
Code Structure
The backend is split across three files:
| File | Responsibility |
|---|---|
main.go |
Server setup, routing, serveDir, handlePost, pageTypeHandler interface, readPageSettings |
render.go |
Shared helpers: markdown rendering, heading extraction, file listing, icons, formatting |
diary.go |
Diary page type: all types, templates, and render functions |
When adding a new special folder type, create a new .go file. Do not add type-specific
logic to main.go or render.go.
Special Folder Types (pageTypeHandler)
Folders can opt into special rendering by placing a .page-settings file in them.
Format: one key = value per line; # lines are comments.
# example
type = diary
The server walks up from the requested path looking for a .page-settings file. When
found, it determines the depth of the current path relative to that root and dispatches
to the matching pageTypeHandler.
Interface (defined in main.go):
type specialPage struct {
Content template.HTML
SuppressListing bool
}
type pageTypeHandler interface {
handle(root, fsPath, urlPath string) *specialPage
}
handle returns nil when the handler does not apply. SuppressListing hides the
default file/folder table (used when the special content replaces it).
Registering a new type: implement the interface in a new file and register via
init():
func init() {
pageTypeHandlers = append(pageTypeHandlers, &myHandler{})
}
serveDir iterates pageTypeHandlers and uses the first non-nil result. It has no
knowledge of specific types.
Diary type (diary.go)
Activated by type = diary in a .page-settings file. Folder structure:
Root/ ← .page-settings (type = diary)
YYYY/ ← depth 1 — year view (month sections + photo counts)
YYYY-MM-DD Description.ext ← photos live here, named with date prefix
MM/ ← depth 2 — month view (day sections with content + photos)
DD/ ← depth 3 — day view (index.md content + photo grid)
index.md
Photos are associated to days by parsing the YYYY-MM-DD prefix from filenames in the
year folder. No thumbnailing is performed — images are served at full resolution with
loading="lazy". The year view shows only photo counts, not grids, for performance.
Auth
- Basic auth is sufficient — this is a personal tool on a private VPN
- Do not over-engineer access control
What to Avoid
- No database of any kind
- No indexing or caching layer unless explicitly requested and justified
- No parallel folder structures (the DokuWiki anti-pattern:
pages/mirrored bymedia/) - No frameworks (web, ORM, DI, etc.)
- No build steps for frontend assets
- Do not suggest Docker unless the user asks — a plain binary is preferred
Development Order
When building new features, follow this priority order:
- Correctness on the filesystem (never corrupt or lose user files)
- Mobile usability
- Simplicity of implementation
- Performance
Out of Scope (for now)
These are explicitly deferred — do not implement or scaffold unless asked:
- Full-text search
- File upload via browser
- Version history / git integration
- Multi-user support
- Tagging or metadata beyond
index.mdcontent