Finalizing the code #1

Merged
biosias merged 30 commits from claude into main 2026-09-12 16:35:28 +02:00
Member
No description provided.
biosias added 30 commits 2026-09-12 16:35:23 +02:00
Drops the expa-YYYY category directory in favor of author/slug/slug.md
with .tag and .date marker files; adds mtime-based IndexedDB caching,
concurrency-limited crawling, an HTML-autoindex mtime fallback, and the
#post/<author>/<slug> route this layout change requires.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Bumps the IndexedDB DB name on schema changes so a code deploy can't
serve stale-shaped cached articles. One author's directory listing
failing no longer takes the whole site down; a health banner now
covers that all-authors-failed case too, not just the low-ratio one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Adds cmjs/js/i18n.js (t()/loadStrings()/getLocale()) and
cmjs/strings/en.json as the default string table; every hardcoded
Czech UI string in views.js/router.js/discovery.js and the static
index.html shell now goes through it, so a translation just means
adding cmjs/strings/<locale>.json and setting <html lang>.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
cmjs/strings/cs.json restores the site's original Czech UI text
(now sourced as a translation file rather than hardcoded); sk.json
is a new Slovak translation. Both cover every key in en.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
author-profile.css/html and cmjs/blog-config/tags.json existed and
were used but were undocumented. Also fixes the search feature
description (it already matched tags, not just title/description/
author) and adds a tag-filtering bullet to match the routing table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Also fixes renderFilterNavigation() to render years/tags
independently instead of bailing out entirely when #year-filters
is missing, which would have silently broken tag filtering too.
The "all articles" reset link now moves into whichever section
renders first.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
resolveMediaUrl() now resolves frontmatter image/avatar filenames
relative to the referencing .md file's own directory instead of a
shared cmjs/pictures/ folder, which is removed along with
PICTURES_URL. fetchAuthorProfile() now returns its own file path so
avatars resolve the same way. Migrates the existing hello-world
example's preview image into its article directory.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
discoverArticles() now dedupes concurrent callers into one shared
crawl and exposes onArticlesProgress() for incremental results;
loadArticleList() (and everything built on it: pagination, author/
category/tag filters, the sidebar counts) renders as soon as the
first articles resolve and keeps updating while the rest crawl in
the background, with a "still indexing…" note until done. The crawl
also visits the most-recently-touched directories first (mtime,
best-effort) so early results tend to already be close to correct;
final display order is unchanged (always the authoritative `date`).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Splits discovery into a cheap indexing phase (buildStub — directory
listing only: .tag/.date markers and a new `draft` marker file, no
markdown fetch) and a separate hydrateArticle() step that only runs
for articles actually being rendered: the current page's cards, a
visited article, or a search (which needs real text, so it hydrates
everything it's searching). Sorting, filtering, pagination, and the
sidebar counts all work from stubs alone.

`draft` (an empty marker file, same pattern as .tag/.date) replaces
the frontmatter draft:/status: check entirely, since that also needs
to be knowable without a content fetch. Bumps CACHE_SCHEMA_VERSION
since the cached article shape changed (added root/hydrated fields).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
resolveStub() now caches every resolved stub (not just ones that get
hydrated), so a repeat visit skips the listing fetch for articles
nothing has ever rendered too, not only already-viewed ones. Once a
page's cards render, the next page's content hydrates in the
background so "next" is usually an instant cache hit. loadAuthorView
fetches the profile and article list in parallel instead of
sequentially.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
cache.js: replaces the per-directory getCacheEntry() point lookup
with getAllCacheEntries(), a single bulk read via IDBObjectStore
.getAll(). discovery.js's runCrawl() kicks this off in parallel with
the author/post-directory requests and passes the resulting Map into
resolveStub() as a synchronous in-memory lookup, cutting hundreds of
individual IndexedDB round-trips down to one.

hydrateArticles() now takes an optional onProgress callback;
loadSearch() uses it to render matches as hydration resolves instead
of blocking on the whole result set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
router.js: recognises a trailing /page/N on a #search/... hash,
falling back to treating the whole remainder as the query for
hand-typed/legacy links (the app itself always encodes the query as
one segment, so this is unambiguous for real usage).

views.js: renderArticleListBody() is generalized to take an explicit
heading/emptyKey/loadingKey instead of assuming the article-list's
own copy, so loadSearch() can reuse it directly with a
{type:'search', value:query} pseudo-filter driving the same
pageHash()-based pagination links as author/category/tag views.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
No functional change — hydrateArticles() already processes stubs in
their existing array order, and discoverArticles() already sorts
that array newest-first by the authoritative date field, so search
results (and page hydration) already fill in newest-first. Adds a
comment making this explicit, since it's a non-obvious "free"
property of the sort order rather than something the code visibly
does on purpose.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Replaces .tag/.date/draft marker files with a single naming
convention: <slug>--<DDMMYYYY>--<tag1>+<tag2>+... (optionally
prefixed with "_" for draft). This eliminates the per-article
listing fetch entirely — every stub field is now parsed straight
out of the directory name the author-level listing already returns,
so indexing costs one request per author, full stop, regardless of
how many articles exist.

discovery.js: buildStub()/resolveStub() become synchronous (pure
string parsing, no fetch); runCrawl() drops the wave-3
crawl/mapWithConcurrency/mtime-priority-ordering entirely, since
there's no more per-item async work to stage. hydrateArticle() and
everything downstream in views.js is unchanged, since the derived
article shape is identical either way.

Removes the example admin/hello-world article (old format, git
was tracking it) and regenerates 10 authors x 20 articles in the new
format for testing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
A double-hyphen separator collides with an easy authoring typo: an
accidental extra "-" in a kebab-case slug (e.g. "hello--world")
silently misparses as slug "hello" + an invalid date, dropping the
article from the index with no visible error. "~" isn't something
anyone would type by accident while writing a normal slug, and
(like "-") is in the URI "unreserved" set, so it never needs
percent-encoding either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
loadSearch now scans stubs in small batches and stops as soon as it
has enough matches for the requested page (plus one, to know whether
a next page exists), instead of hydrating every article up front.
Scan progress is kept per-query so paging forward resumes without
re-checking, and paging backward needs no scanning at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Articles no longer live in their own per-post directory — each is now a
single `<slug>~<DDMMYYYY>~<tag1>+<tag2>.md` file directly inside its
author's directory, alongside that author's bio file and a shared
`media/` folder for images. This lets multiple articles reference the
same picture without duplicating it per article.

discovery.js: buildStub/resolveStub parse the article's own file name
instead of a wrapping directory's; indexing's per-author listing now
filters to `.md` files (excluding the bio file) rather than
subdirectories, and the author's own media/ folder is simply not a
`.md` file, so it's excluded for free. The article file's path is now
also its own cache key (the `root` concept is gone).

cache.js: bumped CACHE_SCHEMA_VERSION (cache key shape changed) and
renamed the object store from the now-inaccurate "slugDirs".

Migrated all existing test content (10,200 articles) to the new flat
layout; no code changes needed to the articles' own content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
The demo tag registry, site name, and the year-category tag convention
(expa-YYYY) were all modeled on a specific Czech astronomy club. Swapped
in generic, English placeholders instead:

- blog-config/tags.json: 12 generic topic tags (guides, news,
  tutorials, ...) replacing the Czech astronomy-themed ones.
- The year/category tag convention is now `year-YYYY` instead of
  `expa-YYYY` (discovery.js's category regex, the derived field renamed
  expa -> categoryYear, and every views.js/README usage).
- siteName and yearLabel strings (en/cs/sk) and index.html's hardcoded
  title/header text no longer say "Expedition Blog(s)" / "Expa {year}".
- README examples updated to match (kometa -> guides, expa-2026 ->
  year-2026).

Also regenerated all test content (cmjs/articles/, gitignored) with the
new generic tags and year- prefix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
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
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
index.html and its site-shell CSS (reset.css, header.css, layout.css)
now live under demo/, separate from the library's own css/, js/,
strings/, templates/, and articles/ at the repo root. Root css/ now
holds only css/components/ — the styles any embedding page actually
needs.

Fixed demo/index.html's own relative paths to the component CSS and
router.js (one level up now); its own site-shell CSS links needed no
change since they moved along with it. No BASE_URL/MARKED_URL changes
in js/config.js — those resolve from the script's own location via
import.meta.url, independent of wherever the HTML page that loaded it
lives.

Updated README.md throughout: project structure diagram, Translations'
index.html mentions, the Embedding section's site-level CSS example
paths, the nginx config's SPA location block (now /demo/ instead of
root), and Running locally's preview instructions.

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
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
New README subsection under "Embedding in an existing page" explaining
that #list-view/#article-view (the only required pair, also where
search results render), #search-form/#search-input, #tag-filters, and
#year-filters are all independent mount points that can be placed
anywhere in a custom layout rather than stacked the way the demo does
-- includes a #year-filters example, which wasn't documented anywhere
before despite still being fully supported by renderFilterNavigation().

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
Found while reviewing README completeness against the last bug-fix
pass:
- "Crawl health" bullet didn't mention the new rootFailed case (the
  root author-listing fetch failing outright, distinct from every
  individual author's listing failing).
- "Back link" feature bullet still described the old fixed-to-page-1
  behavior instead of the fixed return-to-referring-page behavior.
- No mention that #list-view/#article-view are checked for at startup
  with a friendly error, unlike the truly-optional mount points.
- No mention that author directory names are case-sensitive and
  matched exactly against #author/... URLs and profile lookups.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
Dead code -- nothing in the codebase imported or called it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXSVPS3UmmASiiprPfcnUd
biosias merged commit 78765b95f5 into main 2026-09-12 16:35:28 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Astronomicka_Expedice/cmkiss#1