From 51c8c771c6bde24db7df1e6e62519694623ca568 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Fri, 25 Sep 2026 14:15:44 -0400 Subject: [PATCH] feat(config): backdate GitHub releases via custom fetch implementation --- apps/docs/blume.config.ts | 56 ++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/apps/docs/blume.config.ts b/apps/docs/blume.config.ts index 04f37c6..26eb312 100644 --- a/apps/docs/blume.config.ts +++ b/apps/docs/blume.config.ts @@ -1,7 +1,38 @@ import { defineConfig } from "blume"; -import { filesystem, githubReleases } from "blume/sources"; +import { custom, filesystem } from "blume/sources"; +import { githubReleasesSource } from "blume/sources/github-releases.ts"; import redirects from "./redirects"; +/** + * GitHub releases → /changelog, dated by the topic's real finish instant. + * + * Blume dates a release entry by `published_at`, which the GitHub API cannot + * backdate — so the topic releases backfilled on one day (apps/cli/ + * release-topics.ts) would all read that day. Their tags are timever + * (`YYYYMMDD.HHMMSS.NNNNNNNNN`, UTC) of the moment the topic finished, so this + * fetch rewrites each timever release's `published_at` to that instant before + * Blume maps it; anything else passes through untouched. The config-level + * `githubReleases()` accepts no `fetchImpl`, hence the `custom()` wrap of the + * underlying source (the `blume/sources/*.ts` path the Blume docs use for + * custom sources), with the context the built-in adapter would receive. + */ +const TIMEVER = /^(\d{4})(\d{2})(\d{2})\.(\d{2})(\d{2})(\d{2})\.\d{9}$/; + +const releasesDatedByTag: typeof fetch = async (input, init) => { + const res = await fetch(input, init); + if (!res.ok) return res; + const releases: unknown = await res.json(); + if (Array.isArray(releases)) { + for (const release of releases) { + const m = typeof release?.tag_name === "string" ? release.tag_name.match(TIMEVER) : null; + if (m) release.published_at = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`; + } + } + // Blume reads only `ok` and the JSON body, so the original headers (whose + // content-length/encoding describe the upstream bytes) are not carried over. + return Response.json(releases, { status: res.status }); +}; + // Blume's defaults (`inter`, `ibm-plex-mono`) are fetched from fonts.gstatic.com // on every build, which has timed out on the Pages runner and, fully offline, // silently ships no fonts at all. The same two families from Fontsource, as @@ -46,11 +77,24 @@ export default defineConfig({ content: { sources: [ filesystem({ root: "content" }), - githubReleases({ - prefix: "changelog", - owner: "prdlk", - repo: "leetcode", - }), + custom( + githubReleasesSource( + { + name: "changelog", + prefix: "changelog", + owner: "prdlk", + repo: "leetcode", + fetchImpl: releasesDatedByTag, + }, + // What resolve.ts hands the built-in adapter: `.blume/cache//`, + // and a refresh on every load (the source reads nothing else). + { + projectRoot: import.meta.dirname, + cacheDir: `${import.meta.dirname}/.blume/cache/changelog`, + mode: "build", + }, + ), + ), ], }, theme: {