chore: update documentation links to be used from product.json (#16912)

* chore: update documentation links to be used from product.json
Signed-off-by: Sonia Sandler <ssandler@redhat.com>

* chore: adjust types in product.json
Signed-off-by: Sonia Sandler <ssandler@redhat.com>
This commit is contained in:
Sonia Sandler 2026-04-09 15:01:42 -04:00 committed by GitHub
parent 1095fded1c
commit a7f05e926a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 359 additions and 188 deletions

View file

@ -20,6 +20,7 @@ import type { ApiSenderType } from '@podman-desktop/core-api/api-sender';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { DocumentationService } from '/@/plugin/documentation/documentation-service.js';
import product from '/@product.json' with { type: 'json' };
const originalConsoleError = console.error;
@ -30,10 +31,35 @@ const mockApiSender = {
send: vi.fn(),
} as unknown as ApiSenderType;
const fallbackDocumentation = [
{
id: 'item1',
name: 'Item 1',
description: 'Some description 1',
url: '/url/1',
category: 'Documentation',
},
{
id: 'item2',
name: 'Item 2',
description: 'Some description 2',
url: '/url/2',
category: 'Tutorial',
},
];
vi.mock(import('/@product.json'));
beforeEach(() => {
vi.resetAllMocks();
documentationService = new DocumentationService(mockApiSender);
console.error = vi.fn();
vi.mocked(product).documentation.links = [
{ link: '/docs/link', category: 'category 1' },
{ link: '/tutorial/link', category: 'category 2' },
{ link: '/some/link', category: 'category 3' },
];
vi.mocked(product).documentation.fallback = fallbackDocumentation;
});
afterEach(() => {
@ -42,42 +68,54 @@ afterEach(() => {
describe('fetchDocumentation', () => {
test('should fetch documentation and tutorials successfully', async () => {
const mockDocsHtml = `
<html>
<body>
<a href="/docs/intro">Introduction</a>
<a href="/docs/containers">Containers Guide</a>
<a href="/docs/kubernetes">Kubernetes Guide</a>
</body>
</html>
`;
const mockDocsJson = [
{
name: 'Introduction',
url: '/docs/intro',
},
{
name: 'Containers Guide',
url: '/docs/containers',
},
{
name: 'Kubernetes Guide',
url: '/docs/kubernetes',
},
];
const mockTutorialHtml = `
<html>
<body>
<a href="/tutorial/getting-started">Getting Started Tutorial</a>
<a href="/tutorial/kubernetes-cluster">Kubernetes Cluster Tutorial</a>
</body>
</html>
`;
const mockTutorialJson = [
{
name: 'Getting Started Tutorial',
url: '/tutorial/getting-started',
},
{
name: 'Kubernetes Cluster Tutorial',
url: '/tutorial/kubernetes-cluster',
},
];
const fetchSpy = vi
.spyOn(global, 'fetch')
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockDocsHtml),
json: () => Promise.resolve(mockDocsJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockTutorialHtml),
json: () => Promise.resolve(mockTutorialJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await documentationService.fetchDocumentation();
// Verify fetch was called with correct URLs
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy).toHaveBeenCalledWith('https://podman-desktop.io/docs.json');
expect(fetchSpy).toHaveBeenCalledWith('https://podman-desktop.io/tutorials.json');
expect(fetchSpy).toHaveBeenCalledTimes(3);
expect(fetchSpy).toHaveBeenCalledWith('/docs/link');
expect(fetchSpy).toHaveBeenCalledWith('/tutorial/link');
expect(fetchSpy).toHaveBeenCalledWith('/some/link');
// Verify service is initialized
const items = await documentationService.getDocumentationItems();
@ -92,14 +130,18 @@ describe('fetchDocumentation', () => {
const items = await documentationService.getDocumentationItems();
expect(items).toBeDefined();
expect(items.length).toBeGreaterThan(0);
expect(items.length).toBe(2);
// Should include fallback items
const introItem = items.find(item => item.id === 'docs-intro');
expect(introItem).toBeDefined();
expect(introItem?.name).toBe('Introduction & Getting Started');
const firstItem = items.find(item => item.id === 'item1');
expect(firstItem).toBeDefined();
expect(firstItem?.name).toBe('Item 1');
expect(fetchSpy).toHaveBeenCalledTimes(2);
const secondItem = items.find(item => item.id === 'item2');
expect(secondItem).toBeDefined();
expect(secondItem?.name).toBe('Item 2');
expect(fetchSpy).toHaveBeenCalledTimes(3);
});
test('should use fallback when HTTP response is not ok', async () => {
@ -115,291 +157,389 @@ describe('fetchDocumentation', () => {
expect(items).toBeDefined();
expect(items.length).toBeGreaterThan(0);
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy).toHaveBeenCalledTimes(3);
});
});
describe('getDocumentationItems', () => {
test('should initialize automatically if not initialized', async () => {
const mockDocsHtml = '<a href="/docs/auto">Auto Init</a>';
const mockTutorialHtml = '<a href="/tutorial/auto">Auto Tutorial</a>';
const mockDocsJson = [
{
name: 'Auto Init',
url: '/docs/auto',
},
];
const mockTutorialJson = [
{
name: 'Auto Tutorial',
url: '/tutorial/auto',
},
];
const fetchSpy = vi
.spyOn(global, 'fetch')
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockDocsHtml),
json: () => Promise.resolve(mockDocsJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockTutorialHtml),
json: () => Promise.resolve(mockTutorialJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
const items = await documentationService.getDocumentationItems();
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy).toHaveBeenCalledTimes(3);
expect(items).toBeDefined();
expect(items.length).toBeGreaterThan(0);
});
test('should return cached items after initialization', async () => {
const mockDocsHtml = '<a href="/docs/cached">Cached</a>';
const mockTutorialHtml = '<a href="/tutorial/cached">Cached Tutorial</a>';
const mockDocsJson = [
{
name: 'Cached',
url: '/docs/cached',
},
];
const mockTutorialJson = [
{
name: 'Cached Tutorial',
url: '/tutorial/cached',
},
];
// Only mock once - should be cached after first call
const fetchSpy = vi
.spyOn(global, 'fetch')
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockDocsHtml),
json: () => Promise.resolve(mockDocsJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockTutorialHtml),
json: () => Promise.resolve(mockTutorialJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
const firstCall = await documentationService.getDocumentationItems();
const secondCall = await documentationService.getDocumentationItems();
expect(firstCall).toStrictEqual(secondCall); // Same content
expect(fetchSpy).toHaveBeenCalledTimes(2); // Only called once for initialization
expect(fetchSpy).toHaveBeenCalledTimes(3); // Only called once for initialization
});
});
describe('refreshDocumentation', () => {
test('should re-fetch documentation and send update notification', async () => {
const mockDocsHtml = '<a href="/docs/refresh">Refresh Test</a>';
const mockTutorialHtml = '<a href="/tutorial/refresh">Refresh Tutorial</a>';
const mockDocsJson = [
{
name: 'Refresh Test',
url: '/docs/refresh',
},
];
const mockTutorialJson = [
{
name: 'Refresh Tutorial',
url: '/tutorial/refresh',
},
];
// Initial fetch
const fetchSpy = vi
.spyOn(global, 'fetch')
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockDocsHtml),
json: () => Promise.resolve(mockDocsJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockTutorialHtml),
json: () => Promise.resolve(mockTutorialJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await documentationService.fetchDocumentation();
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy).toHaveBeenCalledTimes(3);
// Refresh fetch - add more mock calls
fetchSpy
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockDocsHtml),
json: () => Promise.resolve(mockDocsJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockTutorialHtml),
json: () => Promise.resolve(mockTutorialJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await documentationService.refreshDocumentation();
expect(fetchSpy).toHaveBeenCalledTimes(4); // 2 initial + 2 refresh
expect(fetchSpy).toHaveBeenCalledTimes(6); // 3 initial + 3 refresh
expect(mockApiSender.send).toHaveBeenCalledWith('documentation-updated');
});
});
describe('parseDocumentationContent', () => {
test('should parse documentation and tutorial links correctly', async () => {
const mockDocsHtml = `
<html>
<body>
<nav>
<a href="/docs/intro">Introduction & Getting Started</a>
<a href="/docs/containers">Working with Containers</a>
<a href="/docs/kubernetes">Kubernetes Integration</a>
<a href="/docs/troubleshooting">Troubleshooting Guide</a>
<a href="#edit">Edit this page</a> <!-- Should be filtered out -->
<a href="#next">Next</a> <!-- Should be filtered out -->
</nav>
</body>
</html>
`;
const mockDocsJson = [
{
name: 'Introduction & Getting Started',
url: '/docs/intro',
},
{
name: 'Working with Containers',
url: '/docs/containers',
},
{
name: 'Kubernetes Integration',
url: '/docs/kubernetes',
},
{
name: 'Troubleshooting Guide',
url: '/docs/troubleshooting',
},
];
const mockTutorialHtml = `
<html>
<body>
<nav>
<a href="/tutorial/getting-started">Getting Started</a>
<a href="/tutorial/kubernetes-cluster">Creating a Kubernetes Cluster</a>
<a href="/tutorial/compose">Using Docker Compose</a>
<a href="#edit">Edit this page</a> <!-- Should be filtered out -->
</nav>
</body>
</html>
`;
const mockTutorialJson = [
{
name: 'Getting Started',
url: '/tutorial/getting-started',
},
{
name: 'Creating a Kubernetes Cluster',
url: '/tutorial/kubernetes-cluster',
},
{
name: 'Using Docker Compose',
url: '/tutorial/compose',
},
];
const fetchSpy = vi
.spyOn(global, 'fetch')
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockDocsHtml),
json: () => Promise.resolve(mockDocsJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockTutorialHtml),
json: () => Promise.resolve(mockTutorialJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await documentationService.fetchDocumentation();
const items = await documentationService.getDocumentationItems();
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy).toHaveBeenCalledTimes(3);
// Check that we have core pages plus parsed pages
const docItems = items.filter(item => item.category === 'Documentation');
const tutorialItems = items.filter(item => item.category === 'Tutorial');
console.log(items);
expect(docItems.length).toBeGreaterThanOrEqual(4); // Core + parsed
expect(tutorialItems.length).toBeGreaterThanOrEqual(1); // Core + parsed
// Check that we have expected parsed items and categories
const cat1Items = items.filter(item => item.category === 'category 1');
const cat2Items = items.filter(item => item.category === 'category 2');
expect(cat1Items.length).toBe(4); // parsed
expect(cat2Items.length).toBe(3); // parsed
// Check specific parsed items
const introItem = items.find(item => item.name === 'Introduction & Getting Started' && item.id === 'docs-intro');
const introItem = items.find(item => item.name === 'Introduction & Getting Started');
expect(introItem).toBeDefined();
expect(introItem?.category).toBe('Documentation');
expect(introItem?.url).toBe('https://podman-desktop.io/docs/intro');
expect(introItem?.category).toBe('category 1');
expect(introItem?.url).toBe('/docs/intro');
// Tutorial item may not be parsed if HTML doesn't match regex, so just check core tutorial exists
const coreTutorialItem = items.find(item => item.id === 'tutorial-index');
const coreTutorialItem = items.find(item => item.name === 'Getting Started');
expect(coreTutorialItem).toBeDefined();
expect(coreTutorialItem?.category).toBe('Tutorial');
// Check that filtered items are not included
const editItem = items.find(item => item.name.includes('Edit this page'));
expect(editItem).toBeUndefined();
const nextItem = items.find(item => item.name.includes('Next'));
expect(nextItem).toBeUndefined();
expect(coreTutorialItem?.category).toBe('category 2');
});
test('should handle relative and absolute URLs correctly', async () => {
const mockDocsHtml = `
<a href="/docs/relative">Relative Link</a>
<a href="https://podman-desktop.io/docs/absolute">Absolute Link</a>
`;
const mockDocsJson = [
{
name: 'Relative Link',
url: '/docs/relative',
},
{
name: 'Absolute Link',
url: 'https://podman-desktop.io/docs/absolute',
},
];
const mockTutorialHtml = `
<a href="/tutorial/relative">Relative Tutorial</a>
<a href="https://podman-desktop.io/tutorial/absolute">Absolute Tutorial</a>
`;
const mockTutorialJson = [
{
name: 'Relative Tutorial',
url: '/tutorial/relative',
},
{
name: 'Absolute Tutorial',
url: 'https://podman-desktop.io/tutorial/absolute',
},
];
const fetchSpy = vi
.spyOn(global, 'fetch')
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockDocsHtml),
json: () => Promise.resolve(mockDocsJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockTutorialHtml),
json: () => Promise.resolve(mockTutorialJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await documentationService.fetchDocumentation();
const items = await documentationService.getDocumentationItems();
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy).toHaveBeenCalledTimes(3);
// Since the simple HTML may not match complex regex patterns,
// we should mainly verify core documentation pages exist
const coreDocItem = items.find(item => item.id === 'docs-intro');
expect(coreDocItem).toBeDefined();
expect(coreDocItem?.url).toBe('https://podman-desktop.io/docs/intro');
const docItem = items.find(item => item.name === 'Relative Link');
expect(docItem).toBeDefined();
expect(docItem?.url).toBe('/docs/relative');
const coreTutorialItem = items.find(item => item.id === 'tutorial-index');
expect(coreTutorialItem).toBeDefined();
expect(coreTutorialItem?.url).toBe('https://podman-desktop.io/tutorial');
const tutorialItem = items.find(item => item.name === 'Absolute Tutorial');
expect(tutorialItem).toBeDefined();
expect(tutorialItem?.url).toBe('https://podman-desktop.io/tutorial/absolute');
});
test('should handle empty or malformed HTML gracefully', async () => {
test('should handle empty JSON gracefully', async () => {
const fetchSpy = vi
.spyOn(global, 'fetch')
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(''),
json: () => Promise.resolve([]),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve('<html><body></body></html>'),
json: () => Promise.resolve([]),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await documentationService.fetchDocumentation();
const items = await documentationService.getDocumentationItems();
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy).toHaveBeenCalledTimes(3);
// Should still have core documentation pages
expect(items.length).toBeGreaterThan(0);
const coreItem = items.find(item => item.id === 'docs-intro');
expect(coreItem).toBeDefined();
// Should fallback to predefined documentation when JSON lists are empty
expect(items).toStrictEqual(fallbackDocumentation);
});
test('should remove duplicate items', async () => {
const mockDocsHtml = `
<a href="/docs/duplicate">Duplicate Item</a>
<a href="/docs/duplicate">Duplicate Item</a>
<a href="/docs/unique">Unique Item</a>
`;
const mockDocsJson = [
{
name: 'Duplicate Item',
url: '/docs/duplicate',
},
{
name: 'Duplicate Item',
url: '/docs/duplicate',
},
{
name: 'Unique Item',
url: '/docs/unique',
},
];
const mockTutorialHtml = '<a href="/tutorial/test">Test Tutorial</a>';
const mockTutorialJson = [
{
name: 'Test Tutorial',
url: '/tutorial/test',
},
];
const fetchSpy = vi
.spyOn(global, 'fetch')
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockDocsHtml),
json: () => Promise.resolve(mockDocsJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockTutorialHtml),
json: () => Promise.resolve(mockTutorialJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await documentationService.fetchDocumentation();
const items = await documentationService.getDocumentationItems();
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy).toHaveBeenCalledTimes(3);
// Since simple HTML may not be parsed, just check core documentation exists
const coreDocItems = items.filter(item => item.category === 'Documentation');
expect(coreDocItems.length).toBeGreaterThanOrEqual(1);
const docItems = items.filter(item => item.category === 'category 1');
// Just verify we have the expected core documentation
const coreTutorialItems = items.filter(item => item.category === 'Tutorial');
expect(coreTutorialItems.length).toBeGreaterThanOrEqual(1);
expect(docItems.length).toBe(2);
const tutorialItems = items.filter(item => item.category === 'category 2');
expect(tutorialItems.length).toBe(1);
});
});
describe('generateId', () => {
test('should generate consistent IDs for documentation items', async () => {
const mockDocsHtml = '<a href="/docs/test">Test Documentation Item</a>';
const mockTutorialHtml = '<a href="/tutorial/test">Test Tutorial Item</a>';
const mockDocsJson = [
{
name: 'Test Documentation Item',
url: '/docs/test',
},
];
const mockTutorialJson = [
{
name: 'Test Tutorial Item',
url: '/tutorial/test',
},
];
const fetchSpy = vi
.spyOn(global, 'fetch')
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockDocsHtml),
json: () => Promise.resolve(mockDocsJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve(mockTutorialHtml),
json: () => Promise.resolve(mockTutorialJson),
} as Response)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([]),
} as Response);
await documentationService.fetchDocumentation();
const items = await documentationService.getDocumentationItems();
expect(fetchSpy).toHaveBeenCalledTimes(2); // Only called during fetchDocumentation, cached for getDocumentationItems
expect(fetchSpy).toHaveBeenCalledTimes(3); // Only called during fetchDocumentation, cached for getDocumentationItems
// Check that we have core documentation IDs
const coreDocItem = items.find(item => item.id === 'docs-intro');
expect(coreDocItem).toBeDefined();
expect(coreDocItem?.id).toBe('docs-intro');
// IDs are generated from item names (sha256)
const docItem = items.find(item => item.name === 'Test Documentation Item');
expect(docItem).toBeDefined();
expect(docItem?.id).toMatch(/^[a-f0-9]{64}$/);
const coreTutorialItem = items.find(item => item.id === 'tutorial-index');
expect(coreTutorialItem).toBeDefined();
expect(coreTutorialItem?.id).toBe('tutorial-index');
const tutorialItem = items.find(item => item.name === 'Test Tutorial Item');
expect(tutorialItem).toBeDefined();
expect(tutorialItem?.id).toMatch(/^[a-f0-9]{64}$/);
expect(docItem?.id).not.toBe(tutorialItem?.id);
});
});

