Make config.js truly optional, falling back to config-default.js (AI)

A plain static `import ... from './config.js'` can't gracefully
degrade if that file doesn't exist -- a missing module breaks the
whole graph before any application code can catch it. So the four
consumers (discovery.js, i18n.js, templates.js, router.js) now import
BASE_URL/MARKED_URL/ARTICLES_URL from config-default.js instead, which
tries a dynamic import() of the optional config.js (catchable, unlike
a static import) and merges in whatever it exports, falling back to
its own shipped defaults for anything config.js doesn't override --
or doesn't exist at all. Top-level await makes every importing module
wait for that resolution first, same as a plain synchronous export.

config.js itself is now just an optional, empty-by-default file: only
export the specific constant(s) you want to change. Updated the
README's "Local configuration" section (and its three cross-references
in Running locally / Embedding in an existing page / Pointing
articles/ somewhere else) to drop the now-unnecessary "copy this file
before first use" step entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
This commit is contained in:
biosiasandClaude Sonnet 5 committed 2026-09-12 16:51:18 +02:00
1 parent c4d39ea1ad
commit 9e8af56069
6 files changed
+47 -44

No files matched your search

+12 -23
View File
@@ -59,7 +59,7 @@ from the library — not something an embedding page needs to bring along.
│ └── author-profile.css # Author profile card (avatar, bio, social links)
├── js/
│ ├── config-default.js # Shipped defaults for BASE_URL, MARKED_URL, ARTICLES_URL
│ ├── config.js # Your local overrides — gitignored; see "Local configuration" below
│ ├── config.js # Optional local overrides — gitignored; see "Local configuration" below
│ ├── frontmatter.js # parseFrontmatter() — pure YAML-front parser
│ ├── templates.js # loadTemplates(), templates cache
│ ├── discovery.js # discoverArticles(), fetchJson(), article cache
@@ -89,26 +89,21 @@ from the library — not something an embedding page needs to bring along.
## Local configuration
**Before first use, create your own `js/config.js`** — it doesn't ship with the repo (it's
gitignored) and nothing works without it:
```bash
cp js/config-default.js js/config.js
```
`js/config-default.js` holds the shipped defaults (`BASE_URL`, `MARKED_URL`, `ARTICLES_URL`) and
is tracked in git; every other module imports from `js/config.js` instead, which by default just
re-exports those defaults unchanged. The split exists for using cmjs as a git submodule (or
otherwise pulling in a newer version some other way): since `config.js` is gitignored, updating
cmjs never overwrites it, so any local overrides you've made survive the update. Only
`config-default.js` gets updated.
is tracked in git; every other module imports from it. It also loads an optional `js/config.js`
— gitignored, not part of cmjs's own git history — to override any of them. **Nothing needs to
be created before first use**: if `js/config.js` doesn't exist, the shipped defaults apply as-is.
To override a setting, re-export the rest from `config-default.js` and define your own instead —
e.g. to move where articles live (see "Pointing articles/ somewhere else" below for the matching
nginx-side change this needs):
The split exists for using cmjs as a git submodule (or otherwise pulling in a newer version some
other way): since `config.js` is gitignored, updating cmjs never touches it, so any override
you've made survives. Only `config-default.js` gets updated.
To override a setting, create `js/config.js` and export just the constant(s) you want to change —
anything you don't export falls back to `config-default.js`. For example, to move where articles
live (see "Pointing articles/ somewhere else" below for the matching nginx-side change this
needs):
```js
export { BASE_URL, MARKED_URL } from './config-default.js';
export const ARTICLES_URL = new URL('/blog-content/articles/', location.origin).href;
```
@@ -280,9 +275,6 @@ translate it by editing that page directly (`demo/index.html` for this repo's ow
## Embedding in an existing page
Make sure you've done the one-time setup in "Local configuration" above (`cp js/config-default.js
js/config.js`) wherever you place these files — nothing loads without it.
To add cmjs to any HTML page, three things are needed: the CSS, the two mount-point `<div>`s, and the script.
### 1. CSS — in `<head>`
@@ -529,9 +521,6 @@ of content is ever fetched per page view.
## Running locally
Don't forget the one-time setup step in "Local configuration" above (`cp js/config-default.js
js/config.js`) — nothing loads without it.
Using nginx locally (example for Linux):
```bash
# Symlink or copy the repo to your nginx webroot, then reload:
+31 -17
View File
@@ -1,28 +1,42 @@
// Shipped defaults for BASE_URL, MARKED_URL, ARTICLES_URL, plus the loader
// that lets an optional local js/config.js (gitignored — not part of this
// repo's history; see "Local configuration" in the README) override any of
// them. Every other module imports from THIS file, not config.js directly —
// config.js might not exist at all (nothing has created one yet, or it was
// deleted), and a plain `import ... from './config.js'` would then break
// the whole module graph with no way to catch it. A dynamic import() can be
// caught, so that's what resolves it below; top-level await makes every
// module that imports this one wait for that resolution before it runs,
// same as if these were plain synchronous exports.
//
// config.js, if present, only needs to export whichever of these it wants
// to change — anything it doesn't export falls back to the default here.
// All paths are derived from this module's own location via import.meta.url,
// so they resolve correctly regardless of where the embedding HTML page lives.
//
// This file ships with cmjs and is tracked in git — don't edit it directly.
// Everything else imports from the sibling `config.js` instead (gitignored,
// not part of this repo's history), which re-exports these defaults as-is
// unless overridden. That split means your own overrides (e.g. a custom
// ARTICLES_URL) survive updating cmjs — as a git submodule or by any other
// means of pulling in a newer version — since a fresh copy of this file
// never touches your untracked config.js. See "Local configuration" in the
// README.
const DEFAULT_BASE_URL = new URL('../', import.meta.url).href;
const DEFAULT_MARKED_URL = new URL('../vendor/marked.min.js', import.meta.url).href;
const DEFAULT_ARTICLES_URL = new URL('articles/', DEFAULT_BASE_URL).href;
let overrides = {};
try {
overrides = await import('./config.js');
} catch {
// No local config.js (or it failed to load) — shipped defaults stand.
}
// Base URL of the repo root — strings/ and templates/ live here, one level
// up from this file (e.g. https://example.com/).
export const BASE_URL = new URL('../', import.meta.url).href;
export const BASE_URL = overrides.BASE_URL ?? DEFAULT_BASE_URL;
// Path to the marked.js vendor script — lives in vendor/, a sibling of this
// file's own js/ directory, not alongside the app's own modules.
export const MARKED_URL = new URL('../vendor/marked.min.js', import.meta.url).href;
export const MARKED_URL = overrides.MARKED_URL ?? DEFAULT_MARKED_URL;
// Base URL of the articles/ folder. Defaults to a sibling of this file's own
// js/ directory (the repo root's own articles/), but articles don't have to
// live anywhere near the rest of cmjs — replace this line to point anywhere
// under the same origin (`new URL('/blog-content/articles/', location.origin).href`)
// or even a different one, CORS permitting. Whatever it resolves to must
// still be a directory nginx serves a JSON (or HTML) autoindex listing for
// — see "nginx configuration" in the README.
export const ARTICLES_URL = new URL('articles/', BASE_URL).href;
// live anywhere near the rest of cmjs — override it in config.js to point
// anywhere under the same origin, or even a different one, CORS permitting.
// Whatever it resolves to must still be a directory nginx serves a JSON (or
// HTML) autoindex listing for — see "nginx configuration" in the README.
export const ARTICLES_URL = overrides.ARTICLES_URL ?? DEFAULT_ARTICLES_URL;
+1 -1
View File
@@ -1,5 +1,5 @@
import { parseFrontmatter } from './frontmatter.js';
import { BASE_URL, ARTICLES_URL } from './config.js';
import { BASE_URL, ARTICLES_URL } from './config-default.js';
import { mapWithConcurrency } from './concurrency.js';
import { getAllCacheEntries, setCacheEntry, pruneCache } from './cache.js';
import { t } from './i18n.js';
+1 -1
View File
@@ -5,7 +5,7 @@
// locale, that file is fetched too and merged on top — a translation file
// only needs to cover the keys it actually translates, missing ones still
// render in English rather than breaking.
import { BASE_URL } from './config.js';
import { BASE_URL } from './config-default.js';
const FALLBACK_LOCALE = 'en';
+1 -1
View File
@@ -12,7 +12,7 @@
// 4 segments, first = "category", third = "page" → category filter, page N (#category/linux/page/2)
// 4 segments, first = "tag", third = "page" → tag filter, page N (#tag/guides/page/2)
import { MARKED_URL } from './config.js';
import { MARKED_URL } from './config-default.js';
import { loadTemplates } from './templates.js';
import { loadStrings, t } from './i18n.js';
import { loadArticleList, loadAuthorView, loadCategoryView, loadTagView, loadArticle, loadSearch } from './views.js';
+1 -1
View File
@@ -2,7 +2,7 @@
// Each template needs its own <template> element — reusing one would overwrite
// the shared DocumentFragment, leaving both pointing to the last HTML assigned.
import { BASE_URL } from './config.js';
import { BASE_URL } from './config-default.js';
export const templates = {};