Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions app/newsroom/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const metadata = generatePageMetadata({

export default function NewsroomPage() {
const posts = getAllPosts()
// getAllPosts() already sorts featured first, so a featured post is posts[0].
const featuredPost = posts.find((post) => post.featured) ?? null
const recentPosts = featuredPost ? posts.filter((post) => post.slug !== featuredPost.slug) : posts

return (
<SectionWrapper className="min-h-[60vh]">
Expand All @@ -34,11 +37,39 @@ export default function NewsroomPage() {
{posts.length === 0 ? (
<p className="text-gray-500 text-sm">No posts yet. Check back soon.</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post, i) => (
<PostCard key={post.slug} post={post} featured={i === 0} />
))}
</div>
<>
{featuredPost && (
<div className="mb-12">
<div className="flex items-center gap-3 mb-4">
<div className="w-1 h-4 bg-[#3b1445] dark:bg-[#c084d8]" />
<span className="font-mono text-[#3b1445] dark:text-[#c084d8] text-xs tracking-[0.2em] uppercase">
Featured
</span>
</div>
<div className="border border-[#3b1445]/40 dark:border-[#5c2070]/50 rounded-none p-4 sm:p-6">
<PostCard post={featuredPost} featured />
</div>
</div>
)}

{recentPosts.length > 0 && (
<>
{featuredPost && (
<div className="flex items-center gap-3 mb-4">
<div className="w-1 h-4 bg-gray-300 dark:bg-[#1f1f1f]" />
<span className="font-mono text-gray-500 text-xs tracking-[0.2em] uppercase">
Recent
</span>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{recentPosts.map((post, i) => (
<PostCard key={post.slug} post={post} featured={!featuredPost && i === 0} />
))}
</div>
</>
)}
</>
)}
</SectionWrapper>
)
Expand Down
2 changes: 2 additions & 0 deletions components/newsroom/PostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const categoryLabels: Record<NewsroomPost['category'], string> = {
mission: 'Mission',
industry: 'Industry',
partner: 'Partner',
grant: 'Grant',
manifesto: 'Manifesto',
}

interface PostCardProps {
Expand Down
19 changes: 17 additions & 2 deletions lib/newsroom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,27 @@ function toPost(slug: string, data: Record<string, unknown>): NewsroomPost {
excerpt: String(data.excerpt ?? ''),
coverImage: data.coverImage ? String(data.coverImage) : undefined,
ogImage: data.ogImage ? String(data.ogImage) : undefined,
featured: data.featured === true,
}
}

/**
* Featured posts sort first, then everything by date descending. Without a
* featured post this is identical to plain date-descending order.
*
* Mirrored by tests/newsroom-featured.test.mjs — keep the two in step.
*/
export function comparePosts(a: NewsroomPost, b: NewsroomPost): number {
const aFeatured = a.featured === true
const bFeatured = b.featured === true
if (aFeatured !== bFeatured) return aFeatured ? -1 : 1
return new Date(b.date).getTime() - new Date(a.date).getTime()
}

export function getAllPosts(): NewsroomPost[] {
return readAllFiles()
.map(({ slug, data }) => toPost(slug, data))
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.sort(comparePosts)
}

export function getPostBySlug(slug: string): { meta: NewsroomPost; content: string } | null {
Expand All @@ -45,9 +59,10 @@ export function getPostBySlug(slug: string): { meta: NewsroomPost; content: stri
return { meta: toPost(slug, data), content }
}

/** Prefers a featured post so it holds the home-page slot; otherwise the newest. */
export function getLatestPost(): NewsroomPost | null {
const posts = getAllPosts()
return posts[0] ?? null
return posts.find((post) => post.featured === true) ?? posts[0] ?? null
}

export function formatPostDate(dateStr: string): string {
Expand Down
138 changes: 138 additions & 0 deletions tests/newsroom-featured.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Tests for newsroom featured-post pinning (lib/newsroom.ts).
//
// Node 20 cannot import the TypeScript source directly and the repo takes no
// new dependencies, so the comparator below mirrors comparePosts() in
// lib/newsroom.ts. Keep the two in step — the mirror is annotated there too.
//
// The final test does not mirror anything: it reads the real MDX frontmatter
// off disk and guards the category union that item 6a widened, which is the
// one failure mode a mirror cannot catch (a typo'd category silently falls
// back to 'announcement' in toPost()).
import { test } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import path from 'node:path'

/** Mirror of comparePosts() in lib/newsroom.ts */
function comparePosts(a, b) {
const aFeatured = a.featured === true
const bFeatured = b.featured === true
if (aFeatured !== bFeatured) return aFeatured ? -1 : 1
return new Date(b.date).getTime() - new Date(a.date).getTime()
}

/** Mirror of getAllPosts()'s ordering step */
const sortPosts = (posts) => [...posts].sort(comparePosts)

/** Mirror of getLatestPost() */
function pickLatest(posts) {
const sorted = sortPosts(posts)
return sorted.find((p) => p.featured === true) ?? sorted[0] ?? null
}

const post = (slug, date, featured) => ({ slug, date, ...(featured === undefined ? {} : { featured }) })

test('with no featured post, ordering is plain date descending (today\'s behavior)', () => {
const posts = [post('old', '2025-01-01'), post('new', '2026-06-01'), post('mid', '2025-08-01')]
assert.deepEqual(
sortPosts(posts).map((p) => p.slug),
['new', 'mid', 'old'],
)
})

test('a featured post sorts first even when it is the oldest', () => {
const posts = [
post('newest', '2026-07-01'),
post('manifesto', '2024-02-01', true),
post('middle', '2026-01-01'),
]
assert.deepEqual(
sortPosts(posts).map((p) => p.slug),
['manifesto', 'newest', 'middle'],
)
})

test('multiple featured posts stay date-descending among themselves, above the rest', () => {
const posts = [
post('plain-new', '2026-07-01'),
post('feat-old', '2025-01-01', true),
post('feat-new', '2026-03-01', true),
post('plain-old', '2024-01-01'),
]
assert.deepEqual(
sortPosts(posts).map((p) => p.slug),
['feat-new', 'feat-old', 'plain-new', 'plain-old'],
)
})

test('featured: false is treated the same as an absent flag', () => {
const explicit = sortPosts([post('a', '2026-01-01', false), post('b', '2026-02-01', false)])
const absent = sortPosts([post('a', '2026-01-01'), post('b', '2026-02-01')])
assert.deepEqual(explicit.map((p) => p.slug), absent.map((p) => p.slug))
assert.deepEqual(explicit.map((p) => p.slug), ['b', 'a'])
})

test('getLatestPost prefers a featured post over a newer plain one', () => {
const posts = [post('newest', '2026-07-01'), post('pinned', '2024-02-01', true)]
assert.equal(pickLatest(posts).slug, 'pinned')
})

test('getLatestPost falls back to the newest when nothing is featured', () => {
const posts = [post('old', '2025-01-01'), post('newest', '2026-07-01')]
assert.equal(pickLatest(posts).slug, 'newest')
})

test('getLatestPost returns null for an empty newsroom', () => {
assert.equal(pickLatest([]), null)
})

test('removing the featured flag restores the pre-item-6 result exactly', () => {
const withFlag = [post('a', '2026-01-01'), post('b', '2025-01-01', true), post('c', '2026-06-01')]
const withoutFlag = withFlag.map(({ slug, date }) => ({ slug, date }))
assert.equal(pickLatest(withFlag).slug, 'b', 'featured wins while the flag is set')
assert.equal(pickLatest(withoutFlag).slug, 'c', 'newest wins once it is removed')
})

// ── Real content, not a mirror ────────────────────────────────────────────────

const CATEGORIES = ['announcement', 'mission', 'industry', 'partner', 'grant', 'manifesto']

test('every newsroom post declares a category in the union and a valid featured flag', () => {
const dir = path.join(process.cwd(), 'content/newsroom')
if (!fs.existsSync(dir)) return
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.mdx'))
assert.ok(files.length > 0, 'expected at least one newsroom post')

for (const file of files) {
const raw = fs.readFileSync(path.join(dir, file), 'utf8')
const frontmatter = raw.split('---')[1] ?? ''

const category = frontmatter.match(/^category:\s*["']?([\w-]+)["']?\s*$/m)?.[1]
assert.ok(
category && CATEGORIES.includes(category),
`${file}: category ${JSON.stringify(category)} is not one of ${CATEGORIES.join(', ')}`,
)

const featured = frontmatter.match(/^featured:\s*(\S+)\s*$/m)?.[1]
if (featured !== undefined) {
assert.ok(
featured === 'true' || featured === 'false',
`${file}: featured must be true or false, got ${JSON.stringify(featured)}`,
)
}
}
})

test('at most one post is featured at a time', () => {
const dir = path.join(process.cwd(), 'content/newsroom')
if (!fs.existsSync(dir)) return
const featuredCount = fs
.readdirSync(dir)
.filter((f) => f.endsWith('.mdx'))
.filter((f) => /^featured:\s*true\s*$/m.test(fs.readFileSync(path.join(dir, f), 'utf8')))
.length
assert.ok(
featuredCount <= 1,
`${featuredCount} posts are featured; the /newsroom FEATURED slot and the home-page slot each show one`,
)
})
4 changes: 3 additions & 1 deletion types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,10 @@ export interface NewsroomPost {
title: string
date: string
author: string
category: 'announcement' | 'mission' | 'industry' | 'partner'
category: 'announcement' | 'mission' | 'industry' | 'partner' | 'grant' | 'manifesto'
excerpt: string
coverImage?: string
ogImage?: string
/** Pins the post above the date-sorted list and claims the home-page slot */
featured?: boolean
}