Remove the tag label registry entirely (AI)

Tag ids are now displayed as-is everywhere a label was previously
looked up (card/article pills, the tag filter sidebar, the tag filter
heading, and search matching) — no more cmjs/blog-config/tags.json,
fetchTagRegistry(), or the tagLabels/ensureTags() machinery in
views.js that read it.

The tag filter sidebar now excludes year-YYYY ids by an explicit
regex instead of relying on the registry never listing them (that was
an accidental side effect of iterating tagLabels' own entries).

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 13:40:33 +02:00
1 parent e7f8939ced
commit d3c6789cfe
5 files changed
+12 -139

No files matched your search

-20
View File
@@ -73,8 +73,6 @@ cmjs/
│ ├── card.html # Article list card layout
│ ├── article.html # Full article view layout
│ └── author-profile.html # Author profile card layout
├── blog-config/
│ └── tags.json # Tag id → human-readable label registry — see "Tag labels" below
└── articles/
└── author/ # One directory per author
├── author.md # Author profile (optional)
@@ -133,24 +131,6 @@ metadata never requires fetching anything beyond the directory listing that alre
no further per-article request at all, which is what makes the "Article discovery & caching"
section below possible.
### Tag labels
A tag in the file name is just an id (e.g. `guides`) — the human-readable label shown on
tag pills comes from `cmjs/blog-config/tags.json`:
```json
{
"schemaVersion": 1,
"tags": [
{ "id": "guides", "label": "Guides", "description": "", "aliases": [], "active": true }
]
}
```
Registering an id here is optional — an unregistered tag id still works, it just displays as
its raw id instead of a friendly label. `year-YYYY` year ids don't need to be registered here;
they're recognised automatically by pattern and don't use this label registry.
### Frontmatter format
```yaml
-1
View File
@@ -1 +0,0 @@
-89
View File
@@ -1,89 +0,0 @@
{
"schemaVersion": 1,
"tags": [
{
"id": "guides",
"label": "Guides",
"description": "",
"aliases": [],
"active": true
},
{
"id": "news",
"label": "News",
"description": "",
"aliases": [],
"active": true
},
{
"id": "tutorials",
"label": "Tutorials",
"description": "",
"aliases": [],
"active": true
},
{
"id": "announcements",
"label": "Announcements",
"description": "",
"aliases": [],
"active": true
},
{
"id": "interviews",
"label": "Interviews",
"description": "",
"aliases": [],
"active": true
},
{
"id": "opinion",
"label": "Opinion",
"description": "",
"aliases": [],
"active": true
},
{
"id": "case-studies",
"label": "Case Studies",
"description": "",
"aliases": [],
"active": true
},
{
"id": "community",
"label": "Community",
"description": "",
"aliases": [],
"active": true
},
{
"id": "releases",
"label": "Releases",
"description": "",
"aliases": [],
"active": true
},
{
"id": "behind-the-scenes",
"label": "Behind the Scenes",
"description": "",
"aliases": [],
"active": true
},
{
"id": "faq",
"label": "FAQ",
"description": "",
"aliases": [],
"active": true
},
{
"id": "resources",
"label": "Resources",
"description": "",
"aliases": [],
"active": true
}
]
}
-12
View File
@@ -84,18 +84,6 @@ export function resolveMediaUrl(value, articlePath = '') {
return new URL(path.replace(/^\//, ''), articlePath || BASE_URL).href;
}
let tagCache = null;
export async function fetchTagRegistry() {
if (tagCache) return tagCache;
try {
const response = await fetch(`${BASE_URL}blog-config/tags.json`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const registry = await response.json();
tagCache = Array.isArray(registry.tags) ? registry.tags : [];
} catch { tagCache = []; }
return tagCache;
}
let articleCache = null;
let crawlPromise = null;
let crawlStats = { total: 0, loaded: 0, authorsTotal: 0, authorsFailed: 0 };
+12 -17
View File
@@ -1,10 +1,9 @@
import { templates } from './templates.js';
import { deriveContentFields, discoverArticles, fetchAuthorProfile, fetchTagRegistry, findArticle, getCrawlStats, hydrateArticles, onArticlesProgress, resolveMediaUrl } from './discovery.js';
import { deriveContentFields, discoverArticles, fetchAuthorProfile, findArticle, getCrawlStats, hydrateArticles, onArticlesProgress, resolveMediaUrl } from './discovery.js';
import { parseFrontmatter } from './frontmatter.js';
import { t, getLocale } from './i18n.js';
const PAGE_SIZE = 20;
let tagLabels = new Map();
// Bumped at the start of every list-rendering view. A progress listener
// checks its own snapshot of this before touching the DOM, so a background
@@ -12,10 +11,6 @@ let tagLabels = new Map();
// navigated away from (e.g. clicked into a different filter/page).
let currentGeneration = 0;
async function ensureTags() {
if (!tagLabels.size) tagLabels = new Map((await fetchTagRegistry()).map(tag => [tag.id, tag.label]));
}
function formatDate(value) {
if (!value) return '';
const date = new Date(`${String(value).slice(0, 10)}T12:00:00`);
@@ -46,7 +41,7 @@ function clearSearchInput() {
function filterLabel(filter) {
if (!filter) return '';
if (filter.type === 'author') return t('filterAuthor', { value: filter.value });
if (filter.type === 'tag') return t('filterTag', { value: tagLabels.get(filter.value) || filter.value });
if (filter.type === 'tag') return t('filterTag', { value: filter.value });
return t('filterYear', { value: filter.value.replace(/^year-/i, '') });
}
@@ -81,7 +76,7 @@ function renderTags(container, tags) {
const link = document.createElement('a');
link.className = 'tag';
link.href = `#tag/${encodeURIComponent(identifier)}`;
link.textContent = tagLabels.get(identifier) || identifier;
link.textContent = identifier;
container.appendChild(link);
});
}
@@ -137,12 +132,15 @@ function renderFilterNavigation(articles, activeFilter = null) {
const tagList = document.createElement('div');
tagList.className = 'filter-list';
if (!years) tagList.appendChild(allArticlesLink());
[...tagLabels.entries()]
.filter(([id]) => tagCounts.has(id))
.sort((a, b) => (tagLabels.get(a[0]) || a[0]).localeCompare(tagLabels.get(b[0]) || b[0], getLocale()))
.forEach(([id, label]) => {
// A `year-YYYY` id is also a normal entry in `article.tags` (that's how
// it's recognised as the category — see buildStub() in discovery.js),
// but it belongs in the years nav above, not duplicated here.
[...tagCounts.keys()]
.filter(id => !/^year-20\d{2}$/i.test(id))
.sort((a, b) => a.localeCompare(b, getLocale()))
.forEach(id => {
const active = activeFilter?.type === 'tag' && activeFilter.value === id;
tagList.appendChild(renderFilterLink(label, `#tag/${encodeURIComponent(id)}`, tagCounts.get(id), active));
tagList.appendChild(renderFilterLink(id, `#tag/${encodeURIComponent(id)}`, tagCounts.get(id), active));
});
tags.appendChild(tagList);
}
@@ -293,7 +291,6 @@ export async function loadArticleList(filter = null, page = 1) {
const unsubscribe = onArticlesProgress((partialArticles, done) => { render(partialArticles, done); });
try {
await ensureTags();
const allArticles = await discoverArticles();
await render(allArticles, true);
} catch (error) {
@@ -377,9 +374,8 @@ export async function loadSearch(query, page = 1) {
// after, which router.js recognises and parses back into (query, page).
const filter = { type: 'search', value: query };
try {
await ensureTags();
const needle = query.toLocaleLowerCase(getLocale());
const matchesQuery = article => [article.title, article.description, article.author, article.authorName, article.category, ...article.tags.map(tag => tagLabels.get(tag) || tag)]
const matchesQuery = article => [article.title, article.description, article.author, article.authorName, article.category, ...article.tags]
.some(value => String(value || '').toLocaleLowerCase(getLocale()).includes(needle));
const stubs = await discoverArticles();
@@ -475,7 +471,6 @@ export async function loadArticle(hash) {
const view = document.getElementById('article-view');
view.innerHTML = `<p class="loading">${t('loadingGeneric')}</p>`;
try {
await ensureTags();
const article = await findArticle(hash);
const [raw, profile] = await Promise.all([
fetch(article.path).then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.text(); }),