From 885178d1e7fbbaec98cad6ebdfb8be73d5e9f021 Mon Sep 17 00:00:00 2001 From: biosias Date: Sat, 12 Sep 2026 12:32:13 +0200 Subject: [PATCH] Switch the post-directory field separator from -- to ~ (AI) A double-hyphen separator collides with an easy authoring typo: an accidental extra "-" in a kebab-case slug (e.g. "hello--world") silently misparses as slug "hello" + an invalid date, dropping the article from the index with no visible error. "~" isn't something anyone would type by accident while writing a normal slug, and (like "-") is in the URI "unreserved" set, so it never needs percent-encoding either. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd --- README.md | 16 ++++++++++------ cmjs/js/discovery.js | 16 +++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a1a8d61..a6c3050 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A minimal CMS where articles are Markdown files rendered in the browser. No buil 1. The browser traverses `cmjs/articles/` via a server directory index — discovering authors and their post directories; nginx JSON autoindex is preferred, with HTML autoindexes supported as a fallback. 2. Sorting, filtering, and publish status all come from the post directory's own *name* - (`slug--DDMMYYYY--tags`) — knowable straight from the listing that discovers it, with no + (`slug~DDMMYYYY~tags`) — knowable straight from the listing that discovers it, with no further request per article. A `.md` file's actual content (title, description, image) is only fetched once that article is about to be shown — the current page's cards, a single article, or a search — not for the whole site up front. See "Adding an article" and "Article @@ -77,7 +77,7 @@ cmjs/ └── author/ # One directory per author ├── author.md # Author profile (optional) ├── avatar.jpg # Optional — referenced by author.md's `avatar:` field - └── slug--DDMMYYYY--tag1+tag2/ # One directory per post — see "Adding an article" + └── slug~DDMMYYYY~tag1+tag2/ # One directory per post — see "Adding an article" ├── slug.md # Article content + frontmatter └── cover.jpg # Optional — referenced by slug.md's `image:` field ``` @@ -88,7 +88,7 @@ An article's directory *name* is where its publish date, tags, category, and dra live — not in separate files, and not in frontmatter. The format is: ``` -----++… +~~++… ``` - `` — the stable identifier used in the article's URL (`#post/author/slug`). Treat this @@ -98,9 +98,13 @@ live — not in separate files, and not in frontmatter. The format is: - `++…` — zero or more tag ids, joined with `+`. A tag id matching `expa-YYYY` is treated as the article's "year"/category, shown as a badge linking to `#category/expa-YYYY`; every other tag id is a normal topic tag, shown as a pill linking to `#tag/tag-id`. Omit this - whole segment (and its leading `--`) if the article has no tags. + whole segment (and its leading `~`) if the article has no tags. -A leading underscore on the whole directory name — `_--...` — marks the article as a +The separator is `~` (tilde), not a hyphen — deliberately, since a slug is expected to be plain +kebab-case (single hyphens only) and `~` isn't something you'd type by accident while writing +one, unlike an extra `-`. + +A leading underscore on the whole directory name — `_~...` — marks the article as a **draft**: it's excluded from the list, search, and pagination, but still reachable directly by URL. Remove the underscore to publish it. @@ -108,7 +112,7 @@ Steps: 1. Create the author directory if it doesn't exist: `cmjs/articles/author/` 2. Create the post directory using the naming convention above, e.g. - `cmjs/articles/author/my-article--16062026--kometa+expa-2026/` + `cmjs/articles/author/my-article~16062026~kometa+expa-2026/` 3. Create the Markdown file inside, named after the slug: `my-article.md` 4. Optionally add a cover image beside it too, e.g. `photo.jpg`, and reference it from frontmatter with `image: photo.jpg`. Images live next to the article that uses them, not in a diff --git a/cmjs/js/discovery.js b/cmjs/js/discovery.js index daddadd..46a16bf 100644 --- a/cmjs/js/discovery.js +++ b/cmjs/js/discovery.js @@ -151,16 +151,18 @@ function excerpt(content) { // A post directory's own name is the sole authoritative source of its // slug, publish date, tags/category, and draft status — see the README's -// "Adding an article" section. Format: `--[--tag1+tag2+…]`, -// optionally prefixed with `_` for draft. Splitting on the whole name is -// deliberate: a slug is expected not to contain "--" itself, the same way -// it's already expected not to contain "/". Returns null for any directory -// name that doesn't match — callers treat that as a silent skip, e.g. a -// stray directory that isn't a post at all. +// "Adding an article" section. Format: `~[~tag1+tag2+…]`, +// optionally prefixed with `_` for draft. `~` (rather than e.g. "--") is +// deliberate: a slug is expected to be plain kebab-case, and "~" isn't a +// character anyone would type by accident while writing one — unlike a +// double-hyphen separator, which collides with an easy slug typo (an extra +// "-") and silently drops the whole article instead of erroring. Returns +// null for any directory name that doesn't match — callers treat that as a +// silent skip, e.g. a stray directory that isn't a post at all. function parseSlugDirName(name) { const draft = name.startsWith('_'); const core = draft ? name.slice(1) : name; - const [slug, dateSegment, tagsSegment] = core.split('--'); + const [slug, dateSegment, tagsSegment] = core.split('~'); if (!slug || !dateSegment || !/^\d{8}$/.test(dateSegment)) return null; const day = dateSegment.slice(0, 2); const month = dateSegment.slice(2, 4);