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:3000

Deploying 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.

A trip's story page: the winding day-by-day path down the left, the day card, and the route map beneath it

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's entry: the prose, three photographs, and the reaction row

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

The trip map: eighteen stops joined by the route travelled

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: every photograph from the trip in a grid, filterable by place

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