2026-08-23 21:54:17 +02:00
2026-08-23 21:54:17 +02:00
2026-08-23 21:54:17 +02:00
2026-08-23 21:54:17 +02:00
2026-08-23 21:54:17 +02:00
2026-08-23 21:54:17 +02:00

cmjs — Minimal Client-Side CMS

A minimal CMS where articles are Markdown files rendered in the browser. No build step, no database, no server-side code.

How it works

  1. The browser traverses cmjs/articles/ via nginx JSON autoindex — discovering authors, categories, and article files.
  2. Each .md file's YAML frontmatter is parsed to build the article list.
  3. Clicking an article fetches the Markdown file and renders it with marked.js.
  4. Hash-based routing handles navigation — back/forward works without page reloads.

Features

  • Dark mode — automatically follows the OS prefers-color-scheme setting; no toggle needed.
  • Client-side search — the search box in the header filters all articles by title, description, author, and category in-memory. Navigates to #search/query so results are bookmarkable.
  • Reading time — estimated from the article word count (200 wpm) and shown on list cards and in article headers. Computed automatically; no frontmatter field needed.
  • Table of contents — auto-generated from h2/h3 headings and inserted at the top of the article body. Only appears when the article has three or more headings.
  • Back link — a "← Back to articles" link above every article for one-click navigation back to the list.
  • Pagination — article list paginates at 20 articles per page; page state is encoded in the hash so browser history works.
  • Author and category filtering — clicking an author name or category badge shows a filtered list. Both support pagination.
  • Responsive layout — layout, cards, images, and pagination all adapt to narrow screens (breakpoint at 640 px).
  • Keyboard navigation — all interactive elements have visible :focus-visible outlines for keyboard users.
  • Author profiles — a profile card with bio and social links is shown at the top of each author's article list. Created by adding articles/{author}/{author}.md.

Project structure

cmjs/
├── README.md
├── index.html              # Single-page shell — served from the repo root
├── css/                    # Site-level styles for index.html
│   ├── reset.css           # CSS variables, dark mode, box-sizing reset, focus styles
│   ├── header.css          # Site header, title link, search form
│   └── layout.css          # Main content wrapper, state messages, responsive
├── js/
│   └── marked.min.js       # marked.js (local copy, loaded as a global)
└── cmjs/                   # App content — articles, templates, component styles
    ├── css/
    │   └── components/
    │       ├── card.css        # Article list cards and filter heading
    │       ├── article.css     # Single article view, back link, hero image
    │       ├── content.css     # Rendered markdown styles (.content *)
    │       ├── pagination.css  # Prev/next page navigation
    │       └── toc.css         # Auto-generated table of contents
    ├── js/
    │   ├── config.js       # BASE_URL, MARKED_URL, PICTURES_URL (all via import.meta.url)
    │   ├── frontmatter.js  # parseFrontmatter() — pure YAML-front parser
    │   ├── templates.js    # loadTemplates(), templates cache
    │   ├── discovery.js    # discoverArticles(), fetchJson(), article cache
    │   ├── views.js        # loadArticleList, loadArticle, loadAuthorView, loadCategoryView, loadSearch
    │   └── router.js       # Hash router and app entry point
    ├── templates/
    │   ├── card.html       # Article list card layout
    │   └── article.html    # Full article view layout
    ├── articles/
    │   └── author/         # One directory per author
    │       └── category/   # One directory per category
    │           └── slug.md # Article file
    └── pictures/           # Article images (referenced via image: frontmatter field)

Adding an article

  1. Create the author and category directories if they don't exist: cmjs/articles/author/category/
  2. Create a .md file inside: cmjs/articles/author/category/my-article.md
  3. Add a frontmatter block at the top
  4. Write your content — it will appear in the list automatically

author and category are derived from the directory path — no frontmatter fields needed for them.

Frontmatter format

---
title: My Article Title
date: 2026-06-16
description: A short summary shown in the article list.
draft: false
---

Your markdown content here...
Field Required Notes
title yes Shown in the list and article header
date yes ISO format YYYY-MM-DD, used for sorting newest-first
description no Short summary shown under the title in the list
image no Filename of the preview image in the pictures/ directory (e.g. photo.jpg); shown as card thumbnail and article hero
draft no Set to true, yes, or 1 to hide from the list

Reading time is computed automatically from the article word count — no frontmatter field needed.

Author profiles

To add a profile for an author, create a file named {author}.md directly inside the author's directory:

cmjs/articles/bob/bob.md

The profile appears as a card above the article list when a visitor filters by that author. It is optional — authors without a profile file work normally.

