8.5 KiB
datascape
Minimal self-hosted personal wiki. Folders are pages.
Features
-
Pages every folder is a page. Place an
index.mdinside 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
Filesheader. See the View Settings section. -
Search search across all page names (folder names) in the wiki, accessible from the navigation bar.
-
Wikilinks link between pages with
[[search query]]syntax, or[[query::display text]]to override the label. The link opens the search page for that query, so it keeps resolving after a page is renamed or moved. Files embed with standard Markdown image syntax —— relative to the page they sit on. -
Canvases an infinite pan/zoom board stored as a JSON Canvas
.canvasfile next to the page'sindex.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 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 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 section.
Build
# local
go build -o datascape .
# QNAP NAS (linux/arm)
GOOS=linux GOARCH=arm go build -o datascape .
Usage
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 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 |
|---|---|
<diary>/today/ |
<diary>/YYYY/#YYYY-MM-DD (or the year file's insert-section editor when today's section doesn't exist yet) |
<diary>/this-month/ |
<diary>/YYYY/#YYYY-MM |
<diary>/this-year/ |
<diary>/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 <Name>.canvas file in a page folder holding a
JSON Canvas 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 |
|---|---|
<page>/?canvas=<Name> |
the canvas view (canonical) |
<page>/<Name>.canvas |
redirects to the canonical form |
<page>/<Name>.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:
- the JSON parses into the spec's shape;
- node ids are unique, every edge endpoint resolves, and enums/colours/sizes are in range;
- the
X-Canvas-Versiontoken matches the file on disk — a mismatch is a409and you are asked whether to reload or overwrite; - a save that would empty a populated canvas is refused until you confirm it.
The previous contents are kept beside the file as .<Name>.canvas.bak, and the
write itself is atomic. Leaving the page with unsaved changes prompts first.
[[wikilinks]] inside text nodes are search queries, like on a page, so they
keep resolving after a move. File nodes and group backgrounds hold literal
paths and are not rewritten: moving or renaming their target leaves them
rendered as broken references.
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:(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:
- [Example Page](https://example.com)
2026-05-11 14:30
optional comment