feat(site): add changelog section (#1793)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-07-02 11:05:41 -07:00
committed by GitHub
co-authored by Claude
parent 2a8cbd0df4
commit 050d963472
16 changed files with 447 additions and 26 deletions
+44 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import type { Guide, Section, Sidebar } from '../../../types/docs';
import type { Guide, Section, Sidebar, SidebarLink } from '../../../types/docs';
import {
filterSidebar,
findFirstGuide,
@@ -419,6 +419,49 @@ describe('sidebar utilities', () => {
});
});
describe('SidebarLink handling', () => {
const mockLink: SidebarLink = {
href: '/changelog',
sidebarLabel: 'Changelog',
};
it('should pass links through filterSidebar', () => {
const sidebar: Sidebar = [{ sidebarLabel: 'Section', contents: [mockGuide1, mockLink] }];
const result = filterSidebar('html', sidebar);
expect((result[0] as Section).contents).toContainEqual(mockLink);
});
it('should filter out devOnly links in production', () => {
const devLink: SidebarLink = { ...mockLink, devOnly: true };
const sidebar: Sidebar = [{ sidebarLabel: 'Section', contents: [mockGuide1, devLink] }];
const result = filterSidebar('html', sidebar, false);
expect((result[0] as Section).contents).toEqual([mockGuide1]);
});
it('should skip links in findFirstGuide', () => {
const sidebar: Sidebar = [mockLink, mockGuide3];
const result = findFirstGuide('html', sidebar);
expect(result).toBe('guide-3');
});
it('should exclude links from getAllGuideSlugs', () => {
const sidebar: Sidebar = [{ sidebarLabel: 'Section', contents: [mockGuide1, mockLink] }, mockGuide3];
const result = getAllGuideSlugs(sidebar);
expect(result).toEqual(['guide-1', 'guide-3']);
});
it('should ignore links in findGuideBySlug', () => {
const sidebar: Sidebar = [mockLink, mockGuide3];
const result = findGuideBySlug('guide-3', sidebar);
expect(result).toEqual(mockGuide3);
});
});
describe('sidebar config validation', () => {
it('should not have duplicate slugs in sidebar config', async () => {
// Import the real sidebar config
+8 -8
View File
@@ -1,5 +1,5 @@
import type { Guide, Section, Sidebar, SupportedFramework } from '@/types/docs';
import { FRAMEWORK_STYLES, isSection } from '@/types/docs';
import type { Guide, Sidebar, SidebarItem, SupportedFramework } from '@/types/docs';
import { FRAMEWORK_STYLES, isLink, isSection } from '@/types/docs';
import { sidebar } from '../../docs.config';
@@ -13,7 +13,7 @@ import { sidebar } from '../../docs.config';
* @returns true if the item should be visible
*/
export function isItemVisible(
item: Guide | Section,
item: SidebarItem,
framework: SupportedFramework,
isDev: boolean = import.meta.env.DEV
): boolean {
@@ -94,7 +94,7 @@ export function findFirstGuide(
} catch {
// Continue searching other sections
}
} else {
} else if (!isLink(item)) {
// It's a Guide, return its slug
return item.slug;
}
@@ -119,7 +119,7 @@ export function getAllGuideSlugs(sidebarToExtract: Sidebar = sidebar): string[]
if (isSection(item)) {
// Recursively get slugs from section contents
slugs.push(...getAllGuideSlugs(item.contents));
} else {
} else if (!isLink(item)) {
// It's a Guide, add its slug
slugs.push(item.slug);
}
@@ -141,7 +141,7 @@ export function findGuideBySlug(slug: string, sidebarToSearch: Sidebar = sidebar
// Recursively search section contents
const guide = findGuideBySlug(slug, item.contents);
if (guide) return guide;
} else if (item.slug === slug) {
} else if (!isLink(item) && item.slug === slug) {
// Found the guide
return item;
}
@@ -166,7 +166,7 @@ export function getSectionsForGuide(slug: string, sidebarToSearch: Sidebar = sid
// Recursively search section contents with updated path
const result = findInSidebar(item.contents, [...path, item.sidebarLabel]);
if (result !== null) return result;
} else if (item.slug === slug) {
} else if (!isLink(item) && item.slug === slug) {
// Found the guide, return the accumulated path
return path;
}
@@ -191,7 +191,7 @@ export function getValidFrameworksForGuide(guide: Guide, sidebarToSearch: Sideba
const sectionFrameworks = item.frameworks ? inherited.filter((f) => item.frameworks!.includes(f)) : inherited;
const result = findWithRestrictions(item.contents, sectionFrameworks);
if (result) return result;
} else if (item.slug === guide.slug) {
} else if (!isLink(item) && item.slug === guide.slug) {
return item.frameworks ? inherited.filter((f) => item.frameworks!.includes(f)) : inherited;
}
}
+31 -1
View File
@@ -13,7 +13,7 @@ const STATIC_PAGES: { path: string; title: string }[] = [
{ path: 'blog', title: 'Blog' },
];
export type OgTitleEntryKind = 'static' | 'blog' | 'blog-index' | 'author' | 'docs';
export type OgTitleEntryKind = 'static' | 'blog' | 'blog-index' | 'author' | 'docs' | 'changelog' | 'changelog-index';
export type OgTitleSource = 'static' | 'title' | 'ogTitle' | 'frameworkTitle' | 'name';
export interface OgTitleEntry {
@@ -60,6 +60,36 @@ export async function listOgTitleEntries(): Promise<OgTitleEntry[]> {
});
}
const changelogEntries = await getCollection('changelog');
entries.push({
kind: 'changelog-index',
path: 'changelog',
title: 'Changelog',
source: 'static',
});
for (const entry of changelogEntries) {
entries.push({
kind: 'changelog',
path: `changelog/${entry.id}`,
title: `v${entry.data.version} Changelog`,
source: 'title',
collectionId: entry.id,
});
}
const totalChangelogPages = Math.ceil(changelogEntries.length / BLOG_PAGE_SIZE);
for (let page = 2; page <= totalChangelogPages; page += 1) {
entries.push({
kind: 'changelog-index',
path: `changelog/${page}`,
title: 'Changelog',
source: 'static',
});
}
const authors = await getCollection('authors');
for (const author of authors) {