diff --git a/README.md b/README.md index f0b2d66..d70606f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmjs/blog-config/.gitkeep b/cmjs/blog-config/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/cmjs/blog-config/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/cmjs/blog-config/tags.json b/cmjs/blog-config/tags.json deleted file mode 100644 index b568eda..0000000 --- a/cmjs/blog-config/tags.json +++ /dev/null @@ -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 - } - ] -} diff --git a/cmjs/js/discovery.js b/cmjs/js/discovery.js index e783e3d..5b5fb3e 100644 --- a/cmjs/js/discovery.js +++ b/cmjs/js/discovery.js @@ -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 }; diff --git a/cmjs/js/views.js b/cmjs/js/views.js index 7aa4430..68a2e31 100644 --- a/cmjs/js/views.js +++ b/cmjs/js/views.js @@ -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 = `

${t('loadingGeneric')}

`; 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(); }),