From 526756b48cca9101eb5c2cef65760326371540c4 Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:02:44 +0800 Subject: [PATCH] Changelog: show the notes published with each release on GitHub A new release_notes content collection (src/lib/release-notes.ts) reads the notes of every release at build time and renders them with the site's markdown settings, like the Core documents. - English: the release's notes without the download table, GitHub's list of merged pull requests and Enterprise's image and Helm preamble. - Chinese: ThinkWatch Lite's annotated tags hold the notes in Chinese (the text of its update window), read through the GraphQL API. Their short lines followed by items become headings, and the upgrade notes are set off. The first Lite tags were annotated in English; with no notes on the release, that text is the English note. - Core and Enterprise have English notes only; on the Chinese page those releases carry an EN badge. - Heading ids are prefixed per release, since "upgrade-notes" and the like would otherwise repeat on one page. On the page a release with notes opens in place; a link to a release (the latest tiles, a shared anchor, the old #v0.4.0 anchors) opens it. The written-up Enterprise releases keep their title and highlights in the same place. Core v0.46.0 and earlier have only GitHub's generated pull-request list, so they keep linking to GitHub. When GitHub cannot be reached the build warns and the releases link to GitHub as before. Co-Authored-By: Claude Opus 5.5 --- src/components/pages/ChangelogPage.astro | 176 +++++++++++++--------- src/content.config.ts | 15 +- src/i18n/pages/changelog.ts | 6 +- src/lib/changelog.ts | 15 +- src/lib/release-notes.ts | 177 +++++++++++++++++++++++ 5 files changed, 316 insertions(+), 73 deletions(-) create mode 100644 src/lib/release-notes.ts diff --git a/src/components/pages/ChangelogPage.astro b/src/components/pages/ChangelogPage.astro index 03f68a8..0aee89b 100644 --- a/src/components/pages/ChangelogPage.astro +++ b/src/components/pages/ChangelogPage.astro @@ -33,9 +33,11 @@ const products = ["lite", "enterprise", "core"] as const; const count = (id: string) => (id === "all" ? entries.length : entries.filter((e) => e.product === id).length); const filters = [{ id: "all", label: c.all }, ...products.map((id) => ({ id, label: c.filters[id] }))]; +// The latest release of each product; its tile leads to its notes on this page when it has any. const latest = products .map((id) => entries.find((e) => e.product === id)) - .filter((e): e is ChangelogEntry => Boolean(e)); + .filter((e): e is ChangelogEntry => Boolean(e)) + .map((e) => ({ ...e, onPage: Boolean(e.notes || e.releaseNotes) || !e.url })); // Newest month first; releases keep their newest-first order inside it. const months: { key: string; label: string; items: ChangelogEntry[] }[] = []; @@ -62,12 +64,12 @@ for (const e of entries) {

{c.latest}

