feat(site): search (#165)

This commit is contained in:
Darius Cepulis
2025-11-05 13:04:06 -06:00
committed by GitHub
parent a41cdba068
commit 8ba758ab5d
24 changed files with 581 additions and 40 deletions
@@ -5,6 +5,7 @@ import {
findFirstGuide,
findGuideBySlug,
getAllGuideSlugs,
getSectionsForGuide,
getValidStylesForGuide,
} from '../sidebar';
@@ -254,6 +255,95 @@ describe('sidebar utilities', () => {
});
});
describe('getSectionsForGuide', () => {
it('should return empty array for top-level guide', () => {
const result = getSectionsForGuide('guide-3', mockSidebar);
expect(result).toEqual([]);
});
it('should return section label for guide nested in one section', () => {
const result = getSectionsForGuide('guide-1', mockSidebar);
expect(result).toEqual(['Section 1']);
});
it('should return all ancestor section labels in order', () => {
const nestedSidebar: Sidebar = [
{
sidebarLabel: 'Parent',
contents: [
{
sidebarLabel: 'Child',
contents: [mockGuide1],
},
mockGuide2,
],
},
];
const result = getSectionsForGuide('guide-1', nestedSidebar);
expect(result).toEqual(['Parent', 'Child']);
const result2 = getSectionsForGuide('guide-2', nestedSidebar);
expect(result2).toEqual(['Parent']);
});
it('should return empty array if guide is not found', () => {
const result = getSectionsForGuide('non-existent', mockSidebar);
expect(result).toEqual([]);
});
it('should handle deeply nested guides', () => {
const deeplyNestedSidebar: Sidebar = [
{
sidebarLabel: 'Level 1',
contents: [
{
sidebarLabel: 'Level 2',
contents: [
{
sidebarLabel: 'Level 3',
contents: [mockGuide1],
},
],
},
],
},
];
const result = getSectionsForGuide('guide-1', deeplyNestedSidebar);
expect(result).toEqual(['Level 1', 'Level 2', 'Level 3']);
});
it('should return empty array for empty sidebar', () => {
const result = getSectionsForGuide('guide-1', []);
expect(result).toEqual([]);
});
it('should work with sidebar containing mixed top-level and nested guides', () => {
const mixedSidebar: Sidebar = [
mockGuide3, // top level
{
sidebarLabel: 'Getting Started',
contents: [
mockGuide1,
{
sidebarLabel: 'Advanced',
contents: [mockGuide2],
},
],
},
];
expect(getSectionsForGuide('guide-3', mixedSidebar)).toEqual([]);
expect(getSectionsForGuide('guide-1', mixedSidebar)).toEqual(['Getting Started']);
expect(getSectionsForGuide('guide-2', mixedSidebar)).toEqual(['Getting Started', 'Advanced']);
});
});
describe('getValidStylesForGuide', () => {
it('should return framework styles that guide supports', () => {
const result = getValidStylesForGuide(mockGuide1, 'html');
+19
View File
@@ -32,6 +32,25 @@ export function getPreferencesServer(cookies: AstroCookies): Preference {
* Client-side API: Works with document.cookie
*/
export function getPreferenceClient(): Preference {
if (typeof document === 'undefined') return { framework: null, style: null };
const cookies = document.cookie.split(';').reduce((acc, cookie) => {
const [key, value] = cookie.trim().split('=');
if (key) acc[key] = value;
return acc;
}, {} as Record<string, string>);
const framework = cookies[FRAMEWORK_COOKIE] && isValidFramework(cookies[FRAMEWORK_COOKIE])
? cookies[FRAMEWORK_COOKIE]
: null;
const style = framework && cookies[STYLE_COOKIE] && isValidStyleForFramework(framework, cookies[STYLE_COOKIE])
? cookies[STYLE_COOKIE]
: null;
return { framework, style } as Preference;
}
export function setPreferenceClient<T extends SupportedFramework>(framework: T, style: SupportedStyle<T>) {
if (typeof document === 'undefined') return;
if (!isValidFramework(framework)) throw new Error(`Invalid framework: ${framework}`);
+28
View File
@@ -158,6 +158,34 @@ export function findGuideBySlug(slug: string, sidebarToSearch: Sidebar = sidebar
return null;
}
/**
* Get the ancestor section labels for a guide by its slug.
* Returns an array of sidebarLabel strings for all ancestor sections,
* in order from outermost to innermost. If the guide has no ancestors
* (i.e., it's at the top level) or if the guide is not found, returns an empty array.
*
* @param slug - The slug of the guide to find
* @param sidebarToSearch - Optional sidebar to search (defaults to main sidebar config)
* @returns An array of ancestor section labels, or empty array if none or guide not found
*/
export function getSectionsForGuide(slug: string, sidebarToSearch: Sidebar = sidebar): string[] {
function findInSidebar(items: Sidebar, path: string[]): string[] | null {
for (const item of items) {
if (isSection(item)) {
// 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) {
// Found the guide, return the accumulated path
return path;
}
}
return null;
}
return findInSidebar(sidebarToSearch, []) ?? [];
}
/**
* Get valid styles for a guide, optionally filtered by framework.
*
+2
View File
@@ -25,6 +25,7 @@ export function createTechArticleSchema(params: {
updatedDate?: Date;
wordCount?: number;
readingTime?: number;
articleSection?: string;
}): WithContext<TechArticle> {
return {
'@context': 'https://schema.org',
@@ -35,6 +36,7 @@ export function createTechArticleSchema(params: {
...(params.updatedDate && { dateModified: params.updatedDate.toISOString() }),
...(params.wordCount && { wordCount: params.wordCount }),
...(params.readingTime && { timeRequired: `PT${params.readingTime}M` }),
...(params.articleSection && { articleSection: params.articleSection }),
author: {
'@type': 'Organization',
name: 'Video.js',