update readme
This commit is contained in:
1 file changed
+73
-35
@@ -77,15 +77,20 @@ from the library — not something an embedding page needs to bring along.
|
||||
│ ├── card.html # Article list card layout
|
||||
│ ├── article.html # Full article view layout
|
||||
│ └── author-profile.html # Author profile card layout
|
||||
└── articles/
|
||||
└── articles/ # The only folder cmjs reads
|
||||
├── _tags.json # Tag registry (optional, see "Tag registry")
|
||||
└── author/ # One directory per author
|
||||
├── author.md # Author profile (optional)
|
||||
├── avatar.jpg # Optional author avatar
|
||||
└── slug--DDMMYYYY--expa-YYYY+tag1/ # One directory per article
|
||||
├── avatar.jpg # Optional author avatar, referenced by author.md
|
||||
└── slug--DDMMYYYY--expa-YYYY+tag1/ # One directory per article
|
||||
├── slug.md
|
||||
└── cover.jpg # Optional, referenced by image:
|
||||
└── cover.jpg # Optional, referenced by image:
|
||||
```
|
||||
|
||||
Drafts and archived posts live in sibling `drafts/` and `archive/` folders with the same
|
||||
structure (see "Adding an article"). They sit outside `articles/`, so they are never served or
|
||||
listed.
|
||||
|
||||
## Local configuration
|
||||
|
||||
`js/config-default.js` holds the shipped defaults (`BASE_URL`, `MARKED_URL`, `ARTICLES_URL`) and
|
||||
@@ -115,24 +120,39 @@ An embedding page may configure a public DAV proxy without a local file:
|
||||
## Adding an article
|
||||
|
||||
An article is a Markdown file inside a post directory. Its *directory name* contains the stable
|
||||
slug, date, expedition year, and topic tags; publication status comes from the root (`articles`,
|
||||
`drafts`, or `archive`). The format is:
|
||||
slug, date, expedition year, and topic tags. Publication status comes from the root folder the
|
||||
post directory sits in:
|
||||
|
||||
| Root | Status | Shown by cmjs |
|
||||
|------|--------|---------------|
|
||||
| `articles/<author>/` | published | yes |
|
||||
| `drafts/<author>/` | draft | no |
|
||||
| `archive/<author>/` | archived | no |
|
||||
|
||||
Publishing, unpublishing, or archiving a post means moving its whole directory (Markdown and
|
||||
images together) between these roots. cmjs only ever reads `articles/`.
|
||||
|
||||
The directory name format is:
|
||||
|
||||
```
|
||||
<slug>--<DDMMYYYY>--expa-YYYY+<tag1>+<tag2>+…/<slug>.md
|
||||
```
|
||||
|
||||
- `<slug>` — the stable identifier used in the article's URL (`#post/author/slug`). Treat this
|
||||
as permanent: retagging or correcting the date later just means renaming the file (see below),
|
||||
but changing the slug changes the article's URL.
|
||||
- `<slug>` — the stable identifier used in the article's URL (`#post/author/slug`), matching
|
||||
`[a-z0-9][a-z0-9-]{0,95}`. The Markdown file inside the directory must be named `<slug>.md`.
|
||||
Treat the slug as permanent: retagging or correcting the date later just means renaming the
|
||||
directory (see below), but changing the slug changes the article's URL.
|
||||
- `<DDMMYYYY>` — the publish date, 8 digits, no separators (e.g. `16062026` for 16 June 2026).
|
||||
- `<tag1>+<tag2>+…` — zero or more normalized topic ids, joined with `+`. `expa-YYYY` is
|
||||
treated as the article's expedition/year marker, shown as a badge linking to `#tag/expa-YYYY`;
|
||||
every other tag id is a normal topic tag, shown as a pill linking to `#tag/tag-id`. Omit this
|
||||
omit the tag suffix only for a post with no topical tags.
|
||||
An invalid date (e.g. `31022026`) makes the directory be skipped.
|
||||
- `expa-YYYY+<tag1>+<tag2>+…` — normalized tag ids (lowercase `[a-z0-9][a-z0-9.-]*`) joined
|
||||
with `+`. `expa-YYYY` is the article's expedition/year marker, shown as a badge linking to
|
||||
`#tag/expa-YYYY`; every other tag id is a normal topic tag, shown as a pill linking to
|
||||
`#tag/tag-id`. Posts written by the blog editor always have exactly one `expa-YYYY` tag,
|
||||
listed first. The whole `--…` tag suffix is optional for hand-made posts.
|
||||
|
||||
Images (cover and inline) live beside the Markdown file and are referenced by
|
||||
relative paths such as `image: cover.jpg`.
|
||||
Images (cover and inline) live in the post directory, next to the Markdown file, and are
|
||||
referenced by paths relative to it — `image: cover.jpg`, ``, or a subfolder such
|
||||
as `media/photo.jpg`. Absolute `https://` URLs are used as-is.
|
||||
|
||||
Steps:
|
||||
|
||||
@@ -147,7 +167,7 @@ To change an article's date, tags, or publication state later, move/rename its d
|
||||
content doesn't need to change. `author` is derived from the containing directory — no frontmatter field needed
|
||||
for it either. Treat the directory name as case-sensitive and exact: it's matched exactly (not
|
||||
case-insensitively) against `#author/...` URLs and author-profile lookups, so avoid two author
|
||||
directories that differ only in case. Because every one of these is encoded in the file name,
|
||||
directories that differ only in case. Because every one of these is encoded in the directory name,
|
||||
discovering an article's metadata never requires fetching anything beyond the directory listing
|
||||
that already reveals it — no further per-article request at all, which is what makes the "Article
|
||||
discovery & caching" section below possible.
|
||||
@@ -160,8 +180,12 @@ discovery & caching" section below possible.
|
||||
|
||||
```yaml
|
||||
---
|
||||
schema: 2
|
||||
title: My Article Title
|
||||
description: A short summary shown in the article list.
|
||||
authorName: Jane Doe
|
||||
image: cover.jpg
|
||||
updatedAt: '2026-08-27T21:14:03+02:00'
|
||||
---
|
||||
|
||||
Your markdown content here...
|
||||
@@ -170,12 +194,14 @@ Your markdown content here...
|
||||
| Field | Required | Notes |
|
||||
|-------|----------|-------|
|
||||
| `title` | yes | Shown in the list and article header |
|
||||
| `description` | no | Short summary shown under the title in the list |
|
||||
| `image` | no | Path to an image in the author's shared `media/` folder (e.g. `media/photo.jpg`); shown as card thumbnail and article hero |
|
||||
| `description` | no | Short summary shown under the title in the list; an excerpt of the body is used when missing |
|
||||
| `authorName` | no | Display name of the author; falls back to the author directory name |
|
||||
| `image` | no | Cover image, relative to the Markdown file (e.g. `cover.jpg`); shown as card thumbnail and article hero. `cover` is accepted as an alias |
|
||||
| `schema`, `updatedAt` | no | Written by the blog editor; ignored by cmjs |
|
||||
|
||||
Reading time is computed automatically from the article word count — no frontmatter field
|
||||
needed. `date`, `tags`, and `draft` frontmatter fields, if present, are ignored — superseded by
|
||||
the directory name described above.
|
||||
needed. `date`, `tags`, `status`, `author`, `expa`, and `draft` frontmatter fields, if present,
|
||||
are ignored — superseded by the directory name and root described above.
|
||||
|
||||
## Author profiles
|
||||
|
||||
@@ -206,11 +232,22 @@ Short bio rendered as markdown.
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `title` | Display name shown in the profile card. Falls back to the author directory name. |
|
||||
| `avatar` | Path to an image, relative to the author directory — e.g. `media/avatar.jpg` if kept in the shared media folder alongside article images, or a bare filename if kept directly beside `{author}.md`. Displayed as a circular thumbnail. |
|
||||
| `avatar` | Path to an image, relative to the author directory — usually a bare filename such as `avatar.jpg` kept directly beside `{author}.md`. Displayed as a circular thumbnail. |
|
||||
| `github` / `twitter` / `linkedin` / `mastodon` / `website` | Any subset of these renders as pill links below the bio. |
|
||||
|
||||
The body text below the frontmatter is rendered as markdown and shown as the bio.
|
||||
|
||||
## Tag registry
|
||||
|
||||
`articles/_tags.json` (or whatever `TAGS_URL` points to) holds human-readable tag labels:
|
||||
|
||||
```json
|
||||
{"schemaVersion": 1, "tags": [{"id": "guides", "label": "Guides", "description": "", "aliases": [], "active": true}]}
|
||||
```
|
||||
|
||||
The blog editor maintains this file. cmjs reserves the `TAGS_URL` setting for it but doesn't read
|
||||
it yet, so tag pills currently show the raw tag ids.
|
||||
|
||||
## Routing
|
||||
|
||||
| URL hash | View |
|
||||
@@ -225,7 +262,7 @@ The body text below the frontmatter is rendered as markdown and shown as the bio
|
||||
| `#tag/expa-2026/page/2` | Same, page 2 |
|
||||
| `#tag/guides` | All articles tagged "guides", page 1 |
|
||||
| `#tag/guides/page/2` | Same, page 2 |
|
||||
| `#post/alice/my-article` | Single article at `articles/alice/my-article~DDMMYYYY~tags.md` |
|
||||
| `#post/alice/my-article` | Single article at `articles/alice/my-article--DDMMYYYY--expa-YYYY+tags/my-article.md` |
|
||||
|
||||
Author names and category tags on each card and article are clickable links to their respective filter views. A search box in the header navigates to `#search/query`.
|
||||
|
||||
@@ -459,11 +496,11 @@ not built into stock nginx — only worth it if you're already building custom n
|
||||
|
||||
Discovery happens in two phases. **Indexing** crawls `articles/` client-side to build a
|
||||
lightweight "stub" for every article — the author directories, then each author's own directory
|
||||
listing (their article files, bio file, and shared `media/` folder together) along with each
|
||||
file's `mtime`. Non-article entries (the author's own `{author}.md`, the `media/` folder) are
|
||||
simply filtered out of that same listing. That's it: because an article file's *name* already
|
||||
encodes its slug, date, tags, and draft status (see "Adding an article" above), that single
|
||||
listing is enough to sort, filter, paginate, and populate the sidebar's tag/year counts, without
|
||||
listing (their post directories, bio file, and avatar together) along with each entry's ETag
|
||||
(WebDAV) or `mtime` (nginx). Anything that isn't a directory with a valid post name (the
|
||||
author's own `{author}.md`, the avatar, stray files) is simply filtered out of that same
|
||||
listing. That's it: because a post directory's *name* already encodes its slug, date, and tags
|
||||
(see "Adding an article" above), that single listing is enough to sort, filter, paginate, and populate the sidebar's tag/year counts, without
|
||||
any further request per article and without ever fetching an article's actual Markdown.
|
||||
**Hydration** is the separate, deferred step that fetches a specific article's content (title,
|
||||
description, cover image, reading time) — it only ever runs for articles about to be rendered:
|
||||
@@ -474,17 +511,18 @@ articles it has — indexing costs one request per author, full stop, and only ~
|
||||
of content is ever fetched per page view.
|
||||
|
||||
- **Cache** — hydrated articles are cached per article file in IndexedDB (`js/cache.js`),
|
||||
keyed by the file's own path and invalidated by comparing its live `mtime` against the cached
|
||||
one — once *any* visit renders an article, every later visit skips its content fetch too, until
|
||||
the file is renamed (a rename changes the cache key, so a retagged/redated article is treated as
|
||||
new — see "Adding an article" above) or its own mtime changes. Indexing reads the entire cache
|
||||
keyed by the Markdown file's own path and invalidated by comparing the post directory's live
|
||||
ETag (or `mtime` when the listing has no ETags) against the cached one — once *any* visit
|
||||
renders an article, every later visit skips its content fetch too, until the directory is
|
||||
renamed (a rename changes the cache key, so a retagged/redated article is treated as new — see
|
||||
"Adding an article" above) or its contents change. Indexing reads the entire cache
|
||||
once up front (`getAllCacheEntries()`, kicked off in parallel with the author/directory-listing
|
||||
requests) so a warm cache doesn't cost an IndexedDB read per article. If IndexedDB is unavailable
|
||||
(private browsing, disabled storage), discovery transparently falls back to fetching everything
|
||||
live on every visit.
|
||||
- **Cache schema version** — the cached records store the full derived article shape, not raw
|
||||
content, so a code change to how articles are derived (in `js/discovery.js`) can make old
|
||||
cached entries stale in *structure*, not just content — and a file's `mtime` won't catch that.
|
||||
cached entries stale in *structure*, not just content — and an ETag or `mtime` won't catch that.
|
||||
Bump `CACHE_SCHEMA_VERSION` in `js/cache.js` whenever that shape changes; it's baked into
|
||||
the IndexedDB database name, so a bump invalidates every cached entry on the next deploy.
|
||||
- **Concurrency** — `MAX_CONCURRENT_REQUESTS` in `js/discovery.js` (default `10`) caps how
|
||||
@@ -506,7 +544,7 @@ of content is ever fetched per page view.
|
||||
page) until then.
|
||||
- **Crawl health** — if the root author-listing fetch fails outright, if every individual
|
||||
author's own directory listing fails, or if very few of the known candidate files parsed
|
||||
successfully (a malformed file name, or a total breakage like the wrong origin or CORS
|
||||
successfully (a malformed directory name, or a total breakage like the wrong origin or CORS
|
||||
misconfigured), a small warning banner is shown above the article list instead of silently
|
||||
rendering an empty page (see `getCrawlStats()` / `renderCrawlHealthWarning()` in
|
||||
`js/discovery.js` / `js/views.js`).
|
||||
@@ -514,8 +552,8 @@ of content is ever fetched per page view.
|
||||
listing fetch, recency prioritisation, HTML-autoindex fallback, long-term "deploy and forget"
|
||||
constraints) is documented in
|
||||
[`.claude/nginx-sitemap-crawler-plan.md`](.claude/nginx-sitemap-crawler-plan.md), which ships
|
||||
alongside the code. The per-article listing fetch it describes no longer exists (metadata moved
|
||||
into the file name instead, and articles no longer live in a directory of their own), but the
|
||||
alongside the code. The per-article listing fetch it describes no longer exists (all metadata
|
||||
needed for indexing is in the post directory name, so the author listing is enough), but the
|
||||
rest of that reasoning — the caching model, the longevity constraints, the HTML fallback — still
|
||||
applies.
|
||||
|
||||
|
||||
Reference in new issue
Block a user