fix(web): link release notes to GitHub (#2114)

* fix(web): link release notes to GitHub

* test(web): verify release link safety contract
This commit is contained in:
JATMN
2026-08-12 08:26:03 +08:00
committed by GitHub
parent 6e30b40de0
commit c40d663b70
14 changed files with 89 additions and 451 deletions
+3
View File
@@ -4,5 +4,8 @@ import sitemap from '@astrojs/sitemap'
export default defineConfig({
site: 'https://openclaude.gitlawb.com',
trailingSlash: 'always',
redirects: {
'/changelog/': 'https://github.com/Gitlawb/openclaude/releases',
},
integrations: [sitemap()],
})
+50 -92
View File
@@ -1,12 +1,11 @@
import { describe, expect, test } from 'bun:test'
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { verifyDist, npmFreshnessFailure } from './verify-dist'
import { verifyDist } from './verify-dist'
import { SITE } from '../src/data/site'
import { docsPages } from '../src/data/docsNav'
import { releases, releaseUrl } from '../src/data/releases'
import { heroes } from '../src/data/buddy'
import { partners, community } from '../src/data/partners'
@@ -18,23 +17,17 @@ function writePage(dist: string, route: string, html: string): void {
/** Build a minimal dist/ that satisfies every verifyDist assertion. */
function writeValidFixture(dist: string): void {
const navLinks = ['/buddy/', '/changelog/'].map(h => `<a href="${h}">x</a>`).join('')
const navLinks = [`<a href="/buddy/">x</a>`, `<a href="${SITE.releasesUrl}" target="_blank" rel="noopener">x</a>`].join('')
writePage(
dist,
'/',
`<html>v${SITE.version}${navLinks}${partners
`<html>${navLinks}${partners
.map(p => `<a href="${p.url}"><img src="${p.logo}"></a>`)
.join('')}${community.map(c => `<a href="${c.url}">x</a>`).join('')}</html>`,
)
const sidebar = docsPages.map(p => `<a href="${p.href}">x</a>`).join('')
const sidebar = `${docsPages.map(p => `<a href="${p.href}">x</a>`).join('')}<a href="${SITE.releasesUrl}" target="_blank" rel="noopener">x</a>`
for (const p of docsPages) writePage(dist, p.href, `<html>${sidebar}</html>`)
writePage(
dist,
'/changelog/',
`<html>v${SITE.version}${releases
.map(r => `<a href="${releaseUrl(r.version)}">v${r.version}</a>`)
.join('')}</html>`,
)
writePage(dist, '/changelog/', `<html><meta http-equiv="refresh" content="0;url=${SITE.releasesUrl}"><meta name="robots" content="noindex">${SITE.releasesUrl}</html>`)
writePage(
dist,
'/buddy/',
@@ -44,7 +37,7 @@ function writeValidFixture(dist: string): void {
for (const h of heroes) writeFileSync(join(dist, 'buddy', `${h.id}.svg`), '<svg/>')
writeFileSync(
join(dist, 'sitemap-0.xml'),
`<urlset>${['/buddy/', '/changelog/'].map(r => `<loc>${SITE.url}${r}</loc>`).join('')}</urlset>`,
`<urlset><loc>${SITE.url}/buddy/</loc></urlset>`,
)
}
@@ -59,35 +52,6 @@ function withFixture(mutate: (dist: string) => void): string[] {
}
}
describe('release data', () => {
test('keeps releases newest first', () => {
const versions = releases.map(release => release.version.split('.').map(Number))
const sorted = [...versions].sort((a, b) => {
for (let i = 0; i < 3; i++) {
if (a[i] !== b[i]) return (b[i] ?? 0) - (a[i] ?? 0)
}
return 0
})
expect(versions).toEqual(sorted)
})
test('lists the 0.27.0 release with its curated highlights', () => {
expect(releases.find(release => release.version === '0.27.0')).toEqual({
version: '0.27.0',
date: '2026-07-30',
theme: 'auth-ready local proxies and a refreshed web identity',
highlights: [
'opt-in loopback proxy hosts preserve subscription OAuth authentication',
'new Ling 3.0 Flash and Macaron V1 Tall catalog entries',
'centered startup logo and updated Ember Block O web branding',
'agents can spawn subagents from multi-repository parent sessions',
'more reliable tool-failure guard, SDK permission-timeout reporting, stats, and status UI',
],
})
})
})
describe('verifyDist', () => {
test('passes on a complete fixture', () => {
expect(withFixture(() => {})).toEqual([])
@@ -107,13 +71,50 @@ describe('verifyDist', () => {
test('flags a docs sidebar that lost a navigation link', () => {
const failures = withFixture(dist => {
const sidebar = docsPages
.filter(p => p.href !== '/changelog/')
.map(p => `<a href="${p.href}">x</a>`)
.join('')
const sidebar = docsPages.map(p => `<a href="${p.href}">x</a>`).join('')
writePage(dist, '/docs/', `<html>${sidebar}</html>`)
})
expect(failures).toContain('docs sidebar link /changelog/: missing "href=\\"/changelog/\\""')
expect(failures).toContain('docs release notes link: missing safe new-tab release link')
})
test('flags a landing release link that lost target=_blank', () => {
const failures = withFixture(dist => {
const index = join(dist, 'index.html')
writeFileSync(index, readFileSync(index, 'utf8').replace(' target="_blank" rel="noopener"', ' rel="noopener"'))
})
expect(failures).toContain('landing release notes link: missing safe new-tab release link')
})
test('flags a docs release link that lost rel=noopener', () => {
const failures = withFixture(dist => {
const docs = join(dist, 'docs', 'index.html')
writeFileSync(docs, readFileSync(docs, 'utf8').replace(' target="_blank" rel="noopener"', ' target="_blank"'))
})
expect(failures).toContain('docs release notes link: missing safe new-tab release link')
})
test('flags an unsafe release link even when another release link is safe', () => {
const failures = withFixture(dist => {
const index = join(dist, 'index.html')
writeFileSync(index, `${readFileSync(index, 'utf8')}<a href="${SITE.releasesUrl}" target="_blank">x</a>`)
})
expect(failures).toContain('landing release notes link: missing safe new-tab release link')
})
test('flags a legacy changelog page that lost its redirect', () => {
const failures = withFixture(dist => {
const changelog = join(dist, 'changelog', 'index.html')
writeFileSync(changelog, readFileSync(changelog, 'utf8').replace(`<meta http-equiv="refresh" content="0;url=${SITE.releasesUrl}">`, ''))
})
expect(failures).toContain(`legacy changelog redirect: missing "<meta http-equiv=\\"refresh\\" content=\\"0;url=${SITE.releasesUrl}\\">"`)
})
test('flags a legacy changelog redirect that lost noindex', () => {
const failures = withFixture(dist => {
const changelog = join(dist, 'changelog', 'index.html')
writeFileSync(changelog, readFileSync(changelog, 'utf8').replace('<meta name="robots" content="noindex">', ''))
})
expect(failures).toContain('legacy changelog noindex: missing "<meta name=\\"robots\\" content=\\"noindex\\">"')
})
test('flags a missing sprite asset', () => {
@@ -124,7 +125,7 @@ describe('verifyDist', () => {
test('flags a stale landing page missing a partner link', () => {
const failures = withFixture(dist => {
const html = `<html>v${SITE.version}<a href="/buddy/">x</a><a href="/changelog/">x</a>${community
const html = `<html><a href="/buddy/">x</a><a href="${SITE.releasesUrl}" target="_blank">x</a>${community
.map(c => `<a href="${c.url}">x</a>`)
.join('')}</html>`
writeFileSync(join(dist, 'index.html'), html)
@@ -137,7 +138,6 @@ describe('verifyDist', () => {
writeFileSync(join(dist, 'sitemap-0.xml'), `<urlset><loc>${SITE.url}/</loc></urlset>`),
)
expect(failures.some(f => f.startsWith('sitemap entry /buddy/'))).toBe(true)
expect(failures.some(f => f.startsWith('sitemap entry /changelog/'))).toBe(true)
})
test('flags a missing sitemap', () => {
@@ -145,46 +145,4 @@ describe('verifyDist', () => {
expect(failures).toContain('missing dist/sitemap-0.xml')
})
test('flags a changelog entry that lost its release URL', () => {
const failures = withFixture(dist => {
const html = `<html>v${SITE.version}${releases.map(r => `v${r.version}`).join(' ')}</html>`
writeFileSync(join(dist, 'changelog', 'index.html'), html)
})
expect(failures.some(f => f.startsWith('changelog release URL'))).toBe(true)
})
})
describe('npmFreshnessFailure', () => {
function fetchReturning(body: unknown, ok = true): typeof fetch {
return (() =>
Promise.resolve({ ok, json: () => Promise.resolve(body) } as Response)) as typeof fetch
}
test('passes when npm matches the site version', async () => {
expect(await npmFreshnessFailure(fetchReturning({ version: SITE.version }))).toBeNull()
})
test('passes when the site is ahead of npm (release PR before publish)', async () => {
expect(await npmFreshnessFailure(fetchReturning({ version: '0.1.0' }))).toBeNull()
})
test('fails when npm has a newer release than releases.ts', async () => {
const failure = await npmFreshnessFailure(fetchReturning({ version: '999.0.0' }))
expect(failure).toContain('999.0.0')
expect(failure).toContain('web/src/data/releases.ts')
expect(failure).toContain('do not patch it from unrelated PRs')
})
test('skips on network failure instead of breaking the build', async () => {
const offline = (() => Promise.reject(new Error('offline'))) as typeof fetch
expect(await npmFreshnessFailure(offline)).toBeNull()
})
test('skips on a malformed registry response', async () => {
expect(await npmFreshnessFailure(fetchReturning({}))).toBeNull()
expect(await npmFreshnessFailure(fetchReturning({ version: 'not-semver' }))).toBeNull()
expect(await npmFreshnessFailure(fetchReturning({ version: '01.2.3' }))).toBeNull()
expect(await npmFreshnessFailure(fetchReturning({ version: '999.00.0' }))).toBeNull()
expect(await npmFreshnessFailure(fetchReturning({}, false))).toBeNull()
})
})
+18 -50
View File
@@ -1,13 +1,12 @@
// Post-build guard, run by `bun run build` after `astro build`.
// Asserts that the typed data files actually drive the rendered output:
// version propagation, navigation, and the /, /buddy/, /changelog/ routes.
// navigation and the / and /buddy/ routes.
// Kept as a pure function so verify-dist.test.ts can exercise it on fixtures.
import { readFileSync, existsSync } from 'node:fs'
import { join } from 'node:path'
import { SITE } from '../src/data/site'
import { docsPages } from '../src/data/docsNav'
import { releases, releaseUrl } from '../src/data/releases'
import { heroes } from '../src/data/buddy'
import { partners, community } from '../src/data/partners'
@@ -34,9 +33,13 @@ export function verifyDist(dist: string): string[] {
if (html !== '' && !html.includes(needle)) failures.push(`${why}: missing ${JSON.stringify(needle)}`)
}
// ── version propagation (site.ts derives from the newest releases entry) ─
function expectReleaseLinks(html: string, why: string): void {
const releaseLinks = (html.match(/<a\b[^>]*>/g) ?? []).filter(link => link.includes(`href="${SITE.releasesUrl}"`))
if (html !== '' && (releaseLinks.length === 0 || releaseLinks.some(link => !link.includes('target="_blank"') || !link.includes('rel="noopener"'))))
failures.push(`${why}: missing safe new-tab release link`)
}
const index = page('/')
expect(index, `v${SITE.version}`, 'landing version')
// ── navigation exposes every docsNav route, in data AND rendered output ──
const docsIndex = page('/docs/')
@@ -44,18 +47,16 @@ export function verifyDist(dist: string): string[] {
page(p.href) // records a failure if the route didn't build
expect(docsIndex, `href="${p.href}"`, `docs sidebar link ${p.href}`)
}
for (const href of ['/buddy/', '/changelog/'] as const) {
if (!docsPages.some(p => p.href === href)) failures.push(`docsNav missing ${href}`)
expect(index, `href="${href}"`, `landing nav link ${href}`)
}
if (!docsPages.some(p => p.href === '/buddy/')) failures.push('docsNav missing /buddy/')
expect(index, 'href="/buddy/"', 'landing nav link /buddy/')
expectReleaseLinks(index, 'landing release notes link')
expectReleaseLinks(docsIndex, 'docs release notes link')
// ── /changelog/: every release renders with its GitHub release URL ───────
const changelog = page('/changelog/')
for (const r of releases) {
expect(changelog, `v${r.version}`, `changelog release ${r.version}`)
expect(changelog, releaseUrl(r.version), `changelog release URL ${r.version}`)
}
expect(changelog, `v${SITE.version}`, 'changelog current-version pill')
// ── legacy route continues on the canonical GitHub Releases page ─────────
const legacyChangelog = page('/changelog/')
expect(legacyChangelog, SITE.releasesUrl, 'legacy changelog redirect')
expect(legacyChangelog, `<meta http-equiv="refresh" content="0;url=${SITE.releasesUrl}">`, 'legacy changelog redirect')
expect(legacyChangelog, '<meta name="robots" content="noindex">', 'legacy changelog noindex')
// ── /buddy/: every hero renders with its sprite ──────────────────────────
const buddy = page('/buddy/')
@@ -77,7 +78,7 @@ export function verifyDist(dist: string): string[] {
const sitemapFile = join(dist, 'sitemap-0.xml')
if (existsSync(sitemapFile)) {
const sitemap = readFileSync(sitemapFile, 'utf8')
for (const route of ['/buddy/', '/changelog/'])
for (const route of ['/buddy/'])
expect(sitemap, `${SITE.url}${route}`, `sitemap entry ${route}`)
} else {
failures.push('missing dist/sitemap-0.xml')
@@ -86,45 +87,12 @@ export function verifyDist(dist: string): string[] {
return [...new Set(failures)]
}
function newerThan(a: string, b: string): boolean {
const pa = a.split('.').map(Number)
const pb = b.split('.').map(Number)
for (let i = 0; i < 3; i++) {
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0)
}
return false
}
// With web/ standalone, releases.ts is the only version source — this is the
// guard against it silently going stale. Best-effort by design: an unreachable
// registry (offline CI, npm outage) skips the check rather than failing the
// build; only a *confirmed newer* npm release fails. A site version ahead of
// npm is allowed so a release PR can land before the publish completes.
export async function npmFreshnessFailure(fetchImpl: typeof fetch = fetch): Promise<string | null> {
let published: string
try {
const res = await fetchImpl('https://registry.npmjs.org/@gitlawb/openclaude/latest', {
signal: AbortSignal.timeout(5000),
})
if (!res.ok) return null
published = ((await res.json()) as { version?: string }).version ?? ''
} catch {
return null
}
if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(published)) return null
if (newerThan(published, SITE.version))
return `npm latest ${published} is newer than site version ${SITE.version} — releases.ts is stale; do not patch it from unrelated PRs (release/web process owns web/src/data/releases.ts)`
return null
}
if (import.meta.main) {
const failures = verifyDist(join(import.meta.dir, '..', 'dist'))
const stale = await npmFreshnessFailure()
if (stale) failures.push(stale)
if (failures.length > 0) {
console.error(`verify-dist: ${failures.length} failure(s)`)
for (const f of failures) console.error(`${f}`)
process.exit(1)
}
console.log('verify-dist: ok — version, nav, buddy, changelog, partners all verified')
console.log('verify-dist: ok — navigation, buddy, and partners verified')
}
+1 -1
View File
@@ -12,7 +12,7 @@ const path = Astro.url.pathname
<ul>
{group.items.map(item => (
<li>
<a href={item.href} aria-current={path === item.href ? 'page' : undefined}>
<a href={item.href} aria-current={path === item.href ? 'page' : undefined} rel={item.newTab ? 'noopener' : undefined} target={item.newTab ? '_blank' : undefined}>
{item.title}
</a>
</li>
+2 -3
View File
@@ -24,7 +24,7 @@ const columns = [
{
title: 'project',
links: [
{ href: '/changelog/', label: "what's new" },
{ href: SITE.releasesUrl, label: "what's new", newTab: true },
{ href: '/buddy/', label: 'buddy' },
{ href: SITE.github, label: 'github' },
{ href: SITE.npmUrl, label: 'npm' },
@@ -54,7 +54,7 @@ const columns = [
<ul>
{col.links.map(l => (
<li>
<a href={l.href} rel={l.href.startsWith('http') ? 'noopener' : undefined}>{l.label}</a>
<a href={l.href} rel={l.href.startsWith('http') ? 'noopener' : undefined} target={l.newTab ? '_blank' : undefined}>{l.label}</a>
</li>
))}
</ul>
@@ -65,7 +65,6 @@ const columns = [
<span class="brand">
<img src="/openclaude.png" alt="" width="18" height="18" />
<span>openclaude</span>
<span class="ver">v{SITE.version}</span>
</span>
<span class="sep">·</span>
<span>open source coding agent, aligned with <a href={SITE.gitlawb} rel="noopener">gitlawb</a></span>
+3 -3
View File
@@ -5,7 +5,7 @@ const links = [
{ href: '/docs/', label: 'docs' },
{ href: '/docs/providers/', label: 'providers' },
{ href: '/buddy/', label: 'buddy' },
{ href: '/changelog/', label: "what's new" },
{ href: SITE.releasesUrl, label: "what's new", newTab: true },
{ href: SITE.github, label: 'github' },
{ href: SITE.gitlawb, label: 'gitlawb' },
]
@@ -18,7 +18,6 @@ const path = Astro.url.pathname
<a class="brand" href="/" aria-label="openclaude home">
<img src="/openclaude.png" alt="" width="22" height="22" />
<span>openclaude</span>
<span class="ver">v{SITE.version}</span>
</a>
<nav class="nav-links" aria-label="primary">
{links.map(l => (
@@ -26,6 +25,7 @@ const path = Astro.url.pathname
href={l.href}
aria-current={path === l.href ? 'page' : undefined}
rel={l.href.startsWith('http') ? 'noopener' : undefined}
target={l.newTab ? '_blank' : undefined}
>{l.label}</a>
))}
<button type="button" class="theme-toggle" data-theme-toggle aria-live="polite">[light]</button>
@@ -36,7 +36,7 @@ const path = Astro.url.pathname
</div>
<div class="mobile-menu" id="mobile-menu">
{links.map(l => (
<a href={l.href} rel={l.href.startsWith('http') ? 'noopener' : undefined}>{l.label}</a>
<a href={l.href} rel={l.href.startsWith('http') ? 'noopener' : undefined} target={l.newTab ? '_blank' : undefined}>{l.label}</a>
))}
<a href="#" data-theme-toggle role="button">[light]</a>
</div>
+5 -2
View File
@@ -1,6 +1,9 @@
import { SITE } from './site'
export interface DocsNavItem {
title: string
href: string
newTab?: boolean
}
export interface DocsNavGroup {
@@ -31,13 +34,13 @@ export const docsNav: DocsNavGroup[] = [
{
group: 'more',
items: [
{ title: "What's new", href: '/changelog/' },
{ title: "What's new", href: SITE.releasesUrl, newTab: true },
{ title: 'Buddy', href: '/buddy/' },
],
},
]
export const docsPages: DocsNavItem[] = docsNav.flatMap(g => g.items)
export const docsPages: DocsNavItem[] = docsNav.flatMap(g => g.items).filter(item => !item.newTab)
export function pagerFor(href: string): { prev?: DocsNavItem; next?: DocsNavItem } {
const i = docsPages.findIndex(p => p.href === href)
-145
View File
@@ -1,145 +0,0 @@
// Curated release highlights for the /changelog page. Full notes live on
// GitHub releases; this list is the hand-picked "what actually matters"
// per minor version. Newest first.
//
// Ownership: release/web process only (release automation and dedicated web
// release PRs). Do not edit this file from ordinary feature or bugfix PRs,
// including to silence web CI when npm is ahead of the site version.
export interface Release {
version: string
date: string
/** one-line theme of the release */
theme: string
highlights: string[]
}
export const RELEASES_URL = 'https://github.com/Gitlawb/openclaude/releases'
export function releaseUrl(version: string): string {
return `${RELEASES_URL}/tag/v${version}`
}
export const releases: Release[] = [
{
version: '0.27.0',
date: '2026-07-30',
theme: 'auth-ready local proxies and a refreshed web identity',
highlights: [
'opt-in loopback proxy hosts preserve subscription OAuth authentication',
'new Ling 3.0 Flash and Macaron V1 Tall catalog entries',
'centered startup logo and updated Ember Block O web branding',
'agents can spawn subagents from multi-repository parent sessions',
'more reliable tool-failure guard, SDK permission-timeout reporting, stats, and status UI',
],
},
{
version: '0.26.0',
date: '2026-07-27',
theme: 'polish — steadier long turns, sharper streaming feedback',
highlights: [
'long-running tools stay active instead of tripping the query guard',
'streaming token counts appear immediately, not after the first chunk',
'AI/ML API client hardening: passwordless methods and response-shape guards',
'windows: tolerate EPERM from mkdir on drive roots',
'slash-command arguments insert literally — no more accidental regex references',
],
},
{
version: '0.25.0',
date: '2026-07-20',
theme: 'buddy companions, GPT-5.6, and a leaner install',
highlights: [
'buddy: pixel-art hero companions with signature Enter animations — /buddy to hatch one',
'GPT-5.6 family models on Codex, routed through the OpenAI Responses API',
'first-run onboarding experience for third-party providers',
'token optimization: universal tool compression, doom-loop detection, configurable compaction',
'zero-warning npm install, enforced by a release gate and daily 3-OS checks',
'context bar shows live token counts — ctx 74K/200K (37%)',
'new providers: LongCat, AI/ML API foundation, Kimi K3 context variants',
],
},
{
version: '0.24.0',
date: '2026-07-14',
theme: 'effort control and per-model tuning',
highlights: [
'ultrathink keyword detection and the ultracode effort level',
'Cloudflare Workers AI provider integration',
'per-model context_window and max_output_tokens overrides in settings',
'Codex OAuth: manual callback URL paste for SSH and remote sessions',
'/doctor gains WebSearch backend diagnostics',
'/model surfaces inactive provider profiles',
],
},
{
version: '0.23.0',
date: '2026-07-07',
theme: 'codebase intelligence and skills everywhere',
highlights: [
'repo map codebase intelligence',
'smart auto-routing: per-turn simple-vs-strong model selection',
'AI/ML API joins as a first-class provider',
'local skill CLI support plus a native-TypeScript PDF generation skill',
'ship a zero-warning, minimal install',
'openclaude config fully isolated from Claude Code',
],
},
{
version: '0.22.0',
date: '2026-07-06',
theme: 'honest feedback and LSP visibility',
highlights: [
'LSP diagnostics captured and exposed to the agent',
'task reports render as markdown',
'honest-feedback UX pass: visible retries, statusline truncation marker, hint grace period',
'resume picker groups branched sessions',
],
},
{
version: '0.21.0',
date: '2026-06-30',
theme: 'session branching and effort routing',
highlights: [
'/branch forks a conversation into a new session',
'/set-context-window and /clear-context-window commands',
'model-level reasoning effort routing',
'per-agent step limits and deterministic session task reports',
'Claude Opus 4.8 support; ClinePass gateway provider',
'recover GLM/Qwen tool calls emitted as XML text',
'perf: minified CLI bundle and lazy provider catalog loading',
],
},
{
version: '0.20.0',
date: '2026-06-24',
theme: 'background sessions and the bughunter goes public',
highlights: [
'local background sessions — run agents while you keep typing',
'/bughunter public, with /bughunter-security and /bughunter-perf variants',
'per-agent model assignment from the /agents menu',
'/update command with package-manager auto-detection',
'auto-detect and persist project conventions to the wiki',
'OpenAI-compatible credential pool failover; OPENCLAUDE_CONFIG_DIR override',
'GLM 5.2 across Z.AI, Fireworks, Atlas Cloud, and Opengateway',
],
},
{
version: '0.19.0',
date: '2026-06-16',
theme: 'context visibility and new backends',
highlights: [
'/ctx context-window visualization and token bars in /cost',
'NEAR AI provider integration',
'native Gemini Vertex client and auth helpers',
'compactModel option — use a separate model for compaction',
'auto-compact prompt on /resume with a determinate progress bar',
'redacted diagnostic issue reports',
],
},
]
// web/ builds standalone (no access to the repo root), so the newest changelog
// entry doubles as the site's displayed version. verify-dist cross-checks it
// against the published npm version so this list can't silently go stale.
export const latestVersion = releases[0].version
+1 -3
View File
@@ -1,5 +1,3 @@
import { latestVersion } from './releases'
export const SITE = {
url: 'https://openclaude.gitlawb.com',
name: 'openclaude',
@@ -9,9 +7,9 @@ export const SITE = {
installCommand: 'npm install -g @gitlawb/openclaude@latest',
npmUrl: 'https://www.npmjs.com/package/@gitlawb/openclaude',
github: 'https://github.com/Gitlawb/openclaude',
releasesUrl: 'https://github.com/Gitlawb/openclaude/releases',
gitlawb: 'https://gitlawb.com',
gitlawbRepo: 'https://gitlawb.com/node/repos/z6MkqDnb/openclaude',
version: latestVersion,
ogDefault: '/og/default.png',
ogDocs: '/og/docs.png',
} as const
-144
View File
@@ -1,144 +0,0 @@
---
import BaseLayout from '../layouts/BaseLayout.astro'
import { SITE } from '../data/site'
import { releases, releaseUrl, RELEASES_URL } from '../data/releases'
const title = `what's new in openclaude — release highlights | openclaude`
const description = `Curated highlights from every openclaude release: buddy companions, GPT-5.6 support, new providers, background sessions, and more. Currently at v${SITE.version}.`
const jsonLd = [
{
'@context': 'https://schema.org',
'@type': 'WebPage',
name: `what's new in openclaude`,
url: `${SITE.url}/changelog/`,
description,
isPartOf: { '@type': 'WebSite', name: SITE.name, url: SITE.url },
},
{
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'openclaude', item: SITE.url },
{ '@type': 'ListItem', position: 2, name: 'changelog', item: `${SITE.url}/changelog/` },
],
},
]
---
<BaseLayout title={title} description={description} jsonLd={jsonLd}>
<section class="hero grid-bg" aria-labelledby="changelog-heading">
<div class="container hero-inner">
<p class="pill"><span class="dot" aria-hidden="true"></span>currently v{SITE.version}</p>
<h1 id="changelog-heading" class="text-hero">what's new.</h1>
<p class="hero-sub">
the highlights that matter from each release — hand-picked, not a commit dump.
full notes live on <a href={RELEASES_URL} rel="noopener">github releases</a>.
</p>
</div>
</section>
<section class="section" aria-label="release history">
<div class="container">
<ol class="timeline">
{releases.map(r => (
<li class="release">
<div class="release-meta">
<a class="release-ver" href={releaseUrl(r.version)} rel="noopener">v{r.version}</a>
<time datetime={r.date}>{r.date}</time>
</div>
<div class="release-body">
<h2>{r.theme}</h2>
<ul>
{r.highlights.map(h => <li>{h}</li>)}
</ul>
</div>
</li>
))}
</ol>
<p class="text-body-dim older">
looking for something older? the complete history is in the
<a href={`${SITE.github}/blob/main/CHANGELOG.md`} rel="noopener"> changelog on github</a>.
</p>
</div>
</section>
</BaseLayout>
<style>
.hero {
padding: calc(var(--nav-h) + clamp(3.5rem, 10vh, 6rem)) 0 clamp(3rem, 8vh, 5rem);
}
.hero-inner {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 1.4rem;
}
.hero-sub {
max-width: 34rem;
margin: 0;
color: var(--muted);
font-size: clamp(0.9rem, 1.4vw, 1rem);
line-height: 1.7;
}
.hero-sub a { color: var(--accent); }
.timeline {
margin: 0;
padding: 0;
list-style: none;
max-width: 46rem;
}
.release {
display: grid;
grid-template-columns: 7.5rem 1fr;
gap: 1.5rem;
padding: 1.75rem 0;
border-bottom: 1px solid var(--line);
}
.release:first-child { padding-top: 0; }
@media (max-width: 40rem) {
.release { grid-template-columns: 1fr; gap: 0.5rem; }
}
.release-meta {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.release-ver {
color: var(--accent);
font-size: 0.95rem;
font-weight: 600;
}
.release-meta time { color: var(--faint); font-size: 0.72rem; letter-spacing: 0.04em; }
.release-body h2 {
margin: 0 0 0.75rem;
color: var(--ink);
font-size: 0.95rem;
font-weight: 600;
line-height: 1.5;
}
.release-body ul {
margin: 0;
padding: 0;
list-style: none;
}
.release-body li {
position: relative;
padding: 0.25rem 0 0.25rem 1.1rem;
color: var(--muted);
font-size: 0.84rem;
line-height: 1.65;
}
.release-body li::before {
content: '●';
position: absolute;
left: 0;
color: var(--accent);
font-size: 0.5rem;
top: 0.72rem;
}
.older { margin-top: 2rem; font-size: 0.8rem; }
.older a { color: var(--accent); }
</style>
+1 -1
View File
@@ -38,7 +38,7 @@ const toc = [
{group.items
.filter(item => item.href !== '/docs/')
.map(item => (
<li><a href={item.href}>{item.title}</a></li>
<li><a href={item.href} rel={item.newTab ? 'noopener' : undefined} target={item.newTab ? '_blank' : undefined}>{item.title}</a></li>
))}
</ul>
</>
+1 -2
View File
@@ -57,7 +57,6 @@ const jsonLd = [
name: 'openclaude',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'macOS, Linux, Windows',
softwareVersion: SITE.version,
url: SITE.url,
downloadUrl: SITE.npmUrl,
description: SITE.description,
@@ -102,7 +101,7 @@ const jsonLd = [
</div>
<Terminal>
<span class="ln"><span class="t-prompt">$</span> openclaude</span>
<span class="ln t-dim">openclaude v{SITE.version} · provider: ollama · model: qwen3-coder</span>
<span class="ln t-dim">openclaude · provider: ollama · model: qwen3-coder</span>
<span class="ln">&nbsp;</span>
<span class="ln"><span class="t-prompt">&gt;</span> add retry with backoff to the fetch client</span>
<span class="ln">&nbsp;</span>