{latest.map((e) => ( - + {releaseProductName(e.product, lang)} {e.tag} - {e.url && {c.github} ↗} + {e.onPage ? `${c.readNotes} ↓` : `${c.github} ↗`} ))} @@ -92,38 +94,45 @@ for (const e of entries) { {m.items.map(async (entry) => { const name = releaseProductName(entry.product, lang); const notes = entry.notes; - const Content = notes ? (await render(notes)).Content : null; + const published = notes ? undefined : entry.releaseNotes; + const source = notes ?? published; + const Content = source ? (await render(source)).Content : null; + // Core and Enterprise publish their notes in English only + const english = lang === "zh-CN" && published?.data.lang === "en"; + // A release with notes opens in place to show them + const Wrap = Content ? "details" : "div"; + const Row = Content ? "summary" : "div"; return (
  • {/* The anchor the release had while the page listed Enterprise alone (#v0.4.0). */} {entry.legacyId && } -
    - - {name} - {entry.tag} - {entry.url ? ( - - {c.github} ↗ - - ) : } -
    - - {notes && ( -
    -

    {notes.data.title}

    - {notes.data.highlights && ( -
      - {notes.data.highlights.map((h) =>
    • {h}
    • )} -
    - )} - {Content && ( -
    - {c.readNotes} -
    -
    - )} -
    - )} + + + + {name} + + {entry.tag} + {english && EN} + + {entry.url ? ( + + {c.github} ↗ + + ) : } + + + {Content && ( +
    + {notes &&

    {notes.data.title}

    } + {notes?.data.highlights && ( +
      + {notes.data.highlights.map((h) =>
    • {h}
    • )} +
    + )} +
    +
    + )} +
  • ); })} @@ -319,9 +328,16 @@ for (const e of entries) { --prod: 168px; --gap: 20px; } + .cl-wrap > summary { + list-style: none; + cursor: pointer; + } + .cl-wrap > summary::-webkit-details-marker { + display: none; + } .cl-row { display: grid; - grid-template-columns: var(--date) var(--prod) minmax(0, 1fr) 72px; + grid-template-columns: var(--date) var(--prod) minmax(0, 1fr) 72px 14px; align-items: center; gap: 4px var(--gap); min-height: 44px; @@ -331,9 +347,44 @@ for (const e of entries) { transition: background 150ms; } .cl-row:hover, - .changelog-item:target .cl-row { + .changelog-item:target .cl-row, + details[open] > .cl-row { background: rgba(255, 255, 255, 0.03); } + .cl-tagcell { + display: flex; + min-width: 0; + align-items: center; + gap: 10px; + } + .cl-en { + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 4px; + padding: 1px 5px; + font-family: var(--font-mono); + font-size: 9px; + letter-spacing: 0.06em; + color: var(--color-dim); + } + /* A chevron on the releases that open to show their notes */ + .cl-chev { + width: 7px; + height: 7px; + justify-self: center; + border-right: 1.5px solid var(--color-dim); + border-bottom: 1.5px solid var(--color-dim); + transform: translateY(-2px) rotate(45deg); + transition: transform 200ms, border-color 150ms; + } + .cl-row:hover .cl-chev { + border-color: var(--color-text); + } + details[open] > .cl-row .cl-chev { + transform: translateY(2px) rotate(225deg); + } + .cl-chev-none { + visibility: hidden; + } .changelog-item:target .cl-row { box-shadow: inset 2px 0 0 rgb(var(--a1)); } @@ -367,9 +418,13 @@ for (const e of entries) { } @media (max-width: 639px) { .cl-row { - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: minmax(0, 1fr) auto 14px; padding-block: 10px; } + .cl-chev { + grid-column: 3; + grid-row: 1 / 3; + } .cl-prod { grid-column: 1; grid-row: 1; @@ -379,7 +434,7 @@ for (const e of entries) { grid-row: 1; justify-self: end; } - .cl-tag { + .cl-tagcell { grid-column: 1; grid-row: 2; } @@ -390,7 +445,7 @@ for (const e of entries) { } .cl-notes { - padding: 8px 0 28px; + padding: 12px 0 32px; } /* the notes line up under the version */ @media (min-width: 640px) { @@ -406,7 +461,7 @@ for (const e of entries) { color: #fff; } .cl-highlights { - margin: 14px 0 0; + margin: 14px 0 20px; padding: 0; list-style: none; display: flex; @@ -429,35 +484,6 @@ for (const e of entries) { border-radius: 99px; background: linear-gradient(135deg, rgb(var(--a1)), rgb(var(--a2))); } - .cl-more { - margin-top: 16px; - } - .cl-more summary { - display: inline-flex; - min-height: 40px; - align-items: center; - gap: 6px; - cursor: pointer; - list-style: none; - font-size: 14px; - font-weight: 500; - color: var(--color-text); - transition: color 150ms; - } - .cl-more summary::-webkit-details-marker { - display: none; - } - .cl-more summary::after { - content: "↓"; - color: var(--color-dim); - transition: transform 150ms; - } - .cl-more[open] summary::after { - transform: rotate(180deg); - } - .cl-more summary:hover { - color: #fff; - } .cl-prose { margin-top: 12px; font-size: 0.9rem; @@ -466,7 +492,11 @@ for (const e of entries) { margin-top: 1.8em; font-size: 1.1rem; } - .cl-prose :global(h2:first-child) { + .cl-prose :global(h3) { + margin-top: 1.6em; + font-size: 1rem; + } + .cl-prose :global(:is(h2, h3, p):first-child) { margin-top: 0; } /* Hide the # anchor link appended by rehypeAutolinkHeadings */ @@ -497,6 +527,18 @@ for (const e of entries) {