Adds a searchQueries IndexedDB store (js/cache.js) keyed by the exact
search text, holding a { articlePath: { mtime, matched } } map per
query. loadSearch() loads it once per new query, trusts a cached
result whenever an article's mtime still matches instead of running
matchesQuery() again, and writes back whatever's newly checked each
scan round. Repeat searches for the same term — even after a page
reload — now only need to actually check articles that are new or
have changed since, not the whole site again. Capped at
MAX_CACHED_SEARCHES (30) distinct queries with least-recently-used
eviction so trying many different terms over time doesn't grow the
cache unbounded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hydrateArticleNow()'s catch-all discarded network, HTTP, and parsing
errors alike with no diagnostic, using the same fallback for all of
them. A real bug in parseFrontmatter()/deriveContentFields() would
silently leave an article title-less forever with no way to tell it
apart from a transient fetch blip. Now logged via console.error.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
document.title was already set from the siteName string, but a
visible #site-title header link stayed hardcoded in whatever language
it was typed in — a translated site showed a translated tab title
next to an untranslated header. Set once in router.js's init() rather
than showListView(), so a page that loads straight into an article
route still gets it. Documented #site-title as optional embedding
markup and corrected README's claim that <title> isn't JS-templated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wave 2 of the crawl fetched every author's directory listing via a
plain Promise.all, with no limit — a site with hundreds of authors
fired one simultaneous request per author on every cold-cache load.
Routed through mapWithConcurrency/MAX_CONCURRENT_REQUESTS, the same
cap hydrateArticles() already respects for content fetches.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The onArticlesProgress listener invoked async render() without
awaiting or catching it, unlike the final render() call, which is
inside a try/catch. A mid-crawl render failure was an unhandled
rejection that left the list stuck on its loading state forever
instead of showing articlesLoadError.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The loaded/total ratio check treated a low filename-match rate as a
sign of network/CORS breakage, but indexing does no per-article fetch
at all — a low ratio just means many non-conforming .md files coexist
with real articles, which is normal and not a health problem. Removed
that heuristic and its now-unused crawlHealthArticlesFailed string;
real breakage is still fully caught by the authors-failed check.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A blank line between a block-array key and its "- item" lines matched
neither the item nor the key regex, resetting arrayKey to null and
causing the following items to be silently skipped. Blank lines are
now ignored instead of ending the array.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
resolveStub() required a non-null mtime to reuse a cached hydrated
entry, so any listing without a parseable mtime (HTML-autoindex
fallback edge cases) rebuilt every stub from scratch on every crawl.
Comparing mtime directly (including null === null) trusts the cache
when there's no evidence either way, same as before this changed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drafts now live in an author's own draft/ subdirectory instead of a
leading-underscore filename prefix. The crawler already skips
subdirectories when listing an author's files, so nothing under
draft/ is ever indexed; nginx should also deny that path outright so
drafts aren't fetchable by a guessed direct URL either.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cache.js's comment explaining the DB_NAME prefix change referenced
the old "cmjs-" prefix literally as a historical note. Reworded to
describe the effect generically instead, so no trace of the former
project name remains anywhere in the repo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Updates every in-repo mention: README.md title/prose/example paths,
code comments in config-default.js and router.js, and the IndexedDB
DB_NAME prefix in cache.js (cmjs-cache-v3 -> cmkiss-cache-v3).
The DB_NAME change has the same effect as a schema-version bump --
every returning visitor's browser opens a fresh, empty cache under the
new name and the old cmjs-cache-v3 database is left as orphaned
storage. Harmless (cache.js degrades to live-fetch when the cache is
empty), noted in a comment there rather than left as an unexplained
side effect.
Two things this commit does NOT cover, both outside this checkout:
- The local directory itself (still /home/user/Projects/cmjs) --
renamed separately right after this commit.
- The git hosting-side repo name/remote URL
(git.astronomickaexpedice.cz, Astronomicka_Expedice/cmjs.git) --
requires server-side access this session doesn't have; needs a
manual rename on the host plus a local `git remote set-url` update
to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A plain static `import ... from './config.js'` can't gracefully
degrade if that file doesn't exist -- a missing module breaks the
whole graph before any application code can catch it. So the four
consumers (discovery.js, i18n.js, templates.js, router.js) now import
BASE_URL/MARKED_URL/ARTICLES_URL from config-default.js instead, which
tries a dynamic import() of the optional config.js (catchable, unlike
a static import) and merges in whatever it exports, falling back to
its own shipped defaults for anything config.js doesn't override --
or doesn't exist at all. Top-level await makes every importing module
wait for that resolution first, same as a plain synchronous export.
config.js itself is now just an optional, empty-by-default file: only
export the specific constant(s) you want to change. Updated the
README's "Local configuration" section (and its three cross-references
in Running locally / Embedding in an existing page / Pointing
articles/ somewhere else) to drop the now-unnecessary "copy this file
before first use" step entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Lets cmjs be used as a git submodule (or updated by any other means)
without a local config override -- e.g. a custom ARTICLES_URL -- being
clobbered by the update. js/config-default.js now holds the shipped
defaults and is tracked; js/config.js is gitignored and re-exports
those defaults unless overridden, and is what every other module
actually imports from.
Since js/config.js no longer ships with the repo, added a required
one-time setup step (cp js/config-default.js js/config.js) -- new
"Local configuration" section in the README, cross-referenced from
"Running locally", "Embedding in an existing page", and "Pointing
articles/ somewhere else".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Real bugs:
- loadArticle()/loadAuthorView() had no stale-render guard (unlike
loadArticleList()/loadSearch()) -- a slower, superseded request could
overwrite a newer one's render. Both now capture and check a
generation snapshot.
- #post/... links interpolated article.hash (raw, unencoded
"author/slug") directly, but router.js unconditionally
decodeURIComponent()s each segment when parsing -- would throw
(silently swallowed) for any author/slug containing a literal "%" or
other reserved character. Links now go through a new postHref()
helper that encodes each segment individually.
- A startup failure replaced the entire document.body, which would
wipe out a host site's own page content when cmjs is embedded
alongside it. Now scoped to cmjs's own mount points when at least
one exists.
- hydrateArticle() had no in-flight de-duplication -- two concurrent
callers for the same not-yet-hydrated stub (e.g. a page's hydration
racing the next page's prefetch) could each fire a duplicate fetch.
Now tracked via a WeakMap keyed by the stub.
- Leftover "expedition blog"/"expedition year" wording missed by the
earlier astronomy-club genericization pass (that pass's grep didn't
cover the English word "expedition").
Robustness gaps:
- cache.js no longer permanently disables the whole IndexedDB cache
after a single failed transaction -- only openDb() itself failing
does that now.
- Listing mtimes are normalized to minute precision at the one point
they enter the system, so cache comparisons stay consistent even if
a server flips between JSON and HTML-fallback autoindex.
- The root author-listing fetch failing outright is now treated like
"every author's listing failed" (a new rootFailed crawl stat) instead
of surfacing a generic, less-specific error.
- router.js now checks both required mount points exist right after
startup and fails with a clear, catchable message instead of a bare
TypeError the first time showListView()/showArticleView() runs.
- The article view's "back" link now returns to whatever list/search/
filter page the user actually came from (tracked via lastListHash)
instead of always resetting to page 1 of everything.
- filterLabel() now checks filter.type === 'category' explicitly
instead of falling through to it as the default case.
- Author-name filtering is now case-sensitive, matching how author
paths/profile lookups are looked up everywhere else.
- Documented (in code comments and a new README note) that article/bio
Markdown renders via marked.js with no HTML-sanitization pass --
only trusted authors should be adding content.
Minor:
- Extracted applyArticleMetaFields() to share the date/reading-time/
category-badge logic between renderCard() and loadArticle() instead
of duplicating it.
- Clarified resolveMediaUrl()'s WordPress-migration special case as
legacy compatibility, not a general-purpose feature.
- frontmatter.js no longer requires a trailing newline after the
closing "---" -- a file that's nothing but frontmatter now parses
instead of falling through to "no frontmatter at all".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
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
Third-party code is now kept separate from the app's own js/ modules.
Updated js/config.js's MARKED_URL to the new sibling-directory path
(../vendor/marked.min.js) and the README's project structure diagram
accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
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