Widget Documentation: Storybook, TypeDoc & Interactive Examples
Comprehensive widget documentation accelerates developer adoption, reduces support burden, and ensures consistent implementation across teams. When developers can reference live examples, interactive playgrounds, and detailed API documentation, they ship features faster with fewer bugs.
ChatGPT widget development demands exceptional documentation. Unlike traditional web components, widgets render in sandboxed environments with strict constraints: system fonts only, maximum 2 CTAs per card, no nested scrolling. Documentation must capture these nuances through interactive examples that developers can test in real-time, not static screenshots that quickly become outdated.
The best widget documentation combines three approaches: Storybook for component exploration and visual testing, TypeDoc for generated API reference documentation, and interactive sandboxes for hands-on experimentation. This triad gives developers confidence to implement widgets correctly on their first attempt, avoiding the costly iteration cycle of building, testing, and fixing OpenAI approval rejections.
Modern documentation tools automate much of this work. Storybook automatically generates component galleries from story files. TypeDoc extracts API documentation from JSDoc comments. CI/CD pipelines deploy documentation sites on every commit, ensuring developers always reference current implementation patterns rather than stale wiki pages.
This guide covers proven patterns for documenting ChatGPT widgets: Storybook configuration and story writing, TypeDoc integration for API reference generation, interactive playground setup, design system documentation, automated validation, and continuous deployment pipelines. Master these techniques, and you'll build documentation that developers actually use.
Storybook Setup for Widget Components
Storybook provides the ideal environment for developing and documenting ChatGPT widgets. It isolates components from your main application, allowing developers to test widgets with different props, states, and themes without deploying to production. Storybook's addon ecosystem extends functionality with accessibility checks, visual regression testing, and responsive design previews.
Component Story Format (CSF)
Component Story Format 3.0 simplifies widget story authoring with object-based configuration. Each story exports a default meta object defining component metadata and controls, plus named exports for individual component states. This structure enables automatic control inference, reducing boilerplate while maintaining type safety.
Essential Addons
Install Storybook addons tailored for widget development: @storybook/addon-a11y validates WCAG compliance (critical for OpenAI approval), @storybook/addon-viewport tests responsive layouts, and @storybook/addon-interactions documents user workflows through executable tests.
TypeScript Configuration
TypeScript-based Storybook configurations provide autocomplete and type checking for story files. Configure main.ts to import widget components, apply decorators, and register addons. This setup ensures stories remain synchronized with component interfaces as widget APIs evolve.
Here's a production-ready Storybook configuration for ChatGPT widget development:
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
import { mergeConfig } from 'vite';
import path from 'path';
const config: StorybookConfig = {
stories: [
'../src/widgets/**/*.stories.@(js|jsx|ts|tsx)',
'../src/components/**/*.stories.@(js|jsx|ts|tsx)',
],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-viewport',
'@storybook/addon-docs',
'storybook-dark-mode',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
defaultName: 'Documentation',
},
staticDirs: ['../public'],
async viteFinal(config) {
return mergeConfig(config, {
resolve: {
alias: {
'@': path.resolve(__dirname, '../src'),
'@widgets': path.resolve(__dirname, '../src/widgets'),
'@components': path.resolve(__dirname, '../src/components'),
},
},
define: {
'process.env.STORYBOOK': true,
},
});
},
// Preview head configuration for widget runtime simulation
previewHead: (head) => `
${head}
<style>
/* Simulate ChatGPT widget runtime constraints */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Helvetica Neue', Arial, sans-serif !important;
}
</style>
<script>
// Mock window.openai API for widget testing
window.openai = {
setWidgetState: (state) => {
console.log('Widget state updated:', state);
},
showToast: (message, type = 'info') => {
console.log(\`Toast [\${type}]:\`, message);
},
requestFullscreen: () => {
console.log('Fullscreen requested');
},
closeWidget: () => {
console.log('Widget closed');
},
};
</script>
`,
};
export default config;
Storybook Preview Configuration
Configure global decorators, parameters, and viewport definitions in preview.ts. Decorators wrap stories with common context providers (theme, router, mocked window.openai), ensuring widgets render correctly in isolation.
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import React from 'react';
import { themes } from '@storybook/theming';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
docs: {
theme: themes.dark,
},
viewport: {
viewports: {
chatgptMobile: {
name: 'ChatGPT Mobile',
styles: {
width: '390px',
height: '844px',
},
},
chatgptTablet: {
name: 'ChatGPT Tablet',
styles: {
width: '768px',
height: '1024px',
},
},
chatgptDesktop: {
name: 'ChatGPT Desktop',
styles: {
width: '1280px',
height: '720px',
},
},
},
},
backgrounds: {
default: 'chatgpt-dark',
values: [
{
name: 'chatgpt-dark',
value: '#2d2d2d',
},
{
name: 'chatgpt-light',
value: '#ffffff',
},
],
},
},
decorators: [
(Story) => (
<div style={{
padding: '1rem',
maxWidth: '600px',
margin: '0 auto',
}}>
<Story />
</div>
),
],
};
export default preview;
Writing Effective Widget Stories
Widget stories document component states, interaction patterns, and edge cases. Each story demonstrates a specific use case: default state, loading state, error state, or user interaction flow. Effective stories serve dual purposes - development playgrounds for engineers and visual specifications for designers.
Default Story Pattern
Start with a default story showcasing the widget's most common configuration. Use Storybook's args to define default props, then leverage automatic controls generation for interactive property testing. This pattern enables rapid experimentation without modifying component source code.
Interactive Story Pattern
Interactive stories capture user workflows through play functions. These executable scenarios document how users should interact with widgets: clicking buttons, entering form data, or navigating multi-step flows. Play functions double as visual regression tests, catching unintended UI changes during code reviews.
Here are comprehensive widget story examples covering typical ChatGPT widget scenarios:
// src/widgets/BookingCard/BookingCard.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect } from '@storybook/test';
import { BookingCard } from './BookingCard';
const meta: Meta<typeof BookingCard> = {
title: 'Widgets/BookingCard',
component: BookingCard,
parameters: {
layout: 'centered',
docs: {
description: {
component: `
# Booking Card Widget
Displays available booking slots with inline selection and confirmation.
Complies with OpenAI widget constraints:
- Maximum 2 primary CTAs
- System fonts only
- No nested scrolling
- WCAG AA contrast ratios
**Usage in ChatGPT:**
\`\`\`javascript
return {
structuredContent: { availableSlots, selectedSlot },
content: { text: "Here are available time slots..." },
_meta: {
widgetData: {
template: BookingCardTemplate,
state: { slots: availableSlots }
}
}
};
\`\`\`
`,
},
},
},
tags: ['autodocs'],
argTypes: {
slots: {
control: 'object',
description: 'Array of available booking slots',
},
selectedSlotId: {
control: 'text',
description: 'ID of currently selected slot',
},
onSlotSelect: {
action: 'slot-selected',
description: 'Callback fired when user selects a slot',
},
onConfirm: {
action: 'booking-confirmed',
description: 'Callback fired when user confirms booking',
},
loading: {
control: 'boolean',
description: 'Loading state for async operations',
},
},
};
export default meta;
type Story = StoryObj<typeof BookingCard>;
// Default story with available slots
export const Default: Story = {
args: {
slots: [
{
id: 'slot-1',
time: '9:00 AM',
instructor: 'Sarah Johnson',
spotsLeft: 3,
duration: '60 min',
},
{
id: 'slot-2',
time: '10:30 AM',
instructor: 'Mike Chen',
spotsLeft: 5,
duration: '60 min',
},
{
id: 'slot-3',
time: '12:00 PM',
instructor: 'Sarah Johnson',
spotsLeft: 1,
duration: '60 min',
},
],
},
};
// Selected slot state
export const WithSelection: Story = {
args: {
...Default.args,
selectedSlotId: 'slot-2',
},
};
// Loading state during booking confirmation
export const Loading: Story = {
args: {
...Default.args,
selectedSlotId: 'slot-1',
loading: true,
},
};
// Interactive workflow story
export const BookingFlow: Story = {
args: Default.args,
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// Step 1: Find and click first slot
const firstSlot = canvas.getByText('9:00 AM').closest('button');
expect(firstSlot).toBeInTheDocument();
await userEvent.click(firstSlot!);
// Step 2: Verify selection callback fired
expect(args.onSlotSelect).toHaveBeenCalledWith('slot-1');
// Step 3: Find and click confirm button
const confirmButton = canvas.getByText('Confirm Booking');
expect(confirmButton).toBeInTheDocument();
await userEvent.click(confirmButton);
// Step 4: Verify confirmation callback fired
expect(args.onConfirm).toHaveBeenCalledWith('slot-1');
},
};
// Error state (no available slots)
export const NoSlots: Story = {
args: {
slots: [],
},
};
// Mobile viewport test
export const Mobile: Story = {
args: Default.args,
parameters: {
viewport: {
defaultViewport: 'chatgptMobile',
},
},
};
TypeDoc Integration for API Reference
TypeDoc generates comprehensive API documentation from TypeScript source code and JSDoc comments. Unlike handwritten documentation that drifts from implementation, TypeDoc automatically extracts type signatures, parameter descriptions, and usage examples. This automation ensures API documentation remains synchronized with actual code.
JSDoc Annotation Standards
Effective TypeDoc documentation requires disciplined JSDoc annotations. Document component interfaces, prop types, function parameters, return values, and usage examples. Use @example tags to embed code snippets directly in generated documentation. Link related types with @see tags to build interconnected reference documentation.
Custom TypeDoc Themes
Customize TypeDoc's output with custom themes matching your design system. Configure navigation structure, code highlighting, and responsive layouts to create documentation that feels native to your brand. Deploy TypeDoc-generated sites alongside Storybook for comprehensive widget documentation.
Here's a fully-annotated widget component with TypeDoc-ready JSDoc comments:
// src/widgets/BookingCard/BookingCard.tsx
import React, { useState, useCallback } from 'react';
import styles from './BookingCard.module.css';
/**
* Represents a single booking time slot with instructor details
* and availability information.
*
* @interface BookingSlot
* @property {string} id - Unique identifier for the slot
* @property {string} time - Display time (e.g., "9:00 AM")
* @property {string} instructor - Instructor name
* @property {number} spotsLeft - Remaining available spots
* @property {string} duration - Class duration (e.g., "60 min")
*/
export interface BookingSlot {
id: string;
time: string;
instructor: string;
spotsLeft: number;
duration: string;
}
/**
* Props for the BookingCard widget component.
*
* @interface BookingCardProps
* @property {BookingSlot[]} slots - Array of available booking slots
* @property {string} [selectedSlotId] - ID of currently selected slot
* @property {(slotId: string) => void} onSlotSelect - Callback when slot is selected
* @property {(slotId: string) => void} onConfirm - Callback when booking is confirmed
* @property {boolean} [loading=false] - Loading state for async operations
*/
export interface BookingCardProps {
slots: BookingSlot[];
selectedSlotId?: string;
onSlotSelect: (slotId: string) => void;
onConfirm: (slotId: string) => void;
loading?: boolean;
}
/**
* BookingCard widget displays available booking slots with inline selection.
*
* Designed for ChatGPT widget runtime with OpenAI compliance:
* - Maximum 2 primary CTAs (Select + Confirm)
* - System fonts only (no custom typography)
* - No nested scrolling (flat card layout)
* - WCAG AA contrast ratios
*
* @component
* @param {BookingCardProps} props - Component props
* @returns {JSX.Element} Rendered booking card widget
*
* @example
* // Basic usage in MCP server tool handler
* ```tsx
* const BookingCardWidget = () => (
* <BookingCard
* slots={availableSlots}
* onSlotSelect={(id) => window.openai.setWidgetState({ selectedSlot: id })}
* onConfirm={(id) => window.openai.setWidgetState({ confirmedSlot: id })}
* />
* );
* ```
*
* @example
* // With loading state during async booking
* ```tsx
* const [loading, setLoading] = useState(false);
*
* const handleConfirm = async (slotId: string) => {
* setLoading(true);
* await bookSlot(slotId);
* setLoading(false);
* window.openai.setWidgetState({ booked: true });
* };
*
* return (
* <BookingCard
* slots={slots}
* loading={loading}
* onConfirm={handleConfirm}
* />
* );
* ```
*
* @see {@link BookingSlot} for slot data structure
* @see {@link https://openai.com/index/introducing-apps-in-chatgpt/ | OpenAI Apps Documentation}
*/
export const BookingCard: React.FC<BookingCardProps> = ({
slots,
selectedSlotId,
onSlotSelect,
onConfirm,
loading = false,
}) => {
/**
* Handles slot selection with validation and callback invocation.
*
* @param {string} slotId - ID of selected slot
* @fires onSlotSelect
*/
const handleSlotClick = useCallback((slotId: string) => {
if (loading) return; // Prevent interaction during loading
onSlotSelect(slotId);
}, [loading, onSlotSelect]);
/**
* Handles booking confirmation with validation.
*
* @fires onConfirm
*/
const handleConfirmClick = useCallback(() => {
if (!selectedSlotId || loading) return;
onConfirm(selectedSlotId);
}, [selectedSlotId, loading, onConfirm]);
// Empty state when no slots available
if (slots.length === 0) {
return (
<div className={styles.emptyState}>
<p>No available slots at this time.</p>
<p>Please try a different day or time.</p>
</div>
);
}
return (
<div className={styles.bookingCard}>
<div className={styles.slotList} role="list">
{slots.map((slot) => (
<button
key={slot.id}
type="button"
role="listitem"
className={`${styles.slot} ${
selectedSlotId === slot.id ? styles.selected : ''
}`}
onClick={() => handleSlotClick(slot.id)}
disabled={loading}
aria-pressed={selectedSlotId === slot.id}
aria-label={`Select ${slot.time} class with ${slot.instructor}, ${slot.spotsLeft} spots left`}
>
<div className={styles.slotTime}>{slot.time}</div>
<div className={styles.slotDetails}>
<span className={styles.instructor}>{slot.instructor}</span>
<span className={styles.duration}>{slot.duration}</span>
</div>
<div className={styles.availability}>
{slot.spotsLeft} {slot.spotsLeft === 1 ? 'spot' : 'spots'} left
</div>
</button>
))}
</div>
{selectedSlotId && (
<button
type="button"
className={styles.confirmButton}
onClick={handleConfirmClick}
disabled={loading}
aria-busy={loading}
>
{loading ? 'Confirming...' : 'Confirm Booking'}
</button>
)}
</div>
);
};
TypeDoc Configuration
Configure TypeDoc to generate documentation from your widget source code. Specify entry points, output directories, theme customization, and plugin integrations. This configuration ensures consistent documentation generation across your entire widget library.
// typedoc.json
{
"entryPoints": ["src/widgets", "src/components"],
"entryPointStrategy": "expand",
"out": "docs-site/api",
"exclude": [
"**/*.test.ts",
"**/*.test.tsx",
"**/*.stories.tsx",
"**/node_modules/**"
],
"excludePrivate": true,
"excludeProtected": false,
"includeVersion": true,
"readme": "README.md",
"plugin": ["typedoc-plugin-markdown"],
"theme": "default",
"categorizeByGroup": true,
"defaultCategory": "Widgets",
"categoryOrder": [
"Widgets",
"Components",
"Hooks",
"Utilities",
"Types"
],
"sort": ["source-order"],
"searchInComments": true,
"validation": {
"notExported": true,
"invalidLink": true,
"notDocumented": true
},
"name": "ChatGPT Widget API Reference",
"navigation": {
"includeCategories": true,
"includeGroups": true
}
}
Interactive Playground Implementation
Interactive playgrounds enable developers to test widget code without local setup. Embed CodeSandbox or StackBlitz instances directly in documentation, pre-configured with widget runtime mocks and example data. Developers modify code in-browser, see instant results, and copy working implementations to production.
CodeSandbox Embed Configuration
CodeSandbox embeds support live code editing with hot module reloading. Configure embeds to auto-run on load, hide irrelevant file tree sections, and highlight key code sections. Pre-populate sandboxes with widget boilerplate, reducing friction from "empty canvas" to "working example."
Here's an interactive playground component that embeds live widget examples:
// src/docs/components/InteractivePlayground.tsx
import React, { useState, useEffect } from 'react';
import { UnControlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/dracula.css';
import 'codemirror/mode/javascript/javascript';
/**
* Props for the InteractivePlayground component.
*
* @interface InteractivePlaygroundProps
* @property {string} initialCode - Initial code to display in editor
* @property {string} [title] - Playground title
* @property {Record<string, any>} [dependencies] - External dependencies to inject
*/
interface InteractivePlaygroundProps {
initialCode: string;
title?: string;
dependencies?: Record<string, any>;
}
/**
* Interactive code playground for testing ChatGPT widgets in-browser.
*
* Features:
* - Live code editing with syntax highlighting
* - Hot reload preview pane
* - Error boundary with helpful messages
* - window.openai API mock injection
*
* @component
* @param {InteractivePlaygroundProps} props - Component props
* @returns {JSX.Element} Interactive playground component
*
* @example
* ```tsx
* <InteractivePlayground
* title="Booking Card Example"
* initialCode={`
* const slots = [
* { id: '1', time: '9:00 AM', instructor: 'Sarah', spotsLeft: 3 }
* ];
*
* function App() {
* return <BookingCard slots={slots} />;
* }
* `}
* />
* ```
*/
export const InteractivePlayground: React.FC<InteractivePlaygroundProps> = ({
initialCode,
title = 'Widget Playground',
dependencies = {},
}) => {
const [code, setCode] = useState(initialCode);
const [output, setOutput] = useState<string>('');
const [error, setError] = useState<string | null>(null);
// Mock window.openai API for widget testing
const windowOpenaiMock = `
window.openai = {
setWidgetState: (state) => {
console.log('Widget state updated:', state);
window.postMessage({ type: 'widget-state', state }, '*');
},
showToast: (message, type = 'info') => {
console.log(\`Toast [\${type}]:\`, message);
},
requestFullscreen: () => {
console.log('Fullscreen requested');
},
closeWidget: () => {
console.log('Widget closed');
},
};
`;
useEffect(() => {
try {
// Create isolated execution context
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
const iframeWindow = iframe.contentWindow;
if (!iframeWindow) {
throw new Error('Failed to create execution context');
}
// Inject dependencies and mocks
iframeWindow.eval(windowOpenaiMock);
// Execute user code
const result = iframeWindow.eval(`
(function() {
${code}
})()
`);
setOutput(String(result || 'Code executed successfully'));
setError(null);
// Cleanup
document.body.removeChild(iframe);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
setOutput('');
}
}, [code]);
return (
<div className="playground-container">
<div className="playground-header">
<h3>{title}</h3>
<button
onClick={() => setCode(initialCode)}
className="reset-button"
>
Reset Code
</button>
</div>
<div className="playground-layout">
<div className="editor-pane">
<h4>Code Editor</h4>
<CodeMirror
value={code}
options={{
mode: 'javascript',
theme: 'dracula',
lineNumbers: true,
lineWrapping: true,
}}
onChange={(editor, data, value) => {
setCode(value);
}}
/>
</div>
<div className="preview-pane">
<h4>Output</h4>
{error ? (
<div className="error-message">
<strong>Error:</strong> {error}
</div>
) : (
<div className="output-message">{output}</div>
)}
</div>
</div>
<div className="playground-footer">
<p>
<strong>Tip:</strong> Open browser console (F12) to see{' '}
<code>window.openai</code> API calls.
</p>
</div>
</div>
);
};
Design System Documentation
Design systems document reusable patterns, component guidelines, and visual specifications. For ChatGPT widgets, design systems must capture OpenAI's strict constraints: system fonts only, maximum 2 CTAs per card, WCAG AA contrast ratios, and no nested scrolling. Codify these rules in documentation to prevent approval rejections.
Component Library Documentation
Document each widget component with usage guidelines, do/don't examples, and accessibility requirements. Include visual examples of correct implementations alongside anti-patterns that violate OpenAI constraints. This dual approach teaches developers what to build and what to avoid.
Here's a design system generator that creates comprehensive widget documentation:
// scripts/generate-design-system-docs.ts
import fs from 'fs/promises';
import path from 'path';
import glob from 'glob';
/**
* Represents a documented design pattern or component.
*
* @interface DesignPattern
* @property {string} name - Pattern name
* @property {string} description - Pattern description
* @property {string[]} examples - Code examples
* @property {string[]} rules - Design rules and constraints
* @property {string[]} antiPatterns - What NOT to do
*/
interface DesignPattern {
name: string;
description: string;
category: string;
examples: string[];
rules: string[];
antiPatterns: string[];
}
/**
* Extracts design patterns from widget source files.
*
* Scans JSDoc comments for special @pattern annotations and compiles
* them into a structured design system documentation.
*
* @param {string} sourceDir - Source directory to scan
* @returns {Promise<DesignPattern[]>} Extracted patterns
*/
async function extractPatterns(sourceDir: string): Promise<DesignPattern[]> {
const patterns: DesignPattern[] = [];
const files = glob.sync(`${sourceDir}/**/*.tsx`);
for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
// Extract @pattern annotations from JSDoc comments
const patternRegex = /@pattern\s+(\w+)\s*\n\s*\*\s*(.+?)(?=\n\s*\*\s*@|\n\s*\*\/)/gs;
let match;
while ((match = patternRegex.exec(content)) !== null) {
const [, name, description] = match;
// Extract associated code example
const exampleRegex = new RegExp(
`@example.*?\\n.*?\`\`\`tsx\\n([\\s\\S]+?)\`\`\``,
'g'
);
const exampleMatch = exampleRegex.exec(content);
patterns.push({
name,
description: description.trim(),
category: path.basename(path.dirname(file)),
examples: exampleMatch ? [exampleMatch[1].trim()] : [],
rules: [],
antiPatterns: [],
});
}
}
return patterns;
}
/**
* Generates Markdown documentation from design patterns.
*
* @param {DesignPattern[]} patterns - Patterns to document
* @returns {string} Generated Markdown content
*/
function generateMarkdown(patterns: DesignPattern[]): string {
const categorized = patterns.reduce((acc, pattern) => {
if (!acc[pattern.category]) {
acc[pattern.category] = [];
}
acc[pattern.category].push(pattern);
return acc;
}, {} as Record<string, DesignPattern[]>);
let markdown = `# ChatGPT Widget Design System\n\n`;
markdown += `Last updated: ${new Date().toLocaleDateString()}\n\n`;
markdown += `This design system documents proven patterns for building ChatGPT-compliant widgets.\n\n`;
markdown += `## OpenAI Compliance Requirements\n\n`;
markdown += `All widgets MUST adhere to these constraints:\n\n`;
markdown += `- **System fonts only** (no custom typography)\n`;
markdown += `- **Maximum 2 primary CTAs** per inline card\n`;
markdown += `- **No nested scrolling** (flat layouts only)\n`;
markdown += `- **WCAG AA contrast ratios** (4.5:1 for normal text)\n`;
markdown += `- **No custom domains** (frame_domains subject to strict review)\n\n`;
for (const [category, categoryPatterns] of Object.entries(categorized)) {
markdown += `## ${category}\n\n`;
for (const pattern of categoryPatterns) {
markdown += `### ${pattern.name}\n\n`;
markdown += `${pattern.description}\n\n`;
if (pattern.rules.length > 0) {
markdown += `**Rules:**\n\n`;
pattern.rules.forEach((rule) => {
markdown += `- ${rule}\n`;
});
markdown += `\n`;
}
if (pattern.examples.length > 0) {
markdown += `**Example:**\n\n`;
pattern.examples.forEach((example) => {
markdown += `\`\`\`tsx\n${example}\n\`\`\`\n\n`;
});
}
if (pattern.antiPatterns.length > 0) {
markdown += `**❌ Anti-Patterns (Do NOT use):**\n\n`;
pattern.antiPatterns.forEach((antiPattern) => {
markdown += `- ${antiPattern}\n`;
});
markdown += `\n`;
}
}
}
return markdown;
}
/**
* Main execution function.
*/
async function main() {
const sourceDir = path.join(__dirname, '../src/widgets');
const outputPath = path.join(__dirname, '../docs-site/design-system.md');
console.log('Extracting design patterns from:', sourceDir);
const patterns = await extractPatterns(sourceDir);
console.log(`Found ${patterns.length} patterns`);
const markdown = generateMarkdown(patterns);
await fs.writeFile(outputPath, markdown, 'utf-8');
console.log('Design system documentation generated:', outputPath);
}
main().catch(console.error);
Documentation Testing & Validation
Documentation testing validates that code examples execute correctly, links resolve to valid targets, and documentation remains synchronized with implementation. Automated validation catches documentation drift before developers encounter broken examples in production.
Link Validation
Validate internal documentation links during CI/CD pipeline execution. Broken links frustrate developers and undermine documentation credibility. Use tools like markdown-link-check to scan documentation for 404s, ensuring every reference resolves correctly.
Code Example Testing
Extract code examples from documentation and execute them as unit tests. This approach guarantees examples remain functional as widget APIs evolve. Tools like markdown-code-runner automate example extraction and execution, failing CI builds when documentation examples break.
Here's a comprehensive documentation validator with automated link checking and code validation:
// scripts/validate-documentation.ts
import fs from 'fs/promises';
import path from 'path';
import glob from 'glob';
import fetch from 'node-fetch';
import { marked } from 'marked';
/**
* Validation result for a single documentation file.
*
* @interface ValidationResult
* @property {string} file - File path
* @property {string[]} errors - Validation errors
* @property {string[]} warnings - Validation warnings
*/
interface ValidationResult {
file: string;
errors: string[];
warnings: string[];
}
/**
* Extracts all links from Markdown content.
*
* @param {string} content - Markdown content
* @returns {string[]} Array of link URLs
*/
function extractLinks(content: string): string[] {
const links: string[] = [];
const tokens = marked.lexer(content);
function walkTokens(tokens: marked.Token[]) {
for (const token of tokens) {
if (token.type === 'link') {
links.push(token.href);
}
if ('tokens' in token && token.tokens) {
walkTokens(token.tokens);
}
}
}
walkTokens(tokens);
return links;
}
/**
* Validates that a link resolves correctly.
*
* @param {string} link - Link URL
* @param {string} baseDir - Base directory for relative links
* @returns {Promise<boolean>} True if link is valid
*/
async function validateLink(link: string, baseDir: string): Promise<boolean> {
// Internal link (relative path)
if (link.startsWith('/') || link.startsWith('.')) {
const absolutePath = path.resolve(baseDir, link);
try {
await fs.access(absolutePath);
return true;
} catch {
return false;
}
}
// External link (HTTP/HTTPS)
if (link.startsWith('http://') || link.startsWith('https://')) {
try {
const response = await fetch(link, { method: 'HEAD', timeout: 5000 });
return response.ok;
} catch {
return false;
}
}
// Anchor link (skip validation)
if (link.startsWith('#')) {
return true;
}
return false;
}
/**
* Extracts code blocks from Markdown content.
*
* @param {string} content - Markdown content
* @returns {Array<{language: string, code: string}>} Code blocks
*/
function extractCodeBlocks(content: string): Array<{ language: string; code: string }> {
const blocks: Array<{ language: string; code: string }> = [];
const codeBlockRegex = /```(\w+)\n([\s\S]+?)```/g;
let match;
while ((match = codeBlockRegex.exec(content)) !== null) {
blocks.push({
language: match[1],
code: match[2],
});
}
return blocks;
}
/**
* Validates a single documentation file.
*
* @param {string} filePath - File path to validate
* @returns {Promise<ValidationResult>} Validation result
*/
async function validateFile(filePath: string): Promise<ValidationResult> {
const result: ValidationResult = {
file: filePath,
errors: [],
warnings: [],
};
const content = await fs.readFile(filePath, 'utf-8');
const baseDir = path.dirname(filePath);
// Validate frontmatter
if (!content.startsWith('---')) {
result.errors.push('Missing frontmatter metadata');
}
// Validate required frontmatter fields
const frontmatterMatch = content.match(/^---\n([\s\S]+?)\n---/);
if (frontmatterMatch) {
const frontmatter = frontmatterMatch[1];
const requiredFields = ['title', 'meta_description', 'keywords'];
for (const field of requiredFields) {
if (!frontmatter.includes(`${field}:`)) {
result.errors.push(`Missing required frontmatter field: ${field}`);
}
}
}
// Validate links
const links = extractLinks(content);
for (const link of links) {
const isValid = await validateLink(link, baseDir);
if (!isValid) {
result.errors.push(`Broken link: ${link}`);
}
}
// Validate code blocks
const codeBlocks = extractCodeBlocks(content);
for (const block of codeBlocks) {
// Check for common anti-patterns in code examples
if (block.language === 'typescript' || block.language === 'tsx') {
if (block.code.includes('any')) {
result.warnings.push('Code example uses "any" type (consider specific types)');
}
if (block.code.includes('console.log') && !block.code.includes('window.openai')) {
result.warnings.push('Code example contains console.log (consider removing)');
}
}
}
return result;
}
/**
* Main validation function.
*/
async function main() {
const docsDir = path.join(__dirname, '../docs-site');
const files = glob.sync(`${docsDir}/**/*.md`);
console.log(`Validating ${files.length} documentation files...\n`);
const results: ValidationResult[] = [];
for (const file of files) {
const result = await validateFile(file);
results.push(result);
if (result.errors.length > 0 || result.warnings.length > 0) {
console.log(`📄 ${path.relative(docsDir, file)}`);
result.errors.forEach((error) => console.log(` ❌ ${error}`));
result.warnings.forEach((warning) => console.log(` ⚠️ ${warning}`));
console.log('');
}
}
const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
console.log(`\nValidation complete:`);
console.log(` Files: ${files.length}`);
console.log(` Errors: ${totalErrors}`);
console.log(` Warnings: ${totalWarnings}`);
if (totalErrors > 0) {
process.exit(1);
}
}
main().catch(console.error);
CI/CD Documentation Pipeline
Continuous deployment pipelines automate documentation publishing. On every commit to main, build Storybook, generate TypeDoc reference documentation, validate links, and deploy to hosting. This automation ensures documentation stays current without manual intervention.
GitHub Actions Workflow
Configure GitHub Actions to build and deploy documentation sites. Use separate jobs for Storybook, TypeDoc, and validation, running them in parallel for faster builds. Deploy artifacts to GitHub Pages, Netlify, or Vercel for zero-config hosting.
Here's a production-ready CI/CD pipeline for automated documentation deployment:
# .github/workflows/deploy-docs.yml
name: Deploy Documentation
on:
push:
branches:
- main
paths:
- 'src/widgets/**'
- 'src/components/**'
- 'docs-site/**'
- '.storybook/**'
pull_request:
branches:
- main
workflow_dispatch:
jobs:
validate:
name: Validate Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run documentation validator
run: npm run validate:docs
- name: Check for broken links
run: npm run check:links
build-storybook:
name: Build Storybook
runs-on: ubuntu-latest
needs: validate
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Storybook
run: npm run build:storybook
- name: Upload Storybook artifact
uses: actions/upload-artifact@v4
with:
name: storybook-build
path: storybook-static/
retention-days: 7
build-typedoc:
name: Build TypeDoc
runs-on: ubuntu-latest
needs: validate
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate TypeDoc
run: npm run build:typedoc
- name: Upload TypeDoc artifact
uses: actions/upload-artifact@v4
with:
name: typedoc-build
path: docs-site/api/
retention-days: 7
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
needs: [build-storybook, build-typedoc]
if: github.ref == 'refs/heads/main'
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download Storybook artifact
uses: actions/download-artifact@v4
with:
name: storybook-build
path: dist/storybook
- name: Download TypeDoc artifact
uses: actions/download-artifact@v4
with:
name: typedoc-build
path: dist/api
- name: Create index page
run: |
cat > dist/index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChatGPT Widget Documentation</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 50px auto; padding: 0 20px; }
h1 { color: #333; }
.docs-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-top: 40px; }
.docs-card { border: 1px solid #ddd; border-radius: 8px; padding: 24px; text-decoration: none; color: #333; transition: box-shadow 0.2s; }
.docs-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.docs-card h2 { margin: 0 0 12px 0; font-size: 20px; }
.docs-card p { margin: 0; color: #666; font-size: 14px; }
</style>
</head>
<body>
<h1>ChatGPT Widget Documentation</h1>
<p>Comprehensive documentation for building production-ready ChatGPT widgets.</p>
<div class="docs-grid">
<a href="/storybook" class="docs-card">
<h2>📚 Storybook</h2>
<p>Interactive component library with live examples and controls</p>
</a>
<a href="/api" class="docs-card">
<h2>📖 API Reference</h2>
<p>Complete TypeDoc-generated API documentation</p>
</a>
</div>
</body>
</html>
EOF
- name: Setup GitHub Pages
uses: actions/configure-pages@v4
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: dist/
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4
Conclusion
Comprehensive widget documentation accelerates development velocity, reduces support burden, and ensures OpenAI approval on first submission. The combination of Storybook (interactive component exploration), TypeDoc (auto-generated API reference), and interactive playgrounds (hands-on experimentation) creates documentation that developers trust and reference daily.
Automated validation and continuous deployment pipelines keep documentation synchronized with code. Documentation drift becomes impossible when CI/CD validates links, tests code examples, and deploys updates automatically. This investment in documentation infrastructure pays dividends throughout your widget library's lifecycle.
Ready to build production-ready ChatGPT apps? Start your free trial with MakeAIHQ and generate OpenAI-compliant widgets in minutes, not weeks.
Related Resources
- ChatGPT Widget Development Complete Guide - Master React patterns and window.openai API
- MCP Server Development Complete Guide - Build robust backend infrastructure
- ChatGPT App Testing & QA Complete Guide - Implement comprehensive testing strategies
- Integration Testing for MCP Servers - Validate cross-service interactions
- ChatGPT App Store Submission Guide - Navigate OpenAI approval process
External Resources
- Storybook Documentation - Official Storybook guides and API reference
- TypeDoc Documentation - TypeDoc configuration and usage
- Design Systems Guide - Best practices for design system documentation
About MakeAIHQ: The no-code platform for building ChatGPT apps. From zero to ChatGPT App Store in 48 hours - no coding required.