Profile frontmatter

---
title: Bob Johnson
avatar: bob-avatar.jpg
github: https://github.com/bob
twitter: https://twitter.com/bob
linkedin: https://linkedin.com/in/bob
mastodon: https://mastodon.social/@bob
website: https://bob.example.com
---

Short bio rendered as markdown.
Field Notes
title Display name shown in the profile card. Falls back to the author directory name.
avatar Filename in pictures/ — 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.

Routing

URL hash View
(empty) All articles, page 1
#page/2 All articles, page 2
#search/linux Search results for "linux"
#author/alice All articles by alice, page 1
#author/alice/page/2 All articles by alice, page 2
#category/linux All articles in "linux", page 1
#category/linux/page/2 All articles in "linux", page 2
#alice/linux/my-article Single article at cmjs/articles/alice/linux/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.

Customising the layout

The HTML structure for the article list card and the full article view live in cmjs/templates/:

  • cmjs/templates/card.html — the card shown in the article list
  • cmjs/templates/article.html — the full article view (header, hero image, content)

Edit these files directly to change layout — no JS changes needed. Elements are identified by data-field attributes, which is how the JS modules find and populate them. Keep existing data-field values when rearranging; add new ones if you add new fields.

Embedding in an existing page

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>

<!-- Site-level styles (or replace with your own) -->
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/header.css">
<link rel="stylesheet" href="css/layout.css">

<!-- Component styles -->
<link rel="stylesheet" href="cmjs/css/components/card.css">
<link rel="stylesheet" href="cmjs/css/components/article.css">
<link rel="stylesheet" href="cmjs/css/components/content.css">
<link rel="stylesheet" href="cmjs/css/components/pagination.css">
<link rel="stylesheet" href="cmjs/css/components/toc.css">

The site-level CSS files (reset.css, header.css, layout.css) are optional — they style the shell around the CMS. If your page already has a layout and reset, you can skip them and only load the component stylesheets. Note that reset.css also defines the CSS custom properties (colours, radius) used throughout — if you skip it, define the variables yourself.

2. Mount points — in <body>

<div id="list-view"></div>
<div id="article-view" hidden></div>

Place these wherever you want the CMS to appear. The router toggles hidden between them on navigation.

Optionally add a search form anywhere in the page:

<form id="search-form" role="search">
    <input type="search" id="search-input" placeholder="Search articles…" autocomplete="off">
</form>

3. Script — before </body>

<script type="module" src="cmjs/js/router.js"></script>

router.js is the only script tag needed. It loads marked.min.js automatically at startup (path configured in cmjs/js/config.js).

Subdirectory pages: If the embedding page lives in a subdirectory (e.g. /blog/index.html), relative paths like cmjs/... will resolve incorrectly. Use root-relative paths with a leading / for the CSS links and script tag:

<link rel="stylesheet" href="/cmjs/css/components/card.css">
<script type="module" src="/cmjs/js/router.js"></script>

All internal fetches (articles, templates, marked.js) resolve relative to the module files via import.meta.url and work automatically — only the HTML tags need the / prefix.

Minimal example

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Site</title>
    <link rel="stylesheet" href="cmjs/css/components/card.css">
    <link rel="stylesheet" href="cmjs/css/components/article.css">
    <link rel="stylesheet" href="cmjs/css/components/content.css">
    <link rel="stylesheet" href="cmjs/css/components/pagination.css">
    <link rel="stylesheet" href="cmjs/css/components/toc.css">
</head>
<body>
    <div id="list-view"></div>
    <div id="article-view" hidden></div>

    <script type="module" src="cmjs/js/router.js"></script>
</body>
</html>

nginx configuration

Point nginx root at the repo root. The autoindex directive applies recursively to all subdirectories under /cmjs/articles/.

server {
    listen 80;
    server_name example.com;
    root /var/www/cmjs;

    # Serve the SPA
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Enable JSON directory listing for article discovery (applies to all subdirs)
    location /cmjs/articles/ {
        autoindex on;
        autoindex_format json;
    }
}

Note: autoindex_format json requires nginx 1.7.9+. Check with nginx -v.

Running locally

Using nginx locally (example for Linux):

# Symlink or copy the repo to your nginx webroot, then reload:
sudo nginx -s reload

Python's http.server returns HTML directory listings, not JSON, so article discovery won't work with it. Use nginx.

S
Description
No description provided
Readme
812 KiB
0 Stars 4 Watchers 0 Forks
Languages
JavaScript 85.1%
CSS 13%
HTML 1.9%