Files
zettelclean/README.md
Ruben Carlo Benante 35cb6dd976
Some checks failed
Zettelclean CI / build (20.x) (push) Has been cancelled
Zettelclean CI / build (22.x) (push) Has been cancelled
Zettelclean CI / build (24.x) (push) Has been cancelled
Release Zettelclean / build (push) Has been cancelled
rename folder feature added. 4s edit wait, 6s cooldown adjust
2026-07-29 11:07:30 -03:00

6.6 KiB

Zettelclean

Keep a Zettelkasten filename's slug in sync with the note's H1 heading.

Zettelclean resolves the "three sources of truth for a title" problem (filename vs H1 vs YAML title:) by separating concerns:

  • The H1 heading in the note body is the single source of truth for the human title.
  • The filename is a derived machine identity: TIMEID-slug(H1).md.
  • No YAML is written. Files stay pure Markdown.

It is a small, one-way, prefix-gated renamer: edit the H1, and a moment later the filename's slug tail follows. Nothing else is touched.

How it works

  • A note is a zettel when its filename starts with a TIMEID: a run of 12 to 18 digits (a timestamp) followed by any non-digit or the end of the name - so 20260728145404-my-note.md, 202607281454.md, and even 202607281454 my note.md all qualify. Files without that prefix are ignored, so plain notes, daily notes, Excalidraw and Kanban files are never renamed.
  • The TIMEID is minted once and never changes. Only the slug tail is regenerated.
  • When you edit the H1, zettelclean waits about 4 seconds of idle, then renames the file to TIMEID-slug(H1).md using Obsidian's own rename (so backlinks are updated). A 6 second cooldown prevents a just-renamed file from being renamed again.
  • Sync is strictly one-way, H1 to filename. Renaming a file by hand is never fought.
  • Delete the H1 and the filename collapses back to the bare TIMEID.md.

The slug is case-preserving, ASCII, dash-separated: accents are folded (Ciências becomes Ciencias), runs of punctuation and whitespace collapse to a single dash, and leading and trailing dashes are trimmed.

Creating notes

Use Obsidian's built-in Unique note creator core plugin to mint the TIMEID:

  1. Settings -> Core plugins -> enable "Unique note creator".
  2. Set "Unique note format" to YYYYMMDDHHmmss (14 digits, second precision - avoids same-minute collisions).
  3. Click the "Create new unique note" ribbon icon. You get 20260728145404.md; type your # Title, and zettelclean takes over from there.

Zettelclean also accepts an existing 12-digit (YYYYMMDDHHmm) vault if you prefer not to change the setting.

Retrofitting existing notes

Notes created before you adopted this workflow have no TIMEID. Right-click such a note in the file explorer and choose "Prefix timestamp to filename". It mints a fresh timestamp, prepends it, and the note is now a zettel that syncs on future H1 edits.

Slugging a folder name

Right-click a folder and choose "Slug folder name" to ASCII-clean it in place (this folder only): Citações becomes Citacoes. The rename goes through Obsidian, so [[links]] into the folder are updated. It aborts if a sibling of that name already exists, and never touches the vault root.

Pretty sidebar (optional)

By default the file explorer shows the slugged filename (with dashes). If you want the sidebar, tabs and graph to show the pretty H1 instead - with no YAML in your files - install the Front Matter Title community plugin and set its feature templates to:

#heading | _basename

That reads the first H1 directly and falls back to the filename when there is none.

Settings

None. Zettelclean is zero-config in this version. The 4 second settle and 6 second cooldown are fixed.

Development

Dependencies are managed with pnpm (see "Why pnpm, not npm" below). Node 22+ ships corepack, so you do not need to install pnpm globally, and the packageManager field in package.json pins the exact version.

corepack pnpm install     # first time, or after a dependency change
corepack pnpm run dev     # compile in watch mode
corepack pnpm run build   # typecheck and produce main.js
corepack pnpm test        # run the slug unit tests (Node's built-in runner)
corepack pnpm run lint    # eslint with the Obsidian ruleset

The pure string logic lives in src/slug.ts and is unit-tested in isolation; the Obsidian wiring lives in src/main.ts. Tests run on Node's built-in test runner (node --test) - there is no vitest or jest dependency.

Why pnpm, not npm

Do not run npm install or npm ci in this repo. This project is developed on a ZFS filesystem, and npm's installer renames many directories in parallel (to hoist and dedupe packages). That races ZFS's directory-metadata handling and aborts with:

npm error code ENOTEMPTY
npm error syscall rename

pnpm avoids the problem entirely: it hard-links packages from a global content-addressable store instead of renaming directories into place, so it installs cleanly on the same disk.

Notes for contributors:

  • Only npm install / npm ci are affected. Running scripts through npm (npm run build) still works, because that just spawns tsc/esbuild and installs nothing. Still, prefer pnpm everywhere for consistency.
  • The lockfile is pnpm-lock.yaml (committed). There is no package-lock.json; do not create one.

License

GNU General Public License v3.0 or later (GPL-3.0-or-later). Copyright (C) 2026 Ruben Carlo Benante (Dr. Béco) rcb@beco.cc. See LICENSE for the full text.