Skip to content

Commit eae55a5

Browse files
committed
feat(changelog): publish curated product updates
1 parent b6861b3 commit eae55a5

65 files changed

Lines changed: 1520 additions & 466 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/PULL_REQUEST_TEMPLATE.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ Brief description of what this PR does and why.
33

44
Fixes #(issue)
55

6+
## Product update (if relevant)
7+
Who benefits, and what can they do now? Note rollout or plan restrictions, link the relevant Sim integration or docs guide, and include a real product demo or changelog production brief when available.
8+
69
## Type of Change
710
- [ ] Bug fix
811
- [ ] New feature

‎apps/sim/AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ For a common task, start from its skill (`.agents/skills/<name>/SKILL.md`):
2121

2222
<!-- BEGIN:nextjs-agent-rules -->
2323

24-
# This is NOT the Next.js you know
24+
## This is NOT the Next.js you know
2525

2626
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
2727

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Metadata } from 'next'
2+
import { notFound } from 'next/navigation'
3+
import { CHANGELOG_SECTION, getAllEntryMeta, getEntryBySlug } from '@/lib/changelog'
4+
import { buildPostGraphJsonLd, buildPostMetadata } from '@/lib/content/seo'
5+
import { ChangelogArticle, ChangelogLayout } from '@/app/(landing)/changelog/components'
6+
import { JsonLd } from '@/app/(landing)/components/json-ld'
7+
8+
export const dynamicParams = false
9+
export const revalidate = 3600
10+
11+
interface ChangelogEntryPageProps {
12+
params: Promise<{ slug: string }>
13+
}
14+
15+
export async function generateStaticParams() {
16+
return (await getAllEntryMeta()).map((entry) => ({ slug: entry.slug }))
17+
}
18+
19+
export async function generateMetadata({ params }: ChangelogEntryPageProps): Promise<Metadata> {
20+
const { slug } = await params
21+
const entry = await getEntryBySlug(slug)
22+
return entry ? buildPostMetadata(entry) : {}
23+
}
24+
25+
export default async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {
26+
const { slug } = await params
27+
const entry = await getEntryBySlug(slug)
28+
if (!entry) notFound()
29+
30+
return (
31+
<ChangelogLayout>
32+
<JsonLd data={buildPostGraphJsonLd(entry, CHANGELOG_SECTION)} />
33+
<ChangelogArticle entry={entry} />
34+
</ChangelogLayout>
35+
)
36+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { CHANGELOG_SECTION, getAllEntryMeta } from '@/lib/changelog'
2+
import { buildCollectionPageJsonLd } from '@/lib/content/seo'
3+
import { buildLandingMetadata } from '@/lib/landing/seo'
4+
import {
5+
ChangelogGrid,
6+
ChangelogHeader,
7+
ChangelogLayout,
8+
} from '@/app/(landing)/changelog/components'
9+
import { BackLink } from '@/app/(landing)/components/back-link'
10+
import { JsonLd } from '@/app/(landing)/components/json-ld'
11+
12+
export const revalidate = 3600
13+
export const metadata = buildLandingMetadata({
14+
title: 'Changelog Archive | Sim',
15+
description: 'Browse product updates from Sim and find the full technical release history.',
16+
path: '/changelog/archive',
17+
})
18+
19+
export default async function ChangelogArchivePage() {
20+
const entries = await getAllEntryMeta()
21+
return (
22+
<ChangelogLayout>
23+
<JsonLd
24+
data={buildCollectionPageJsonLd(
25+
{ ...CHANGELOG_SECTION, name: 'Changelog Archive', basePath: '/changelog/archive' },
26+
entries
27+
)}
28+
/>
29+
<ChangelogHeader
30+
title='All updates'
31+
lead='New capabilities, improvements, and fixes in Sim, with the details in one place.'
32+
actions={<BackLink href='/changelog' label='Back to changelog' />}
33+
/>
34+
<ChangelogGrid entries={entries} />
35+
<a
36+
href='https://github.com/simstudioai/sim/releases'
37+
target='_blank'
38+
rel='noopener noreferrer'
39+
className='text-[var(--text-secondary)] text-small underline underline-offset-4'
40+
>
41+
Earlier releases and full technical history on GitHub
42+
</a>
43+
</ChangelogLayout>
44+
)
45+
}
Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,39 @@
1-
import { createLogger } from '@sim/logger'
2-
import { getErrorMessage } from '@sim/utils/errors'
3-
import { ChangelogActions, ChangelogTimeline } from '@/app/(landing)/changelog/components'
4-
import type { ChangelogEntry, GitHubRelease } from '@/app/(landing)/changelog/types'
5-
import { mapReleases, releasesEndpoint } from '@/app/(landing)/changelog/utils'
6-
import { ProseHero, ProseShell } from '@/app/(landing)/components/prose-page'
7-
8-
const logger = createLogger('Changelog')
9-
10-
/**
11-
* Changelog page - reuses the shared prose primitives ({@link ProseShell} +
12-
* {@link ProseHero}) so its headline and column match Terms and Privacy, then
13-
* renders the GitHub-release timeline. The first page of releases is
14-
* fetched here on the server at build/revalidate time; the {@link ChangelogTimeline}
15-
* client leaf paginates the rest. Re-authored from the prior dark changelog onto
16-
* the platform light tokens.
17-
*/
18-
19-
const LEAD =
20-
'Every new feature, improvement, and fix in Sim, the open-source AI workspace, with release notes straight from GitHub.'
21-
22-
async function getInitialEntries(): Promise<ChangelogEntry[]> {
23-
try {
24-
// boundary-raw-fetch: external GitHub Releases API (cross-origin), not a same-origin contract
25-
const res = await fetch(releasesEndpoint(1), {
26-
headers: { Accept: 'application/vnd.github+json' },
27-
next: { revalidate: 3600 },
28-
})
29-
const releases = (await res.json()) as GitHubRelease[]
30-
return mapReleases(releases ?? [])
31-
} catch (error) {
32-
logger.warn('Failed to load initial changelog releases from GitHub', {
33-
error: getErrorMessage(error),
34-
})
35-
return []
36-
}
37-
}
1+
import Link from 'next/link'
2+
import { CHANGELOG_SECTION, getAllEntryMeta, LATEST_ENTRY_LIMIT } from '@/lib/changelog'
3+
import { buildCollectionPageJsonLd } from '@/lib/content/seo'
4+
import {
5+
ChangelogActions,
6+
ChangelogGrid,
7+
ChangelogHeader,
8+
ChangelogLayout,
9+
} from '@/app/(landing)/changelog/components'
10+
import { JsonLd } from '@/app/(landing)/components/json-ld'
3811

3912
export default async function Changelog() {
40-
const entries = await getInitialEntries()
41-
13+
const metadata = await getAllEntryMeta()
14+
const visible = metadata.slice(0, LATEST_ENTRY_LIMIT)
4215
return (
43-
<ProseShell>
44-
<ProseHero title='Changelog' lead={LEAD} actions={<ChangelogActions />} />
45-
<section id='releases' aria-label='Release history'>
46-
<ChangelogTimeline initialEntries={entries} />
47-
</section>
48-
</ProseShell>
16+
<ChangelogLayout>
17+
<JsonLd data={buildCollectionPageJsonLd(CHANGELOG_SECTION, visible)} />
18+
<ChangelogHeader
19+
title='Changelog'
20+
lead={CHANGELOG_SECTION.description}
21+
actions={<ChangelogActions />}
22+
/>
23+
<ChangelogGrid entries={visible} featureLatest />
24+
<div className='flex flex-wrap gap-6 border-[var(--border)] border-t pt-6 text-[var(--text-secondary)] text-sm'>
25+
<Link href='/changelog/archive' className='underline underline-offset-4'>
26+
All updates
27+
</Link>
28+
<a
29+
href='https://github.com/simstudioai/sim/releases'
30+
target='_blank'
31+
rel='noopener noreferrer'
32+
className='underline underline-offset-4'
33+
>
34+
Earlier releases on GitHub
35+
</a>
36+
</div>
37+
</ChangelogLayout>
4938
)
5039
}

‎apps/sim/app/(landing)/changelog/components/changelog-actions/changelog-actions.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,18 @@ import { GithubOutlineIcon } from '@/components/icons'
99
* beneath the changelog headline. A small client leaf because `ChipLink` is a
1010
* Client Component and its `leftIcon` is a component reference that cannot cross
1111
* the server→client boundary as a prop (same pattern as the platform pill CTA).
12-
* GitHub is the primary filled chip; Docs and RSS are the default pills.
1312
*/
1413
export function ChangelogActions() {
1514
return (
1615
<div className='flex flex-wrap items-center gap-1'>
1716
<ChipLink
18-
variant='primary'
1917
shape='round'
2018
href='https://github.com/simstudioai/sim/releases'
2119
target='_blank'
2220
rel='noopener noreferrer'
2321
leftIcon={GithubOutlineIcon}
2422
>
25-
View on GitHub
23+
Technical releases
2624
</ChipLink>
2725
<ChipLink
2826
shape='round'
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { cn } from '@sim/emcn'
2+
import type { ContentPost } from '@/lib/content/schema'
3+
import { BackLink } from '@/app/(landing)/components/back-link'
4+
import { formatPostDate } from '@/app/(landing)/components/content-utils'
5+
import { HOME_TYPE, LANDING_TYPE } from '@/app/(landing)/components/landing-layout'
6+
7+
interface ChangelogArticleProps {
8+
entry: ContentPost
9+
backHref?: string
10+
archiveHref?: string
11+
}
12+
13+
/** A focused reading column contains the update's product recordings and release details. */
14+
export function ChangelogArticle({
15+
entry,
16+
backHref = '/changelog',
17+
archiveHref = '/changelog/archive',
18+
}: ChangelogArticleProps) {
19+
const updated =
20+
entry.updated && entry.updated.slice(0, 10) !== entry.date.slice(0, 10)
21+
? entry.updated
22+
: undefined
23+
return (
24+
<article id={entry.slug} className='mx-auto w-full max-w-[900px]'>
25+
<header className='flex flex-col items-start gap-6'>
26+
<BackLink href={backHref} label='Back to changelog' />
27+
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 text-[var(--text-secondary)] text-sm'>
28+
{entry.draft ? <span>Draft</span> : null}
29+
<time dateTime={entry.date}>{formatPostDate(entry.date)}</time>
30+
{entry.release ? <span>{entry.release.versions.join(' · ')}</span> : null}
31+
{updated ? <time dateTime={entry.updated}>Updated {formatPostDate(updated)}</time> : null}
32+
</div>
33+
<h1 className={cn('text-balance text-[var(--text-primary)]', LANDING_TYPE.proof)}>
34+
{entry.title}
35+
</h1>
36+
<p className={cn('text-pretty text-[var(--text-secondary)]', LANDING_TYPE.lead)}>
37+
{entry.description}
38+
</p>
39+
</header>
40+
<section
41+
id='update-details'
42+
aria-labelledby='update-details-heading'
43+
className='mt-10 border-[var(--border)] border-t pt-4'
44+
>
45+
<h2 id='update-details-heading' className='sr-only'>
46+
In this update
47+
</h2>
48+
<entry.Content />
49+
</section>
50+
<footer
51+
className={cn(
52+
'mt-10 flex flex-wrap gap-6 border-[var(--border)] border-t pt-6 text-[var(--text-secondary)]',
53+
HOME_TYPE.meta
54+
)}
55+
>
56+
{entry.release?.url ? (
57+
<a
58+
href={entry.release.url}
59+
target='_blank'
60+
rel='noopener noreferrer'
61+
className='underline underline-offset-4'
62+
>
63+
Technical release notes
64+
</a>
65+
) : null}
66+
<BackLink href={archiveHref} label='All updates' />
67+
</footer>
68+
</article>
69+
)
70+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ChangelogArticle } from './changelog-article'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { ContentMeta } from '@/lib/content/schema'
2+
import { ChangelogCard } from '@/app/(landing)/changelog/components/changelog-grid/components/changelog-card'
3+
4+
interface ChangelogGridProps {
5+
entries: ContentMeta[]
6+
featureLatest?: boolean
7+
basePath?: string
8+
}
9+
10+
/** A featured story and two-column reading grid, using the customer-story composition. */
11+
export function ChangelogGrid({ entries, featureLatest = false, basePath }: ChangelogGridProps) {
12+
return (
13+
<section
14+
id='updates'
15+
aria-labelledby='updates-heading'
16+
className='grid grid-cols-2 gap-x-8 gap-y-12 max-md:grid-cols-1'
17+
>
18+
<h2 id='updates-heading' className='sr-only'>
19+
Product updates
20+
</h2>
21+
{entries.length > 0 ? (
22+
entries.map((entry, index) => (
23+
<ChangelogCard
24+
key={entry.slug}
25+
entry={entry}
26+
featured={featureLatest && index === 0}
27+
priority={index === 0}
28+
basePath={basePath}
29+
/>
30+
))
31+
) : (
32+
<p className='col-span-full py-12 text-[var(--text-secondary)] text-md'>
33+
Product updates will appear here as they are published.
34+
</p>
35+
)}
36+
</section>
37+
)
38+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { cn } from '@sim/emcn'
2+
import { ArrowRight } from '@sim/emcn/icons'
3+
import Image from 'next/image'
4+
import Link from 'next/link'
5+
import { CHANGELOG_SECTION } from '@/lib/changelog/constants'
6+
import type { ContentMeta } from '@/lib/content/schema'
7+
import { formatPostDate } from '@/app/(landing)/components/content-utils'
8+
import { HOME_TYPE, LANDING_STAGE_RADIUS } from '@/app/(landing)/components/landing-layout'
9+
10+
interface ChangelogCardProps {
11+
entry: ContentMeta
12+
featured: boolean
13+
priority: boolean
14+
basePath?: string
15+
}
16+
17+
const IMAGE_SIZES =
18+
'(min-width: 1728px) 671px, (min-width: 1280px) calc((100vw - 80px) * 5 / 12 - 16px), (min-width: 1024px) calc((100vw - 72px) * 5 / 12 - 16px), (min-width: 768px) calc(50vw - 48px), calc(100vw - 56px)'
19+
20+
export function ChangelogCard({
21+
entry,
22+
featured,
23+
priority,
24+
basePath = CHANGELOG_SECTION.basePath,
25+
}: ChangelogCardProps) {
26+
return (
27+
<Link
28+
id={entry.slug}
29+
href={`${basePath}/${entry.slug}`}
30+
aria-labelledby={`${entry.slug}-heading`}
31+
className={cn(
32+
'group min-w-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--text-primary)] focus-visible:outline-offset-4',
33+
LANDING_STAGE_RADIUS,
34+
featured
35+
? 'col-span-2 grid grid-cols-2 items-center gap-10 max-md:col-span-1 max-md:grid-cols-1 max-md:gap-5'
36+
: 'flex flex-col gap-5'
37+
)}
38+
>
39+
<div
40+
className={cn(
41+
'relative aspect-video overflow-hidden border border-[var(--border)] bg-[var(--surface-2)]',
42+
LANDING_STAGE_RADIUS
43+
)}
44+
>
45+
<Image
46+
src={entry.ogImage}
47+
alt={entry.ogAlt ?? entry.title}
48+
fill
49+
sizes={IMAGE_SIZES}
50+
priority={priority}
51+
className='object-cover'
52+
/>
53+
</div>
54+
<div className='flex min-w-0 flex-col items-start gap-4'>
55+
<time dateTime={entry.date} className='text-[var(--text-secondary)] text-sm'>
56+
{formatPostDate(entry.date)}
57+
</time>
58+
<h3
59+
id={`${entry.slug}-heading`}
60+
className={cn('text-balance text-[var(--text-primary)]', HOME_TYPE.h3)}
61+
>
62+
{entry.title}
63+
</h3>
64+
<p className={cn('text-pretty text-[var(--text-secondary)]', HOME_TYPE.body)}>
65+
{entry.description}
66+
</p>
67+
<span className='inline-flex items-center gap-2 text-[var(--text-primary)] text-sm'>
68+
Read update
69+
<ArrowRight
70+
aria-hidden='true'
71+
className='size-4 text-[var(--text-icon)] transition-transform duration-200 group-hover:translate-x-0.5 motion-reduce:transition-none'
72+
/>
73+
</span>
74+
</div>
75+
</Link>
76+
)
77+
}

0 commit comments

Comments
 (0)