feat(docs): added generating tutorial and docs json files

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
This commit is contained in:
Evzen Gasta 2025-09-25 13:52:52 +02:00 committed by Evžen Gasta
parent 50676498de
commit 1efe2ec1a8
4 changed files with 432 additions and 0 deletions

2
website/.gitignore vendored
View file

@ -11,6 +11,8 @@
.docusaurus
.cache-loader
static/release-notes
docs.json
tutorials.json
# Cache
.eslintcache

View file

@ -3,6 +3,7 @@
import { resolve } from 'node:path';
import { createNotesFiles } from './release-notes-parser';
import Storybook from './storybook';
import { generateJsonOverviewFile } from './sidebar-content-parser';
const lightCodeTheme = require('prism-react-renderer').themes.github;
const darkCodeTheme = require('prism-react-renderer').themes.dracula;
@ -339,6 +340,22 @@ const config = {
id: 'tutorial',
path: 'tutorial',
routeBasePath: 'tutorial',
// Extract tutorial navigation using the navigation utils
/** @param {{ defaultSidebarItemsGenerator: any, [key: string]: any }} param0 */
async sidebarItemsGenerator({ defaultSidebarItemsGenerator, ...args }) {
const sidebarItems = await defaultSidebarItemsGenerator(args);
// Generate tutorials navigation using the utility function
await generateJsonOverviewFile(
sidebarItems,
'tutorial',
'https://podman-desktop.io',
'./static/tutorials.json',
);
return sidebarItems;
},
},
],
'./src/plugins/github-metadata-plugin.ts',
@ -375,6 +392,19 @@ const config = {
sidebarCollapsed: false,
sidebarPath: require.resolve('./sidebars.js'),
editUrl: 'https://github.com/podman-desktop/podman-desktop/tree/main/website',
// Enhanced function to extract navigation using the navigation utils
/** @param {{ defaultSidebarItemsGenerator: any, [key: string]: any }} param0 */
async sidebarItemsGenerator({ defaultSidebarItemsGenerator, ...args }) {
// Get the default sidebar items (what Docusaurus normally generates)
const sidebarItems = await defaultSidebarItemsGenerator(args);
// Generate docs navigation using the utility function
await generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json');
// Return the original sidebar items (unchanged)
return sidebarItems;
},
},
blog: {
blogTitle: 'Podman Desktop blog!',

View file

@ -0,0 +1,304 @@
import { writeFile } from 'node:fs/promises';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { extractNavigationFromSidebar, generateJsonOverviewFile } from './sidebar-content-parser';
// Mock the writeFile function
vi.mock('node:fs/promises', () => ({
writeFile: vi.fn(),
}));
beforeEach(() => {
vi.clearAllMocks();
});
describe('extractNavigationFromSidebar', () => {
test('should extract navigation items from simple doc items', () => {
const sidebarItems = [
{
type: 'doc' as const,
id: 'intro',
label: 'Introduction',
},
{
type: 'doc' as const,
id: 'running-a-kubernetes-cluster',
label: 'Running a Kubernetes cluster',
},
];
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
expect(result).toEqual([
{
name: 'Introduction',
url: 'https://podman-desktop.io/docs/intro',
},
{
name: 'Running a kubernetes cluster',
url: 'https://podman-desktop.io/docs/running-a-kubernetes-cluster',
},
]);
});
test('should handle items without labels by using id and formatting it', () => {
const sidebarItems = [
{
type: 'doc' as const,
id: 'deploying-a-kubernetes-application',
},
];
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
expect(result).toEqual([
{
name: 'Deploying a kubernetes application',
url: 'https://podman-desktop.io/docs/deploying-a-kubernetes-application',
},
]);
});
test('should handle tutorial index case correctly', () => {
const sidebarItems = [
{
type: 'doc' as const,
id: 'index',
label: 'Introduction',
},
];
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'tutorial');
expect(result).toEqual([
{
name: 'Introduction',
url: 'https://podman-desktop.io/tutorial',
},
]);
});
test('should handle category items recursively', () => {
const sidebarItems = [
{
type: 'category' as const,
label: 'Getting Started',
items: [
{
type: 'doc' as const,
id: 'intro',
label: 'Introduction',
},
{
type: 'doc' as const,
id: 'installation',
label: 'Installation',
},
],
},
];
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
expect(result).toEqual([
{
name: 'Introduction',
url: 'https://podman-desktop.io/docs/intro',
},
{
name: 'Installation',
url: 'https://podman-desktop.io/docs/installation',
},
]);
});
test('should format names correctly by replacing hyphens with spaces and capitalizing first letter', () => {
const sidebarItems = [
{
type: 'doc' as const,
id: 'getting-started-with-compose',
label: 'Getting started with Compose',
},
{
type: 'doc' as const,
id: 'running-an-ai-application',
},
];
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
expect(result).toEqual([
{
name: 'Getting started with compose',
url: 'https://podman-desktop.io/docs/getting-started-with-compose',
},
{
name: 'Running an AI application',
url: 'https://podman-desktop.io/docs/running-an-ai-application',
},
]);
});
test('should keep AI capitalized in names', () => {
const sidebarItems = [
{
type: 'doc' as const,
id: 'ai-lab-start-recipe',
label: 'AI Lab Start Recipe',
},
{
type: 'doc' as const,
id: 'running-an-ai-application',
},
{
type: 'doc' as const,
id: 'ai-powered-tools',
},
];
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
expect(result).toEqual([
{
name: 'AI lab start recipe',
url: 'https://podman-desktop.io/docs/ai-lab-start-recipe',
},
{
name: 'Running an AI application',
url: 'https://podman-desktop.io/docs/running-an-ai-application',
},
{
name: 'AI powered tools',
url: 'https://podman-desktop.io/docs/ai-powered-tools',
},
]);
});
test('should skip items with empty names', () => {
const sidebarItems = [
{
type: 'doc' as const,
id: '',
label: '',
},
{
type: 'doc' as const,
id: 'valid-item',
label: 'Valid Item',
},
];
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
expect(result).toEqual([
{
name: 'Valid item',
url: 'https://podman-desktop.io/docs/valid-item',
},
]);
});
});
describe('generateJsonOverviewFile', () => {
test('should generate JSON file for docs section', async () => {
const sidebarItems = [
{
type: 'doc' as const,
id: 'intro',
label: 'Introduction',
},
];
await generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json');
expect(writeFile).toHaveBeenCalledWith(
'./static/docs.json',
JSON.stringify([
{
name: 'Introduction',
url: 'https://podman-desktop.io/docs/intro',
},
]),
);
});
test('should generate JSON file for tutorial section', async () => {
const sidebarItems = [
{
type: 'doc' as const,
id: 'index',
label: 'Introduction',
},
];
await generateJsonOverviewFile(sidebarItems, 'tutorial', 'https://podman-desktop.io', './static/tutorials.json');
expect(writeFile).toHaveBeenCalledWith(
'./static/tutorials.json',
JSON.stringify([
{
name: 'Introduction',
url: 'https://podman-desktop.io/tutorial',
},
]),
);
});
test('should sort items alphabetically by name', async () => {
const sidebarItems = [
{
type: 'doc' as const,
id: 'zebra',
label: 'Zebra',
},
{
type: 'doc' as const,
id: 'apple',
label: 'Apple',
},
{
type: 'doc' as const,
id: 'banana',
label: 'Banana',
},
];
await generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json');
const expectedContent = JSON.stringify([
{
name: 'Apple',
url: 'https://podman-desktop.io/docs/apple',
},
{
name: 'Banana',
url: 'https://podman-desktop.io/docs/banana',
},
{
name: 'Zebra',
url: 'https://podman-desktop.io/docs/zebra',
},
]);
expect(writeFile).toHaveBeenCalledWith('./static/docs.json', expectedContent);
});
test('should propagate writeFile errors', async () => {
const mockError = new Error('Write failed');
vi.mocked(writeFile).mockRejectedValueOnce(mockError);
const sidebarItems = [
{
type: 'doc' as const,
id: 'intro',
label: 'Introduction',
},
];
// Should throw the error
await expect(
generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json'),
).rejects.toThrow('Write failed');
});
});