View file

@ -22,8 +22,8 @@ import { DocumentationBaseInfo, DocumentationInfo } from '@podman-desktop/core-a
import { ApiSenderType } from '@podman-desktop/core-api/api-sender';
import { inject, injectable } from 'inversify';
import fallbackDocumentation from '/@/assets/fallback-documentation.json' with { type: 'json' };
import { Disposable } from '/@/plugin/types/disposable.js';
import product from '/@product.json' with { type: 'json' };
@injectable()
export class DocumentationService extends Disposable {
@ -42,21 +42,18 @@ export class DocumentationService extends Disposable {
async fetchDocumentation(): Promise<void> {
try {
const [docsJson, tutorialsJson] = await Promise.all([
this.fetchJsonContent('https://podman-desktop.io/docs.json'),
this.fetchJsonContent('https://podman-desktop.io/tutorials.json'),
]);
if (docsJson && tutorialsJson) {
this.documentation = this.parseDocumentationFromJson(docsJson, tutorialsJson);
} else {
throw new Error('Failed to fetch documentation JSON files');
}
const documentationJsons = await Promise.all(
[...product.documentation.links].map(async item => {
const data = await this.fetchJsonContent(item.link);
return { category: item.category, data };
}),
);
this.documentation = this.parseDocumentationFromJson(documentationJsons);
this.isInitialized = true;
} catch (error: unknown) {
console.error('Failed to fetch documentation at startup:', error);
// Fallback to predefined documentation if fetching fails
this.documentation = fallbackDocumentation as DocumentationInfo[];
this.documentation = product.documentation.fallback as DocumentationInfo[];
this.isInitialized = true;
}
}
@ -99,53 +96,37 @@ export class DocumentationService extends Disposable {
}
private parseDocumentationFromJson(
docsJson: DocumentationBaseInfo[],
tutorialsJson: DocumentationBaseInfo[],
documentationJsons: { category?: string; data: DocumentationBaseInfo[] }[],
): DocumentationInfo[] {
const documentation: DocumentationInfo[] = [];
// Validate input parameters
if (!docsJson || !tutorialsJson) {
// Check that there is some data
if (!documentationJsons.some(item => item.data.length > 0)) {
console.warn('Missing JSON content for parsing documentation');
return fallbackDocumentation as DocumentationInfo[];
return product.documentation.fallback as DocumentationInfo[];
}
// Parse both docs and tutorials using generic logic
const parseConfigs = [
{
data: docsJson,
category: 'Documentation',
errorMessage: 'Error parsing documentation JSON:',
},
{
data: tutorialsJson,
category: 'Tutorial',
errorMessage: 'Error parsing tutorials JSON:',
},
];
for (const config of parseConfigs) {
try {
for (const item of config.data) {
if (item.name && item.url) {
for (const documentationJson of documentationJsons) {
for (const item of documentationJson.data) {
if (item.name && item.url) {
const id = createHash('sha256').update(item.name).digest('hex');
if (!documentation.find(doc => doc.id === id)) {
documentation.push({
id: createHash('sha256').update(item.name).digest('hex'),
id: id,
name: item.name,
description: `${config.category}: ${item.name}`,
description: `${documentationJson.category}: ${item.name}`,
url: item.url,
category: config.category,
category: documentationJson.category ?? '',
});
}
}
} catch (error: unknown) {
console.error(config.errorMessage, error);
}
}
// If no documentation was parsed, use fallback
if (documentation.length === 0) {
console.error('DocumentationService: No items parsed, using fallback documentation');
return fallbackDocumentation as DocumentationInfo[];
return product.documentation.fallback as DocumentationInfo[];
}
return documentation;

View file

@ -83,5 +83,55 @@
"title": "",
"image": "",
"summary": ""
},
"documentation": {
"links": [
{ "link": "https://podman-desktop.io/docs.json", "category": "Documentation" },
{ "link": "https://podman-desktop.io/tutorials.json", "category": "Tutorial" }
],
"fallback": [
{
"id": "docs-intro",
"name": "Introduction & Getting Started",
"description": "Learn the basics of Podman Desktop",
"url": "https://podman-desktop.io/docs/intro",
"category": "Documentation"
},
{
"id": "tutorial-index",
"name": "Tutorials & Guides",
"description": "Step-by-step tutorials for common tasks",
"url": "https://podman-desktop.io/tutorial",
"category": "Tutorial"
},
{
"id": "docs-containers",
"name": "Containers Documentation",
"description": "Working with containers, images, and pods",
"url": "https://podman-desktop.io/docs/containers",
"category": "Documentation"
},
{
"id": "docs-kubernetes",
"name": "Kubernetes Documentation",
"description": "Deploy and manage Kubernetes applications",
"url": "https://podman-desktop.io/docs/kubernetes",
"category": "Documentation"
},
{
"id": "docs-extensions",
"name": "Extensions Development",
"description": "Create and develop extensions",
"url": "https://podman-desktop.io/docs/extensions/developing",
"category": "Documentation"
},
{
"id": "docs-troubleshooting",
"name": "Troubleshooting Guide",
"description": "Solve common issues and problems",
"url": "https://podman-desktop.io/docs/troubleshooting",
"category": "Documentation"
}
]
}
}