Make the articles/ folder location independently configurable (AI)

Added ARTICLES_URL to js/config.js, defaulting to the current
behavior (a sibling of js/ at the repo root) but editable to point
anywhere under the same origin (or a different one, CORS permitting)
-- articles no longer have to live alongside the rest of cmjs.

discovery.js's three hardcoded `${BASE_URL}articles/...` usages
(author listing, per-author directory, author bio fetch) now all go
through ARTICLES_URL instead.

Documented in README's nginx configuration section, including that
whatever ARTICLES_URL points at needs its own matching autoindex
location block.

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 15:53:35 +02:00
1 parent e739c7cf8c
commit 22f17a748b
3 files changed
+38 -7

No files matched your search

+23 -1
View File
@@ -58,7 +58,7 @@ from the library — not something an embedding page needs to bring along.
│ ├── toc.css # Auto-generated table of contents
│ └── author-profile.css # Author profile card (avatar, bio, social links)
├── js/
│ ├── config.js # BASE_URL, MARKED_URL (all via import.meta.url)
│ ├── config.js # BASE_URL, MARKED_URL, ARTICLES_URL (all via import.meta.url)
│ ├── frontmatter.js # parseFrontmatter() — pure YAML-front parser
│ ├── templates.js # loadTemplates(), templates cache
│ ├── discovery.js # discoverArticles(), fetchJson(), article cache
@@ -353,6 +353,28 @@ server {
> `autoindex_format json` is ever lost (config drift, an older nginx), the crawler falls back
> to parsing nginx's default HTML directory listing instead of breaking outright.
### Pointing articles/ somewhere else
By default the crawler looks for articles at `articles/` next to the rest of cmjs (`ARTICLES_URL`
in `js/config.js`, computed as a sibling of the `js/` folder). Articles don't have to live there —
edit `ARTICLES_URL` in `js/config.js` to point anywhere under the same origin (or a different one,
CORS permitting), e.g.:
```js
export const ARTICLES_URL = new URL('/blog-content/articles/', location.origin).href;
```
Whatever it resolves to needs its own matching `location` block enabling the JSON (or HTML)
autoindex — it doesn't have to be named `articles/` or nested under wherever cmjs itself lives:
```nginx
location /blog-content/articles/ {
autoindex on;
autoindex_format json;
autoindex_localtime off;
}
```
### Compression
Markdown and directory-listing JSON are both plain text, so gzip shrinks them well — and since
+11 -2
View File
@@ -1,10 +1,19 @@
// 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.
// Base URL of the repo root — articles/, strings/, and templates/ all live
// here, one level up from this file (e.g. https://example.com/).
// 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;
// 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;
// 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;
+4 -4
View File
@@ -1,5 +1,5 @@
import { parseFrontmatter } from './frontmatter.js';
import { BASE_URL } from './config.js';
import { BASE_URL, ARTICLES_URL } from './config.js';
import { mapWithConcurrency } from './concurrency.js';
import { getAllCacheEntries, setCacheEntry, pruneCache } from './cache.js';
import { t } from './i18n.js';
@@ -280,7 +280,7 @@ async function runCrawl() {
const cacheSnapshotPromise = getAllCacheEntries();
// Wave 1 (root) — always fetched live: the author directories.
const authorEntries = await fetchJson(`${BASE_URL}articles/`);
const authorEntries = await fetchJson(ARTICLES_URL);
const authors = authorEntries.filter(entry => entry.type === 'directory' && !entry.name.startsWith('.'));
// Wave 2 — always fetched live, in full, for every author (not just the
@@ -301,7 +301,7 @@ async function runCrawl() {
// silently producing an empty article list.
let authorsFailed = 0;
const authorFileLists = await Promise.all(authors.map(async author => {
const authorRoot = `${BASE_URL}articles/${encodeURIComponent(author.name)}/`;
const authorRoot = `${ARTICLES_URL}${encodeURIComponent(author.name)}/`;
const bioFileName = `${author.name}.md`;
try {
const entries = await fetchJson(authorRoot);
@@ -357,7 +357,7 @@ export async function findArticle(hash) {
}
export async function fetchAuthorProfile(author) {
const path = `${BASE_URL}articles/${encodeURIComponent(author)}/${encodeURIComponent(author)}.md`;
const path = `${ARTICLES_URL}${encodeURIComponent(author)}/${encodeURIComponent(author)}.md`;
try {
const response = await fetch(path);
if (!response.ok) return null;