Hosting
A VPS, Node and Caddy, one deploy script. A public journal needs no database, and every optional capability — mail, sign-in, guests, push, print — is off by default.
npm install
npm run dev # http://localhost:3000Deploying to a VPS is a longer walk — see docs/runbook.md in the repository.
What a day looks like
One markdown file per update, carrying whatever fields are actually known. An empty field beats a guessed one.
One markdown file per update, in
content/<username>/trips/<trip-id>/entries/, named YYYY-MM-DD-slug.md.
Frontmatter is plain YAML, Obsidian-compatible:
---
title: "Lanterns of Hoi An"
date: "2026-08-26"
time: "16:45" # orders several updates within one day
location: "Hoi An"
country: "Vietnam"
lat: 15.8801
lng: 108.338
transportMode: "bus" # flight | train | bus | motorbike | bicycle | boat | car | taxi | walk
transportFrom: "Da Lat"
transportTo: "Hoi An"
gallery:
- src: "/media/<trip-id>/hoi-an/01.jpg" # trip-relative; the username is added at read time
type: "image"
width: 1200
height: 800
costs:
- { label: "Dinner", amount: 180000, category: "food", currency: "VND" }
status: draft # present ⇒ not on the site
---
The diary text, in plain markdown.
Gaps are fine. A trip's own trip.md carries its title, dates, who was on it,
and its visibility: private, public or guest. An unrecognised value reads
as private, never as public — a typo must not publish somebody's trip.
The one thing that stays out of an agent's hands is invention. One made-up
memory, presented to somebody's family as fact, is not recoverable — so an
agent writes what it was told and leaves a field empty rather than guessing,
and content nobody lived is marked test: true and says so on the page.
What you get
Every picture below is the demo journal, on a production build. Nothing in it belongs to a real person.

The story page. The rail on the left is the trip — one stop per day, with what it cost. Scrolling it walks the travellers from stop to stop.

One day: markdown prose, its gallery, the day's spend, and the reactions
readers leave. This is one file in entries/, rendered.

Every stop on one map, drawn from the lat and lng in each entry's
frontmatter. The base map is baked into the build — no tile server, no API key.

The gallery, filterable by place, with a slideshow behind the button.