The repo is already named cmjs, so the nested cmjs/ subfolder was redundant. Moved css/, js/, strings/, templates/, and the gitignored articles/ up to the repo root and removed the empty cmjs/ directory, so the whole repo (or just css/js/strings/templates/articles) can be dropped into an existing site and embedded as-is — no wrapper folder required. This also fixes a real bug: js/config.js's MARKED_URL previously reached outside cmjs/ (../../js/marked.min.js) to find the vendored marked.min.js, which already lived at the repo root as a sibling of cmjs/ rather than inside it. Now that both files sit in the same js/ folder, MARKED_URL is a same-directory reference and no longer depends on anything outside this one folder tree. BASE_URL's own formula is unchanged (config.js is still exactly one level below whatever holds articles/, strings/, and templates/ — previously that was cmjs/, now it's the repo root). Updated index.html, .gitignore (/cmjs/articles/ -> /articles/), and every path reference throughout README.md (project structure diagram, embedding instructions, nginx config, discovery/caching docs) to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
94 lines
5.2 KiB
JavaScript
94 lines
5.2 KiB
JavaScript
// Entry point — loads templates then dispatches routes on hash changes.
|
|
//
|
|
// Hash routing scheme:
|
|
// 0 segments → all articles, page 1
|
|
// first = "search" → search results (#search/query)
|
|
// first = "search", ends "/page/N" → search results, page N (#search/query/page/2)
|
|
// 2 segments, first = "page" → all articles, page N (#page/2)
|
|
// 2 segments, first = "author" → author filter, page 1 (#author/alice)
|
|
// 2 segments, first = "category" or "tag" → filtered list, page 1
|
|
// 3 segments, first = "post" → single article (#post/alice/my-article)
|
|
// 4 segments, first = "author", third = "page" → author filter, page N (#author/alice/page/2)
|
|
// 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 { loadTemplates } from './templates.js';
|
|
import { loadStrings, t } from './i18n.js';
|
|
import { loadArticleList, loadAuthorView, loadCategoryView, loadTagView, loadArticle, loadSearch } from './views.js';
|
|
|
|
// Dynamically inject marked.js as a plain global script before the app starts.
|
|
function loadScript(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const s = document.createElement('script');
|
|
s.src = url;
|
|
s.onload = resolve;
|
|
s.onerror = () => reject(new Error(`Failed to load ${url}`));
|
|
document.head.appendChild(s);
|
|
});
|
|
}
|
|
|
|
// Parse a page-number segment — returns 1 for any non-positive-integer value.
|
|
const parsePage = s => { const n = +s; return Number.isInteger(n) && n >= 1 ? n : 1; };
|
|
|
|
function router() {
|
|
const hash = location.hash.slice(1); // strip leading #
|
|
const parts = hash.split('/').filter(Boolean);
|
|
|
|
// Search: decode query and guard against empty string (#search or #search/).
|
|
// A trailing "/page/N" is peeled off for pagination first — the query
|
|
// itself is always a single encodeURIComponent()'d segment when built
|
|
// by this app (see the search-form handler below), so this only
|
|
// matters for hand-typed/legacy multi-segment links, where joining the
|
|
// rest back with "/" keeps a literal slash in the query working too.
|
|
if (parts[0] === 'search') {
|
|
const rest = parts.slice(1);
|
|
const hasPage = rest.length >= 3 && rest[rest.length - 2] === 'page' && /^\d+$/.test(rest[rest.length - 1]);
|
|
const queryParts = hasPage ? rest.slice(0, -2) : rest;
|
|
const q = decodeURIComponent(queryParts.join('/'));
|
|
if (q) loadSearch(q, hasPage ? parsePage(rest[rest.length - 1]) : 1); else loadArticleList();
|
|
}
|
|
else if (parts.length === 0) loadArticleList();
|
|
else if (parts.length === 2 && parts[0] === 'page') loadArticleList(null, parsePage(parts[1]));
|
|
else if (parts.length === 2 && parts[0] === 'author') loadAuthorView(decodeURIComponent(parts[1]));
|
|
else if (parts.length === 2 && parts[0] === 'category') loadCategoryView(decodeURIComponent(parts[1]));
|
|
else if (parts.length === 2 && parts[0] === 'tag') loadTagView(decodeURIComponent(parts[1]));
|
|
else if (parts.length === 3 && parts[0] === 'post') loadArticle(parts.slice(1).map(decodeURIComponent).join('/'));
|
|
else if (parts.length === 4 && parts[0] === 'author' && parts[2] === 'page') loadAuthorView(decodeURIComponent(parts[1]), parsePage(parts[3]));
|
|
else if (parts.length === 4 && parts[0] === 'category' && parts[2] === 'page') loadCategoryView(decodeURIComponent(parts[1]), parsePage(parts[3]));
|
|
else if (parts.length === 4 && parts[0] === 'tag' && parts[2] === 'page') loadTagView(decodeURIComponent(parts[1]), parsePage(parts[3]));
|
|
else loadArticleList();
|
|
}
|
|
|
|
// All routing — initial and hashchange — waits for this before running.
|
|
let ready;
|
|
|
|
function init() {
|
|
// Wire up search form submission → navigate to #search/query
|
|
document.getElementById('search-form')?.addEventListener('submit', e => {
|
|
e.preventDefault();
|
|
const q = document.getElementById('search-input')?.value?.trim();
|
|
if (q) location.hash = `#search/${encodeURIComponent(q)}`;
|
|
});
|
|
|
|
// Strings load first — everything else (including the error handler
|
|
// right below) may call t(), so it needs to already have resolved.
|
|
ready = loadStrings().then(() => Promise.all([loadScript(MARKED_URL), loadTemplates()]));
|
|
|
|
// Show a top-level error if dependencies fail to load
|
|
ready.catch(err => {
|
|
document.body.textContent = t('appStartError', { message: err.message });
|
|
});
|
|
|
|
ready.then(router).catch(() => {});
|
|
}
|
|
|
|
// readyState guard in case DOMContentLoaded already fired
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
window.addEventListener('hashchange', () => ready.then(router).catch(() => {}));
|