feat: add release notes page with MDX content system
Implemented comprehensive release notes feature with: - MDX-based content system for easy release note authoring - Thematic sidebar navigation (Foundation, Features, Infrastructure) - Auto-generated pages from MDX frontmatter - Overview page with status filtering (completed/in_progress/planned) - Detail pages with custom MDX components - Mobile-responsive navigation (horizontal scroll tabs) - Integration with timeline (link to detailed release notes) - Added "Releases" link to header navigation Technical implementation: - next-mdx-remote for MDX parsing and rendering - gray-matter for frontmatter extraction - Custom MDX components for images, code blocks, typography - Static site generation (SSG) for performance - Template system for consistent release note structure Files added: - app/(marketing)/releases/ - Route structure and components - content/nl/releases/ - MDX content files and index - lib/mdx/releases.ts - MDX utility functions - docs/specs/releasepage/ - Build plan documentation - docs/templates/release-note-template.mdx - Content template First release note: authentication.mdx (login, signup, password reset) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
122
lib/mdx/releases.ts
Normal file
122
lib/mdx/releases.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Release Notes MDX Utilities
|
||||
*
|
||||
* Functions for loading and parsing release note MDX files
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc'
|
||||
|
||||
const RELEASES_DIR = path.join(process.cwd(), 'content/nl/releases')
|
||||
|
||||
export interface ReleaseFrontmatter {
|
||||
title: string
|
||||
category: string
|
||||
group: 'foundation' | 'features' | 'infrastructure'
|
||||
version: string
|
||||
releaseDate: string
|
||||
status: 'completed' | 'in_progress' | 'planned'
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface ReleaseNote {
|
||||
slug: string
|
||||
frontmatter: ReleaseFrontmatter
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all release note files
|
||||
*/
|
||||
export async function getAllReleases(): Promise<ReleaseNote[]> {
|
||||
const files = fs.readdirSync(RELEASES_DIR)
|
||||
|
||||
const releases = files
|
||||
.filter(file => file.endsWith('.mdx') && !file.startsWith('_'))
|
||||
.map(file => {
|
||||
const slug = file.replace('.mdx', '')
|
||||
const filePath = path.join(RELEASES_DIR, file)
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const { data, content } = matter(fileContent)
|
||||
|
||||
return {
|
||||
slug,
|
||||
frontmatter: data as ReleaseFrontmatter,
|
||||
content,
|
||||
}
|
||||
})
|
||||
|
||||
// Sort by group order and then by category
|
||||
return releases.sort((a, b) => {
|
||||
const groupOrder = { foundation: 1, features: 2, infrastructure: 3 }
|
||||
const aOrder = groupOrder[a.frontmatter.group]
|
||||
const bOrder = groupOrder[b.frontmatter.group]
|
||||
|
||||
if (aOrder !== bOrder) return aOrder - bOrder
|
||||
return a.frontmatter.category.localeCompare(b.frontmatter.category)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single release by slug
|
||||
*/
|
||||
export async function getRelease(slug: string): Promise<ReleaseNote | null> {
|
||||
const filePath = path.join(RELEASES_DIR, `${slug}.mdx`)
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const { data, content } = matter(fileContent)
|
||||
|
||||
return {
|
||||
slug,
|
||||
frontmatter: data as ReleaseFrontmatter,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get releases grouped by their group (foundation, features, infrastructure)
|
||||
*/
|
||||
export async function getReleasesGrouped() {
|
||||
const releases = await getAllReleases()
|
||||
|
||||
return {
|
||||
foundation: releases.filter(r => r.frontmatter.group === 'foundation'),
|
||||
features: releases.filter(r => r.frontmatter.group === 'features'),
|
||||
infrastructure: releases.filter(r => r.frontmatter.group === 'infrastructure'),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category metadata from index file
|
||||
*/
|
||||
export interface CategoryMetadata {
|
||||
slug: string
|
||||
title: string
|
||||
group: string
|
||||
description: string
|
||||
order: number
|
||||
}
|
||||
|
||||
export interface GroupMetadata {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
order: number
|
||||
}
|
||||
|
||||
interface IndexData {
|
||||
groups: GroupMetadata[]
|
||||
categories: CategoryMetadata[]
|
||||
}
|
||||
|
||||
export async function getCategoryMetadata(): Promise<IndexData> {
|
||||
const indexPath = path.join(RELEASES_DIR, '_index.json')
|
||||
const indexContent = fs.readFileSync(indexPath, 'utf-8')
|
||||
return JSON.parse(indexContent)
|
||||
}
|
||||
Reference in New Issue
Block a user