# ADR 018: Journal pages are stored by ISO date; the title format is a setting

> A journal page has three names at once today:

Source: https://nooklet.danielalder.cz/decisions/018-iso-journal-names

Date: 2026-09-11. Status: accepted.

## Context

A journal page has three names at once today:

- its **day**, `journal_day = 20260907`, which is the identity;
- its **stored name**, whatever title format the graph was written with — `Mon, 07.09.2026` in the
  imported Logseq graph, `Sep 7th, 2026` in a graph nooklet created itself;
- its **wire name**, the ISO date, because `journalDayFromWire` and rule 18 address journals by ISO
  everywhere in the API.

The second of those is the odd one out, and it has now caused three separate bugs:

- **B-22** — a search result or block reference hands out the ISO date, `usePageByName` looked the
  name up by key, found nothing, and said "This page doesn't exist yet" about a page with 40 blocks
  in it. Patched by falling back to `parseJournalTitle` → `journal_day`.
- **B-23** — `page.create` guarded only against ISO names, so an agent calling it with
  `"Sep 8th, 2026"` minted an ordinary page that shadows the journal day forever.
- A quieter one, never filed: `ref.dst_page_key` is the reference text folded to lower case, so
  `[[Mon, 07.09.2026]]`, `[[Sep 7th, 2026]]` and `[[2026-09-07]]` are three different keys pointing
  at three different (mostly non-existent) pages. A journal's backlinks were whichever subset
  happened to use the same spelling as its stored name.

Every one of these is the same defect wearing a different hat: **the format a date is displayed in
had become part of its identity.**

## Decision

**A page with a journal day is stored under its ISO name. The displayed title is a preference.
Every recognised way of writing a date resolves to the same key.**

Three changes carry it:

1. **`packages/core`'s `applyOps` derives the name.** `applyPageCreate` and `applyPageRename` both
   run the proposed name through `storedPageName(name, journalDay)`, which returns
   `isoJournalName(day)` for any page with a valid journal day and the proposed name otherwise. The
   op log keeps whatever name the op carried — that is history — while live state has exactly one
   answer.

2. **Reference keys canonicalise.** The server's `normalizeKey` (`apply-ops.ts`) is now
   `normalizePageName(canonicalRefName(name))`, and `canonicalRefName` collapses anything
   `parseJournalTitle` recognises to its ISO name. `page_tag`'s tag keys go through the same
   function. Block text is *not* rewritten: `[[Mon, 07.09.2026]]` stays as the person typed it and
   simply resolves.

3. **Display is a setting.** `apps/web/src/data/page-title.ts` owns a `journalTitleFormat`
   preference and the `displayPageName` / `displayRefName` helpers every view now renders through.
   The importer no longer stores the source graph's `:journal/page-title-format`; it reports it as
   an import note instead, so the person can choose it in settings and see the dates they are used
   to.

An existing graph is migrated by `packages/server/src/journal-names.ts`, run from `open()` in the
CLI and guarded by a `setting` row.

## Why the migration mints ops rather than updating rows

A raw `UPDATE page SET name = …` is one line and would be wrong twice over. Live state is a
projection of the op log, so `nooklet verify` — which replays the log and compares — would flag
every renamed page. And a client bootstraps from a *state snapshot* and then follows the op stream,
so a rename that is not an op is a rename no other device ever hears about: the desktop app would
show `Mon, 07.09.2026` forever while the server had moved on.

So the migration mints real `page.rename` ops with the server's device id. They apply locally,
replicate through sync, and apply identically on the other side because the coercion in (1) lives
in the shared reducer. That is also why `applyPageRename` coerces rather than refusing: a rename to
the ISO name has to be something that can actually happen.

Minting ops needs an HLC and a device id, which exist a layer above `schema.ts`, so this is a
startup task rather than a schema migration. It is idempotent and costs one `setting` read once the
graph is canonical.

The derived indexes are re-keyed in place rather than rebuilt from block content. The mapping (old
key → `canonicalRefName`) is a pure function of the key, so re-parsing 18k blocks would compute the
same answer about a hundred times more slowly.

## Why not the alternatives

**Keep storing the display format; keep patching lookups.** This is the status quo, and B-22's fix
is exactly it — `usePageByName` falls back to the day number when the name misses. It works, one
call site at a time, and every new consumer of page names starts out broken until someone notices.
The three bugs above are three such call sites; the reference index was a fourth nobody had looked
at yet.

**Store the ISO name but leave references uncanonicalised.** Half the change, and strictly worse
than doing nothing: renaming the pages without re-keying `ref` would empty every journal's
backlinks on the spot.

**Canonicalise references but leave names alone.** The other half. References would all agree on
`2026-09-07` and resolve to nothing, because no page is stored under that key.

**Make the display format a server setting rather than a per-device one.** Tempting — it is a
property of the graph, and it would follow you between machines. Left for later because it needs
the settings API that does not exist yet, and because "what my dates look like on this screen" is
genuinely defensible as a device preference. Moving it is a small change when there is somewhere to
move it to.

**Rewrite `[[Mon, 07.09.2026]]` in block text to `[[2026-09-07]]`.** Consistent, and it would let
references resolve with no canonicalisation at all. Rejected: it edits 18k blocks of a person's
prose to make a storage decision visible, and it destroys the thing the format setting is for. The
words in a note belong to whoever wrote them.

## Consequences

- The page title is no longer editable on a journal page — it renders as a heading. There is
  nothing for a rename to mean, and `applyPageRename` would undo it anyway.
- The page switcher and the all-pages filter match against the displayed title *and* the stored
  name, so a journal is found by `2026-09` and by `Sep 7th` alike. Matching one or the other alone
  would have quietly lost half the ways people look for a day.
- `LogseqConfig.journalPageTitleFormat` no longer affects import. It is parsed, reported, and
  otherwise informational — like `journalFileNameFormat` already was.
- The markdown mirror is unaffected: `pageFilePath` already derives a journal's file name from its
  day (`journals/2026_09_10.md`), so no file moves and no `title::` changes.
- A page named `11.12.2024` with no journal day keeps its name and stays reachable by it — both in
  the op layer, which never guesses from a string, and in `resolvePageRef`, which checks for a real
  page before reading a name as a date. What you cannot do is *create* one through `page.create`:
  B-23's guard reads any parseable date as a journal, on the grounds that a date-shaped page name
  is far likelier to be a misdirected journal write than a deliberate title. An imported graph can
  still contain one, which is why the name-before-date order matters.
- One thing to watch: a graph in which someone had already created an ordinary page named
  `2026-09-07` blocks that day's journal from taking its name. The migration reports it rather than
  guessing; the journal keeps its old name and works as before.