View file

@ -0,0 +1,96 @@
import { writeFile } from 'node:fs/promises';
// Type definitions for Docusaurus sidebar items
interface DocSidebarItem {
type: 'doc';
id: string;
label?: string;
}
interface CategorySidebarItem {
type: 'category';
label?: string;
items: SidebarItem[];
}
type SidebarItem = DocSidebarItem | CategorySidebarItem;
interface NavigationItem {
name: string;
url: string;
}
/**
* Extract navigation items from sidebar structure
* @param items - Sidebar items from Docusaurus
* @param baseUrl - Base URL for the site
* @param section - Section type ('docs' or 'tutorial')
* @returns Navigation items
*/
export function extractNavigationFromSidebar(
items: SidebarItem[],
baseUrl: string,
section: 'docs' | 'tutorial' = 'docs',
): NavigationItem[] {
const navItems: NavigationItem[] = [];
items.forEach(item => {
if (item.type === 'doc') {
// Use the label (display name) instead of the id for better readability
const rawName = item.label ?? item.id;
if (!rawName) {
return;
}
// Replace hashes with spaces and capitalize only the first letter of the whole name
const processedName = rawName
.replace(/-/g, ' ') // Replace hyphens with spaces
.replace(/\s+/g, ' ') // Normalize multiple spaces to single space
.trim() // Remove leading/trailing spaces
.toLowerCase() // Convert to lowercase
.replace(/\bai\b/g, 'AI'); // Keep "AI" capitalized (word boundary to avoid partial matches)
let name = processedName.charAt(0).toUpperCase() + processedName.slice(1);
// Handle tutorial index case - it should point to base /tutorial URL
let url: string;
if (section === 'tutorial' && item.id === 'index') {
url = `${baseUrl}/tutorial`;
name = 'Introduction';
} else {
url = `${baseUrl}/${section}/${item.id}`;
}
navItems.push({
name: name,
url: url,
});
} else if (item.type === 'category' && item.items) {
// Recursively extract from categories
navItems.push(...extractNavigationFromSidebar(item.items, baseUrl, section));
}
});
return navItems;
}
/**
* Generate JSON overview file for navigation
* @param sidebarItems - Sidebar items from Docusaurus
* @param section - Section type ('docs' or 'tutorial')
* @param baseUrl - Base URL for the site
* @param outputPath - Path to output the JSON file
*/
export async function generateJsonOverviewFile(
sidebarItems: SidebarItem[],
section: 'docs' | 'tutorial',
baseUrl: string,
outputPath: string,
): Promise<void> {
const navItems = extractNavigationFromSidebar(sidebarItems, baseUrl, section);
await writeFile(
outputPath,
JSON.stringify(navItems.toSorted((a: NavigationItem, b: NavigationItem) => a.name.localeCompare(b.name))),
);
}