# datascape Minimal self-hosted personal wiki. Folders are pages. ## Features - **Pages** every folder is a page. Place an `index.md` inside a folder and it renders as HTML. Drop any other files (PDFs, images, etc.) alongside it and they appear in the listing below the content. Navigating to a path that does not exist shows a **[CREATE]** prompt. - **View settings** per folder, display the file listing as a list or thumbnail grid and pick the sort key/order, via the **view** button in the `Files` header. See the [View Settings](#view-settings) section. - **Search** search across all page names (folder names) in the wiki, accessible from the navigation bar. - **Wikilinks** link between pages with `[[Page Name]]` syntax. When a page is renamed or moved, all wikilinks pointing to it are rewritten automatically to reflect the new path. - **Canvases** an infinite pan/zoom board stored as a [JSON Canvas](https://jsoncanvas.org/) `.canvas` file next to the page's `index.md`. Canvases in a folder appear as tabs above the page content, so a canvas is an alternative *view* of the page rather than a separate document. See the [Canvases](#canvases) section. - **Movie import** import movie entries via the OMDb API. Fetches title, year, runtime, genre, director, cast, plot, and poster, and pre-fills a new page with that metadata. - **Special folder types** folders can opt into custom rendering (e.g. a photo diary with calendar navigation). See the [Special Folder Types](#special-folder-types) section for details. - **Quick-add bookmarklet** save the current browser tab to a predetermined wiki page (e.g. `/Topics/Bookmarks/`) with one click. See the [Quick-Add Bookmarklet](#quick-add-bookmarklet) section. ## Build ```bash # local go build -o datascape . # QNAP NAS (linux/arm) GOOS=linux GOARCH=arm go build -o datascape . ``` ## Usage ```bash go run . -dir ./wiki -addr :8080 go run . -dir ./wiki -addr :8080 -user me -pass secret ``` | Flag | Default | Description | |------|---------|-------------| | `-addr` | `:8080` | Listen address | | `-dir` | `./wiki` | Wiki root directory | | `-cache` | `./cache` | Thumbnail cache directory | | `-user` | _(none)_ | Basic auth username — omit to disable auth | | `-pass` | _(none)_ | Basic auth password | | `-reindex-interval` | `30m` | Periodic search index rebuild interval (`0` disables) | ## View Settings The **view** button in a folder's `Files` header sets how its listing renders, persisting three keys to `.page-settings`: | Key | Values (default first) | |------|------------------------| | `view` | `list`, `thumbnail` | | `sort` | `name`, `modified`, `size` (folders always sort by name, grouped first) | | `order` | `asc`, `desc` | ## Special Folder Types A folder can opt into special rendering by adding a `.page-settings` file. The same file also holds the [View Settings](#view-settings) keys; only the `type` key selects a special renderer: ``` type = diary ``` ### Diary Designed for a chronological photo diary. The whole year lives in a single file as ISO-headed sections; photos are loose JPEGs named with a date prefix. ``` FolderName/ .page-settings ← type = diary YYYY/ index.md ← `# YYYY` + `## YYYY-MM` + `### YYYY-MM-DD` sections YYYY-MM-DD Desc.jpg ← photos named with the date they belong to ``` The year page (`YYYY/`) renders every section in the file with photos attached to each `### YYYY-MM-DD` heading. Months and days the file doesn't yet contain are rendered as **virtual** headings with an `[edit]` button that splices a new section into the year file at the right chronological position; virtual day headings still carry photos for that date. Past years render every month/day slot; the current year stops at today; future years skip virtual entries entirely. The file may contain non-date headings (e.g. `## Events` → `### Festival` between `# YYYY` and `## YYYY-01`); these keep their document position. A sidebar calendar widget shows one month grid at a time; the month-name button opens a dropdown of all twelve months, and a separate year dropdown jumps between years. Day cells link to the matching anchor on the year page regardless of whether the date has a real section yet. #### Persistent date links Each diary root exposes three stable paths intended for browser bookmarks. They resolve against the year page rather than separate per-day URLs: | Path | Redirects to | |------|-------------| | `/today/` | `/YYYY/#YYYY-MM-DD` (or the year file's insert-section editor when today's section doesn't exist yet) | | `/this-month/` | `/YYYY/#YYYY-MM` | | `/this-year/` | `/YYYY/` | Legacy `YYYY/MM/` and `YYYY/MM/DD/` URLs (no longer the canonical form) redirect to the matching anchor on the year page. ## Canvases A canvas is a `.canvas` file in a page folder holding a [JSON Canvas 1.0](https://jsoncanvas.org/spec/1.0/) document. Every canvas in a folder renders as a tab above that page's content, and the page itself is the first tab — so a page and its diagrams are one thing with several views. | URL | What it does | |------|-------------| | `/?canvas=` | the canvas view (canonical) | | `/.canvas` | redirects to the canonical form | | `/.canvas?raw` | the file itself | Create one from the **NEW CANVAS** action or the `+` in the tab strip. ### Nodes All four spec node types render: | Type | Rendered as | |------|-------------| | `text` | markdown, rendered server-side — wikilinks, `![[embeds]]`, tables and task lists all behave as they do on a page | | `file` | a transclusion of another wiki path: markdown pages (narrowed to a heading when `subpath` is set), images, video, PDFs | | `link` | a card for an external URL | | `group` | a labelled frame; dragging it carries the nodes inside it | Node content is rendered by the server and cached in the browser by *content*, so the whole canvas arrives pre-rendered in the initial page load (no per-node requests) and undo/redo of a text edit costs nothing. ### Controls | Gesture | Action | |---------|--------| | drag background | pan | | ctrl/⌘ + wheel, or pinch | zoom | | shift + drag background | marquee select | | drag a node | move it (snaps to a 20px grid; hold alt to bypass) | | drag a corner handle | resize | | drag a side port | draw an edge to another node | | double-click a node | edit it | | double-click empty space | new text node there | Keys: `Ctrl+S` save · `Ctrl+Z` / `Ctrl+Shift+Z` undo/redo · `Del` delete selection · `Ctrl+A` select all · arrows nudge (shift for single pixels) · `T`/`F`/`L`/`G` add a node · `+`/`-`/`0` zoom, `9` fit · `Esc` cancel. Undo covers the whole session; a drag or resize is one entry no matter how many events it spanned. ### Saving Saving is explicit (**SAVE**, or `Ctrl+S`) and uploads the whole document. The server parses and validates it before writing anything, so a malformed payload can never land on disk, and four things are checked in order: 1. the JSON parses into the spec's shape; 2. node ids are unique, every edge endpoint resolves, and enums/colours/sizes are in range; 3. the `X-Canvas-Version` token matches the file on disk — a mismatch is a `409` and you are asked whether to reload or overwrite; 4. a save that would empty a populated canvas is refused until you confirm it. The previous contents are kept beside the file as `..canvas.bak`, and the write itself is atomic. Leaving the page with unsaved changes prompts first. Moving or renaming a page rewrites the canvases that point at it — both the `[[wikilinks]]` inside text nodes and the paths of file nodes and group backgrounds — the same way it rewrites links in `index.md`. Canvas *contents* are not searchable; only the file name is indexed. ## Quick-Add Bookmarklet Replace `wiki.host` with your wiki host and `/Topics/Bookmarks/` with the destination page (one bookmarklet per target): ```javascript javascript:(function(){var s=window.getSelection().toString().trim();var t=s||document.title;var u=location.href;var to='/Topics/Bookmarks/';var q='?to='+encodeURIComponent(to)+'&url='+encodeURIComponent(u)+'&title='+encodeURIComponent(t);window.open('https://wiki.host/quickadd'+q,'quickadd','width=480,height=320');})(); ``` Each save appends an entry of the following form to the destination page's `index.md`: ```markdown - [Example Page](https://example.com) 2026-05-11 14:30 optional comment ```