Files
remark42/site/src/includes/inline.njk
T
Dmitry VerkhoturovandGitHub 929c06d957 site: fetch latest version client-side instead of embedding at build time (#2072)
* site: fetch latest version client-side instead of embedding at build time

The header version badge was filled in by site/src/data/github.js calling
the GitHub releases API at Eleventy build time and baking data[0].tag_name
into every page. This had three failure modes:

1. Layered cache: Buildx caches the yarn build layer; on a release-triggered
   workflow nothing under ./site changes, so the cached HTML (with the
   previous tag baked in) gets shipped. v1.16.0 went out and remark42.com
   kept showing v1.15.0 until a separate site/ commit landed and naturally
   invalidated the COPY layer.

2. Tag mismatch: the deploy pulls ghcr.io/umputun/remark42-site:master,
   but release events build :v1.16.0 and :latest only. A cache-skip
   workflow tweak wouldn't even reach the served image.

3. API propagation race: the workflow fires ~2s after release publish,
   so even with cache disabled the API might still return the previous
   tag from a stale read replica.

All three vanish if the version is fetched in the browser. GitHub serves
the /releases/latest response with Cache-Control: public, max-age=60 so
per-visitor cost is bounded; failures fall through silently and the
badge stays empty rather than wrong.

Changes:
- header.njk: replace {{ github.latestVersion }} with a
  <span data-remark42-version></span> placeholder.
- inline.js: add a fetch of /releases/latest that fills any
  [data-remark42-version] element on the page. fallback is no-op on any
  network/parse failure.
- delete site/src/data/github.js (Eleventy data file is no longer used).
- drop node-fetch from devDependencies (was used only by github.js).

* site: address PR review on version badge fetch

- gate DOM update on DOMContentLoaded — inline.js is loaded sync in <head>,
  so a cache-hit fetch can resolve before the placeholder span is parsed.
- hide placeholder span by default (`hidden`) so a failed/blocked fetch
  doesn't leave a 0.5rem stray gap before the github icon.
- log fetch failures (rate limit, offline, blocked) instead of silently
  swallowing — matches the prior behaviour of build-time github.js.

* site: cache latest version in sessionStorage with 1h TTL

avoids hitting the GitHub API on every page load — repeated navigations
within a tab read from sessionStorage instead. TTL caps stale display at
1h for very long-lived tabs. cleared on tab close, so each new session
fetches once and reuses the result throughout.

* site: address PR review on header & version fetch

Copilot review on the cache commit raised three points:

1. inline.js had a hard-coded `https://api.github.com/repos/umputun/remark42`
   while the templates use `site.githubUrl`. Rename inline.js → inline.njk
   so nunjucks evaluates it, add `githubApiUrl` to site.json, and template
   the fetch URL from it. One place to update if the repo ever moves.

2. header.njk aria-label said "Remark42's GitHub Repository" but the link
   target is `/releases`. Change to "{{ site.name }} releases on GitHub"
   so screen readers describe the actual destination.

3. console.warn on fetch failure (kept after umputun's prior review noted
   the trade-off): addressed in the PR description, no code change.

* site: actually template fetch URL via site.githubApiUrl

Copilot's second pass caught that 7697dcf3 added site.githubApiUrl,
renamed inline.js → inline.njk, and pointed head.njk at the .njk file
— but the fetch() call itself was never changed to use the template
variable. Build output looked correct because the literal hard-coded
URL happened to match what {{ site.githubApiUrl }} would expand to.

* site: normalise Nunjucks spacing in header.njk

{{ site.githubUrl}}/releases → {{ site.githubUrl }}/releases. Cosmetic
only; matches the spacing used everywhere else in the templates.
2026-05-28 13:10:35 -05:00

66 lines
2.1 KiB
Plaintext

const mq = window.matchMedia('(prefers-color-scheme: dark)')
const theme = localStorage.getItem('theme')
if ((theme && theme === 'dark') || (!theme && mq.matches)) {
document.documentElement.classList.add('dark')
}
// fetch the latest release version client-side so the badge tracks releases
// without needing a site rebuild. cached in sessionStorage for up to 1h so
// repeated navigations within a tab don't re-hit the GitHub API on every page
// load. placeholder is `hidden` in the template so a failed/blocked fetch
// leaves no stray gap.
const versionCacheKey = 'remark42-latest-version'
const versionCacheTTL = 60 * 60 * 1000
// script is loaded synchronously in <head>, so a cache-hit fetch can resolve
// before <body> is parsed and [data-remark42-version] exists. defer the DOM
// update until the document is ready.
function applyVersion(tag) {
const update = () => {
document.querySelectorAll('[data-remark42-version]').forEach((el) => {
el.textContent = tag
el.hidden = false
})
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', update)
} else {
update()
}
}
function readVersionCache() {
try {
const raw = sessionStorage.getItem(versionCacheKey)
if (!raw) return null
const { tag, expires } = JSON.parse(raw)
if (!tag || typeof expires !== 'number' || Date.now() > expires) return null
return tag
} catch (e) {
return null
}
}
function writeVersionCache(tag) {
try {
sessionStorage.setItem(versionCacheKey, JSON.stringify({ tag, expires: Date.now() + versionCacheTTL }))
} catch (e) {
// sessionStorage unavailable or full — fall through, fetch will run next load
}
}
const cachedVersion = readVersionCache()
if (cachedVersion) {
applyVersion(cachedVersion)
} else {
fetch('{{ site.githubApiUrl }}/releases/latest')
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status))))
.then((d) => {
if (!d || !d.tag_name) return
writeVersionCache(d.tag_name)
applyVersion(d.tag_name)
})
.catch((err) => console.warn('remark42-site: latest version fetch failed', err))
}