diff --git a/.claude/skills/docusaurus-config/README.md b/.claude/skills/docusaurus-config/README.md new file mode 100644 index 000000000..53aa1b61a --- /dev/null +++ b/.claude/skills/docusaurus-config/README.md @@ -0,0 +1,14 @@ +# Docusaurus Config + +Work with, validate, and modify Docusaurus project configuration + +## Structure + +- `SKILL.md` - Main skill instructions +- `references/` - Detailed documentation loaded as needed +- `scripts/` - Executable code for deterministic operations +- `assets/` - Templates, images, or other resources + +## Usage + +This skill is automatically discovered by Claude when relevant to the task. diff --git a/.claude/skills/docusaurus-config/SKILL.md b/.claude/skills/docusaurus-config/SKILL.md new file mode 100644 index 000000000..2695108de --- /dev/null +++ b/.claude/skills/docusaurus-config/SKILL.md @@ -0,0 +1,60 @@ +--- +name: docusaurus-config +# IMPORTANT: Keep description on ONE line for Claude Code compatibility +# prettier-ignore +description: Use when working with docusaurus.config.js/ts files to validate or modify Docusaurus configuration +--- + +# Docusaurus Config + +## Quick Start + +Configuration lives in `docusaurus.config.js` or `docusaurus.config.ts` at project root. + +```typescript +import { Config } from "@docusaurus/types"; + +const config: Config = { + title: "My Site", // Required + url: "https://example.com", // Required, no trailing / + baseUrl: "/", // Required, must start and end with / + + favicon: "img/favicon.ico", + organizationName: "my-org", + projectName: "my-project", + + presets: [ + [ + "@docusaurus/preset-classic", + { + /* options */ + }, + ], + ], + themeConfig: { + /* theme config */ + }, + customFields: { + /* unknown fields go here */ + }, +}; + +export default config; +``` + +## Core Principles + +- **Required**: `title`, `url`, `baseUrl` are mandatory +- **Custom fields**: Unknown fields must use `customFields` object +- **Validation**: `url` no trailing slash, `baseUrl` must be `/path/` +- **Plugins/themes**: Use string or `[name, options]` array format + +## Common Tasks + +**Before editing**: Read current config to preserve format (JS/TS, ESM/CommonJS) + +**After editing**: Verify required fields, URL formats, and restart dev server + +## Reference Files + +See [references/detailed-guide.md](references/detailed-guide.md) for comprehensive examples diff --git a/.claude/skills/docusaurus-config/references/detailed-guide.md b/.claude/skills/docusaurus-config/references/detailed-guide.md new file mode 100644 index 000000000..c51dab234 --- /dev/null +++ b/.claude/skills/docusaurus-config/references/detailed-guide.md @@ -0,0 +1,318 @@ +# Docusaurus Configuration - Detailed Guide + +## Configuration File Structure + +Docusaurus configuration can be in multiple formats: + +### TypeScript (Recommended) + +```typescript +import { Config } from "@docusaurus/types"; +import { themes as prismThemes } from "prism-react-renderer"; + +const config: Config = { + // Configuration here +}; + +export default config; +``` + +### JavaScript (ESM) + +```javascript +export default { + // Configuration here +}; +``` + +### JavaScript (CommonJS) + +```javascript +module.exports = { + // Configuration here +}; +``` + +### Async Configuration + +```typescript +export default async function createConfig(): Promise { + // Import ESM-only packages + const mdxMermaid = await import("mdx-mermaid"); + + return { + // Configuration here + }; +} +``` + +## Required Fields + +### title + +- Type: `string` +- Required: Yes +- Description: Site title for metadata and browser tabs + +### url + +- Type: `string` +- Required: Yes +- Format: Must not end with trailing slash +- Example: `'https://example.com'` +- Description: Full URL where site will be hosted + +### baseUrl + +- Type: `string` +- Required: Yes +- Format: Must start and end with `/` +- Example: `'/'` or `'/docs/'` +- Description: Path where site is served from + +## Common Optional Fields + +### Site Metadata + +- `tagline`: Short description of your site +- `favicon`: Path to favicon (relative to static folder) +- `organizationName`: GitHub org/user name (for deployment) +- `projectName`: GitHub repo name (for deployment) + +### Deployment + +- `deploymentBranch`: Branch for deployment (default: 'gh-pages') +- `trailingSlash`: Boolean or undefined for trailing slash handling + +### Internationalization + +```typescript +i18n: { + defaultLocale: 'en', + locales: ['en', 'fr', 'es'], +} +``` + +## Themes Configuration + +### Using Presets (Recommended) + +```typescript +presets: [ + [ + '@docusaurus/preset-classic', + { + docs: { + sidebarPath: './sidebars.ts', + editUrl: 'https://github.com/user/repo/tree/main/', + }, + blog: { + showReadingTime: true, + }, + theme: { + customCss: './src/css/custom.css', + }, + }, + ], +], +``` + +### Direct Theme Configuration + +```typescript +themes: ['@docusaurus/theme-classic'], +themeConfig: { + navbar: { + title: 'My Site', + logo: { + alt: 'My Site Logo', + src: 'img/logo.svg', + }, + items: [ + {to: '/docs/intro', label: 'Docs', position: 'left'}, + ], + }, + footer: { + style: 'dark', + links: [], + copyright: `Copyright © ${new Date().getFullYear()}`, + }, +}, +``` + +## Plugins + +### Adding Plugins + +String format (no options): + +```typescript +plugins: ['@docusaurus/plugin-debug'], +``` + +Array format (with options): + +```typescript +plugins: [ + [ + '@docusaurus/plugin-content-docs', + { + id: 'community', + path: 'community', + routeBasePath: 'community', + }, + ], +], +``` + +### Shorthand Notation + +Official Docusaurus packages can use shorthand: + +- `'classic'` → `'@docusaurus/preset-classic'` +- `'plugin-debug'` → `'@docusaurus/plugin-debug'` + +## Custom Fields + +Unknown fields cause validation errors. Use `customFields` for custom data: + +```typescript +customFields: { + apiKey: process.env.API_KEY, + customValue: 'my-value', + complexData: { + nested: true, + }, +} +``` + +Access in components: + +```typescript +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; + +function MyComponent() { + const {siteConfig} = useDocusaurusContext(); + const apiKey = siteConfig.customFields.apiKey; + + return
{apiKey}
; +} +``` + +## Validation Checklist + +When modifying config, verify: + +1. **Required fields present**: + + - ✅ `title` exists + - ✅ `url` exists and has no trailing slash + - ✅ `baseUrl` exists and starts/ends with `/` + +2. **Plugins and themes**: + + - ✅ Use proper package names or shorthand + - ✅ Options passed as second array element + - ✅ No duplicate plugins + +3. **Custom data**: + + - ✅ Unknown fields in `customFields` object + - ✅ No direct custom properties at root level + +4. **File format**: + + - ✅ Valid JS/TS syntax + - ✅ Proper export (ESM or CommonJS) + - ✅ TypeScript types imported if using TS + +5. **Testing**: + - ✅ Restart dev server after changes + - ✅ Build succeeds: `npm run build` + - ✅ No console errors + +## Common Patterns + +### Multi-Instance Docs + +```typescript +plugins: [ + [ + '@docusaurus/plugin-content-docs', + { + id: 'product', + path: 'product', + routeBasePath: 'product', + }, + ], + [ + '@docusaurus/plugin-content-docs', + { + id: 'community', + path: 'community', + routeBasePath: 'community', + }, + ], +], +``` + +### Environment Variables + +```typescript +const config: Config = { + url: process.env.SITE_URL || "https://localhost:3000", + customFields: { + apiEndpoint: process.env.API_ENDPOINT, + }, +}; +``` + +### Babel Customization + +Create `babel.config.js`: + +```javascript +module.exports = { + presets: [require.resolve("@docusaurus/babel/preset")], + plugins: [ + // Your custom Babel plugins + ], +}; +``` + +## Troubleshooting + +### Common Errors + +**"url must not have a trailing slash"** + +- Fix: Change `url: 'https://example.com/'` to `url: 'https://example.com'` + +**"baseUrl must start and end with /"** + +- Fix: Change `baseUrl: 'docs'` to `baseUrl: '/docs/'` + +**"Unknown field 'myField'"** + +- Fix: Move to `customFields: { myField: 'value' }` + +**"Cannot find module '@docusaurus/types'"** + +- Fix: Run `npm install --save-dev @docusaurus/types` + +### Best Practices + +1. Use TypeScript for better autocomplete and type checking +2. Always read existing config before modifying +3. Preserve file format (don't convert ESM to CommonJS) +4. Test locally before deploying +5. Use environment variables for deployment-specific values +6. Document custom fields with comments +7. Keep config organized with clear sections + +## Additional Resources + +- [Official Configuration API](https://docusaurus.io/docs/api/docusaurus-config) +- [Plugin Configuration](https://docusaurus.io/docs/using-plugins) +- [Theme Configuration](https://docusaurus.io/docs/using-themes) +- [Deployment Guide](https://docusaurus.io/docs/deployment) diff --git a/.claude/skills/docusaurus-documentation/README.md b/.claude/skills/docusaurus-documentation/README.md new file mode 100644 index 000000000..fde07c87b --- /dev/null +++ b/.claude/skills/docusaurus-documentation/README.md @@ -0,0 +1,14 @@ +# Docusaurus Docs + +Look up information from the latest Docusaurus documentation at https://docusaurus.io/docs + +## Structure + +- `SKILL.md` - Main skill instructions +- `references/` - Detailed documentation loaded as needed +- `scripts/` - Executable code for deterministic operations +- `assets/` - Templates, images, or other resources + +## Usage + +This skill is automatically discovered by Claude when relevant to the task. diff --git a/.claude/skills/docusaurus-documentation/SKILL.md b/.claude/skills/docusaurus-documentation/SKILL.md new file mode 100644 index 000000000..cb43622b0 --- /dev/null +++ b/.claude/skills/docusaurus-documentation/SKILL.md @@ -0,0 +1,72 @@ +--- +name: docusaurus-documentation +# IMPORTANT: Keep description on ONE line for Claude Code compatibility +# prettier-ignore +description: Use when looking up information from the latest Docusaurus documentation at https://docusaurus.io/docs +--- + +# Docusaurus Docs + +## Quick Start + +When the user asks about Docusaurus features, configuration, or best practices, use the WebFetch tool to look up information from the official Docusaurus documentation. + +```typescript +// Use WebFetch to access Docusaurus documentation +WebFetch({ + url: "https://docusaurus.io/docs/[topic]", + prompt: "What does this page say about [specific question]?", +}); +``` + +## Core Principles + +- Always use WebFetch to get the latest documentation from https://docusaurus.io/docs +- Common documentation paths: /configuration, /api, /guides, /creating-pages, /markdown-features +- Start with the main docs page if you're unsure of the exact path + +## Common Patterns + +### Looking Up Configuration Options + +When users ask about docusaurus.config.js settings, theming, or plugins: + +1. Use WebFetch with https://docusaurus.io/docs/api/docusaurus-config +2. For specific plugins, check https://docusaurus.io/docs/api/plugins/[plugin-name] +3. For theming, use https://docusaurus.io/docs/styling-layout + +### Finding Feature Documentation + +For markdown features, MDX, or content creation: + +- Markdown features: https://docusaurus.io/docs/markdown-features +- MDX and React: https://docusaurus.io/docs/markdown-features/react +- Docs organization: https://docusaurus.io/docs/create-doc + +## Reference Files + +For detailed documentation, see: + +- [references/](references/) - Cached documentation and guides + +## Notes + +- Docusaurus documentation is frequently updated; always fetch latest from https://docusaurus.io/docs +- When uncertain about the exact URL path, start with the main docs page and search +- For version-specific features, check the version selector on the docs site + + diff --git a/.claude/skills/docusaurus-migration/README.md b/.claude/skills/docusaurus-migration/README.md new file mode 100644 index 000000000..7b0ebd6dc --- /dev/null +++ b/.claude/skills/docusaurus-migration/README.md @@ -0,0 +1,14 @@ +# Docusaurus V2 To V3 Migration + +Assist with migrating Docusaurus projects from v2 to v3 + +## Structure + +- `SKILL.md` - Main skill instructions +- `references/` - Detailed documentation loaded as needed +- `scripts/` - Executable code for deterministic operations +- `assets/` - Templates, images, or other resources + +## Usage + +This skill is automatically discovered by Claude when relevant to the task. diff --git a/.claude/skills/docusaurus-migration/SKILL.md b/.claude/skills/docusaurus-migration/SKILL.md new file mode 100644 index 000000000..ce5afc77c --- /dev/null +++ b/.claude/skills/docusaurus-migration/SKILL.md @@ -0,0 +1,53 @@ +--- +name: docusaurus-v2-to-v3-migration +# IMPORTANT: Keep description on ONE line for Claude Code compatibility +# prettier-ignore +description: Use when migrating Docusaurus projects from v2 to v3 +--- + +# Docusaurus V2 To V3 Migration + +## Quick Start + +```json +{ + "@docusaurus/core": "^3.0.0", + "@mdx-js/react": "^3.0.0", + "prism-react-renderer": "^2.1.0", + "react": "^18.2.0" +} +``` + +## Core Principles + +- **MDX v1 → v3**: Main challenge - escape `{` and `<` characters or wrap in code blocks +- **Node.js >=18.0**: Required for Docusaurus v3 +- **React 18**: Breaking changes may affect custom components + +## Migration Steps + +1. **Pre-check**: Run `npx docusaurus-mdx-checker` to identify MDX issues +2. **Update deps**: Upgrade all @docusaurus packages, React, MDX, prism-react-renderer +3. **Fix MDX**: Escape bare `{` `<` characters, convert GFM autolinks, use code fences +4. **Update config**: Replace `@tsconfig/docusaurus` with `@docusaurus/tsconfig`, update Prism imports +5. **Test**: Run `npm start` then `npm run build` + +## Reference Files + +- [breaking-changes.md](references/breaking-changes.md) - Complete migration guide with examples + + diff --git a/.claude/skills/docusaurus-migration/references/breaking-changes.md b/.claude/skills/docusaurus-migration/references/breaking-changes.md new file mode 100644 index 000000000..fb90834aa --- /dev/null +++ b/.claude/skills/docusaurus-migration/references/breaking-changes.md @@ -0,0 +1,395 @@ +# Docusaurus v2 to v3 Breaking Changes + +## Required Dependency Updates + +### Core Packages + +- `@docusaurus/core`: ^2.x.x → ^3.0.0 +- `@docusaurus/preset-classic`: ^2.x.x → ^3.0.0 +- `@mdx-js/react`: ^1.6.22 → ^3.0.0 +- `prism-react-renderer`: ^1.3.5 → ^2.1.0 + +### Runtime Requirements + +- **Node.js**: >=16.14 → >=18.0 +- **React**: ^17.0.2 → ^18.2.0 +- **TypeScript**: ~4.7.4 → ~5.2.2 + +### TypeScript Configuration + +Replace `@tsconfig/docusaurus` with `@docusaurus/tsconfig`: + +```json +{ + "extends": "@docusaurus/tsconfig", + "compilerOptions": { + // your custom options + } +} +``` + +## MDX v1 to v3 Migration + +The transition from MDX v1 to MDX v3 is the main challenge in adopting Docusaurus v3. + +### Common MDX Issues + +#### Invalid `{` Characters + +**Problem**: Bare braces are now invalid in MDX unless part of JSX expressions. + +**Bad**: + +```md +Use the {key} to access the value +``` + +**Good**: + +```md +Use the `{key}` to access the value +Use the \{key\} to access the value +``` + +#### Invalid `<` Characters + +**Problem**: Bare angle brackets are invalid unless part of HTML/JSX. + +**Bad**: + +```md +If x < 5, then... +``` + +**Good**: + +```md +If `x < 5`, then... +If x \< 5, then... +``` + +#### GFM Autolinks Removed + +**Problem**: `` and `` no longer work. + +**Bad**: + +```md +Contact us at +``` + +**Good**: + +```md +Contact us at [support@example.com](mailto:support@example.com) +Visit [https://example.com](https://example.com) +``` + +#### Indented Code Blocks + +**Problem**: Indented code blocks (4 spaces) are no longer recognized. + +**Bad**: + +```md +Example code: + + const foo = 'bar'; +``` + +**Good**: + +```md +Example code: + +\`\`\`js +const foo = 'bar'; +\`\`\` +``` + +#### Emphasis Adjacent to Spaces + +**Problem**: Emphasis marks next to spaces may not work as expected. + +**Bad**: + +```md +This is _really _ important +``` + +**Good**: + +```md +This is _really_ important +``` + +## Prism React Renderer Changes + +### Theme Import Syntax + +**Old (v1)**: + +```js +const lightTheme = require("prism-react-renderer/themes/github"); +const darkTheme = require("prism-react-renderer/themes/dracula"); +``` + +**New (v2)**: + +```js +const { themes } = require("prism-react-renderer"); +const lightTheme = themes.github; +const darkTheme = themes.dracula; +``` + +### Additional Languages + +Prism React Renderer v2 includes fewer languages by default. Add required languages: + +```js +module.exports = { + themeConfig: { + prism: { + additionalLanguages: ["bash", "diff", "json"], + }, + }, +}; +``` + +## React 18 Migration + +### Automatic JSX Runtime + +React imports are no longer required in files using JSX: + +**Before**: + +```jsx +import React from "react"; + +export default function MyComponent() { + return
Hello
; +} +``` + +**After**: + +```jsx +// No React import needed +export default function MyComponent() { + return
Hello
; +} +``` + +### Hydration Changes + +React 18 has stricter hydration requirements. Ensure: + +- Server and client render the same content +- No conditional rendering based on `typeof window` +- Use `useEffect` for client-only code + +## Pre-Migration Checklist + +1. **Check Node.js version**: `node --version` (must be >=18.0) +2. **Scan for MDX issues**: `npx docusaurus-mdx-checker` +3. **Backup your project**: Commit all changes to git +4. **Review dependencies**: Check for custom plugins that may need updates +5. **Test in staging**: Don't upgrade production directly + +## Migration Steps + +### 1. Update package.json + +```json +{ + "dependencies": { + "@docusaurus/core": "^3.0.0", + "@docusaurus/preset-classic": "^3.0.0", + "@mdx-js/react": "^3.0.0", + "prism-react-renderer": "^2.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@docusaurus/tsconfig": "^3.0.0", + "@docusaurus/types": "^3.0.0", + "typescript": "~5.2.2" + }, + "engines": { + "node": ">=18.0" + } +} +``` + +### 2. Update TypeScript Config + +```json +{ + "extends": "@docusaurus/tsconfig", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@site/*": ["./*"] + } + } +} +``` + +### 3. Install Dependencies + +```bash +npm install +``` + +### 4. Fix MDX Files + +Run the checker and fix reported issues: + +```bash +npx docusaurus-mdx-checker +``` + +Common fixes: + +- Wrap `{` and `<` in backticks or escape them +- Convert GFM autolinks to standard Markdown links +- Replace indented code blocks with fenced code blocks + +### 5. Update docusaurus.config.js + +Update Prism configuration: + +```js +const { themes } = require("prism-react-renderer"); + +module.exports = { + themeConfig: { + prism: { + theme: themes.github, + darkTheme: themes.dracula, + additionalLanguages: ["bash", "diff", "json"], + }, + }, +}; +``` + +### 6. Test Locally + +```bash +npm start +``` + +Visit http://localhost:3000 and check: + +- All pages load without errors +- MDX components render correctly +- Code blocks display properly +- Navigation works + +### 7. Production Build + +```bash +npm run build +``` + +Fix any build errors before deploying. + +## Optional Improvements + +### Migrate to ESM + +Convert `docusaurus.config.js` to `docusaurus.config.mjs`: + +```js +import { themes } from "prism-react-renderer"; + +export default { + // config +}; +``` + +### Use TypeScript Config + +Rename to `docusaurus.config.ts`: + +```typescript +import type { Config } from "@docusaurus/types"; +import { themes } from "prism-react-renderer"; + +const config: Config = { + // config +}; + +export default config; +``` + +### Rename MD to MDX + +For files containing JSX, rename `.md` → `.mdx`: + +```bash +# Example +mv blog/my-post.md blog/my-post.mdx +``` + +### Update Math Packages + +If using math support: + +```json +{ + "remark-math": "^6.0.0", + "rehype-katex": "^7.0.0" +} +``` + +## Troubleshooting + +### MDX Parse Errors + +Use the [MDX Playground](https://mdxjs.com/playground/) to debug: + +1. Enable "remark-gfm" plugin +2. Enable "remark-directive" plugin +3. Paste your MDX content +4. Fix reported errors + +### Build Failures + +Common causes: + +- Outdated plugins or themes +- Custom components using React 17 APIs +- Invalid MDX syntax not caught by checker + +### Hydration Mismatches + +React 18 is stricter about hydration. Check for: + +- Client-only code in render +- Conditional rendering based on `window` +- Inconsistent server/client output + +### Missing Prism Languages + +If code blocks don't highlight properly, add to `additionalLanguages`: + +```js +prism: { + additionalLanguages: ['java', 'php', 'ruby', 'python'], +} +``` + +## Getting Help + +- **Official Docs**: https://docusaurus.io/docs/migration/v3 +- **GitHub Discussions**: https://github.com/facebook/docusaurus/discussions +- **Discord**: Join the Docusaurus Discord for real-time help +- **MDX Docs**: https://mdxjs.com/docs/ + +## Resources + +- [MDX v3 Migration Guide](https://mdxjs.com/migrating/v3/) +- [React 18 Upgrade Guide](https://react.dev/blog/2022/03/08/react-18-upgrade-guide) +- [Prism React Renderer Changelog](https://github.com/FormidableLabs/prism-react-renderer/blob/master/CHANGELOG.md) diff --git a/.claude/skills/docusaurus-plugins/README.md b/.claude/skills/docusaurus-plugins/README.md new file mode 100644 index 000000000..2f76cca94 --- /dev/null +++ b/.claude/skills/docusaurus-plugins/README.md @@ -0,0 +1,14 @@ +# Docusaurus Plugin Guide + +Guide for creating different types of Docusaurus plugins including remark, rehype, theme, content, and lifecycle plugins + +## Structure + +- `SKILL.md` - Main skill instructions +- `references/` - Detailed documentation loaded as needed +- `scripts/` - Executable code for deterministic operations +- `assets/` - Templates, images, or other resources + +## Usage + +This skill is automatically discovered by Claude when relevant to the task. diff --git a/.claude/skills/docusaurus-plugins/SKILL.md b/.claude/skills/docusaurus-plugins/SKILL.md new file mode 100644 index 000000000..a21590ecb --- /dev/null +++ b/.claude/skills/docusaurus-plugins/SKILL.md @@ -0,0 +1,65 @@ +--- +name: docusaurus-plugins +# IMPORTANT: Keep description on ONE line for Claude Code compatibility +# prettier-ignore +description: Use when creating Docusaurus plugins (remark, rehype, theme, content, lifecycle) to extend markdown, modify HTML, or add custom functionality +--- + +# Docusaurus Plugin Guide + +## Quick Start + +```javascript +// Remark plugin - transforms markdown AST +module.exports = function remarkPlugin(options = {}) { + return async function transformer(ast, vfile) { + const { visit } = require("unist-util-visit"); + + visit(ast, "link", (node) => { + // Transform nodes + node.data = node.data || {}; + node.data.hProperties = { className: "custom" }; + }); + + return ast; + }; +}; + +// In docusaurus.config.js: +// remarkPlugins: [require('./plugins/my-plugin')] +``` + +## Core Principles + +- **5 Plugin Types**: Remark (markdown), Rehype (HTML), Lifecycle (routes/webpack), Theme (components), Content (custom data) +- **Remark**: Transforms markdown before HTML conversion, use `unist-util-visit` for AST traversal +- **Rehype**: Transforms HTML after compilation, processes HAST (HTML AST) +- **Lifecycle**: Most flexible, implements hooks like `loadContent()`, `contentLoaded()`, `postBuild()` +- **Export function**: Returns transformer (remark/rehype) or plugin object (lifecycle) + +## Reference Files + +Detailed guides for each plugin type: + +- [references/remark-plugins.md](references/remark-plugins.md) - Markdown transformation +- [references/rehype-plugins.md](references/rehype-plugins.md) - HTML processing +- [references/lifecycle-plugins.md](references/lifecycle-plugins.md) - Routes, webpack, global data +- [references/theme-plugins.md](references/theme-plugins.md) - Themes and swizzling +- [references/content-plugins.md](references/content-plugins.md) - Custom content types +- [references/package-structure.md](references/package-structure.md) - Publishing and config + + diff --git a/.claude/skills/docusaurus-plugins/references/content-plugins.md b/.claude/skills/docusaurus-plugins/references/content-plugins.md new file mode 100644 index 000000000..a5c0f90e5 --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/content-plugins.md @@ -0,0 +1,650 @@ +# Content Plugins for Docusaurus + +Content plugins create **custom content types** beyond the default docs and blog. They load, process, and make content available throughout your site. + +## When to Use Content Plugins + +- Custom content types (tutorials, changelog, team members, etc.) +- External data sources (APIs, databases) +- Generated content (API docs from code) +- Multi-source aggregation +- Custom routing patterns + +## Content Plugin Structure + +```javascript +// plugins/plugin-content-changelog/index.js +const fs = require("fs-extra"); +const path = require("path"); +const matter = require("gray-matter"); + +module.exports = function contentChangelogPlugin(context, options) { + const { + changelogPath = "changelog", + routeBasePath = "changelog", + include = ["**/*.md"], + } = options; + + const contentPath = path.resolve(context.siteDir, changelogPath); + + return { + name: "docusaurus-plugin-content-changelog", + + // Load all changelog entries + async loadContent() { + const entries = []; + const files = await fs.readdir(contentPath); + + for (const file of files) { + if (!file.endsWith(".md")) continue; + + const filePath = path.join(contentPath, file); + const content = await fs.readFile(filePath, "utf-8"); + const { data: frontmatter, content: body } = matter(content); + + entries.push({ + id: file.replace(".md", ""), + slug: frontmatter.slug || file.replace(".md", ""), + title: frontmatter.title, + version: frontmatter.version, + date: frontmatter.date, + type: frontmatter.type || "feature", // feature, fix, breaking + body, + filePath, + }); + } + + // Sort by date (newest first) + entries.sort((a, b) => new Date(b.date) - new Date(a.date)); + + return entries; + }, + + // Create routes and make data available + async contentLoaded({ content, actions }) { + const { createData, addRoute, setGlobalData } = actions; + + // Set global data (accessible via useGlobalData hook) + setGlobalData({ + entries: content, + latestVersion: content[0]?.version, + }); + + // Create changelog list page + const listDataPath = await createData( + "changelog-list.json", + JSON.stringify({ entries: content }), + ); + + addRoute({ + path: `/${routeBasePath}`, + component: "@site/src/components/ChangelogList.js", + exact: true, + modules: { + entries: listDataPath, + }, + }); + + // Create individual entry pages + await Promise.all( + content.map(async (entry) => { + const entryDataPath = await createData( + `changelog-${entry.id}.json`, + JSON.stringify(entry), + ); + + addRoute({ + path: `/${routeBasePath}/${entry.slug}`, + component: "@site/src/components/ChangelogEntry.js", + exact: true, + modules: { + entry: entryDataPath, + }, + }); + }), + ); + }, + + // Optional: Generate additional files after build + async postBuild({ outDir, content }) { + // Generate RSS feed + const rss = generateRSSFeed(content); + await fs.writeFile(path.join(outDir, "changelog.xml"), rss); + + // Generate JSON API + const api = content.map((entry) => ({ + id: entry.id, + title: entry.title, + version: entry.version, + date: entry.date, + type: entry.type, + })); + await fs.writeFile( + path.join(outDir, "changelog.json"), + JSON.stringify(api, null, 2), + ); + }, + }; +}; +``` + +## Configuration + +```javascript +// docusaurus.config.js +module.exports = { + plugins: [ + [ + "./plugins/plugin-content-changelog", + { + changelogPath: "changelog", + routeBasePath: "changelog", + include: ["**/*.md"], + }, + ], + ], +}; +``` + +## Content File Structure + +``` +changelog/ +├── 2024-01-15-v2.0.0.md +├── 2024-01-01-v1.5.0.md +└── 2023-12-15-v1.4.0.md +``` + +### Example Content File + +```markdown +--- +slug: v2-0-0 +title: Version 2.0.0 Released +version: 2.0.0 +date: 2024-01-15 +type: breaking +--- + +# What's New in 2.0.0 + +Major breaking changes and new features. + +## Breaking Changes + +- API endpoint restructured +- Configuration format changed + +## New Features + +- Dark mode support +- Improved performance +``` + +## Component Examples + +### Changelog List Component + +```javascript +// src/components/ChangelogList.js +import React from "react"; +import Layout from "@theme/Layout"; +import Link from "@docusaurus/Link"; +import clsx from "clsx"; + +export default function ChangelogList({ entries }) { + return ( + +
+

Changelog

+ +
+ {entries.entries.map((entry) => ( +
+
+ +

{entry.title}

+ + +
+ + {entry.type} + + v{entry.version} + +
+
+ +
+ {entry.body.slice(0, 200)}... +
+
+ ))} +
+
+
+ ); +} +``` + +### Changelog Entry Component + +```javascript +// src/components/ChangelogEntry.js +import React from "react"; +import Layout from "@theme/Layout"; +import MDXContent from "@theme/MDXContent"; +import Link from "@docusaurus/Link"; + +export default function ChangelogEntry({ entry }) { + return ( + +
+ + ← Back to Changelog + + +
+
+

{entry.title}

+
+ Version {entry.version} + +
+
+ + {entry.body} +
+
+
+ ); +} +``` + +## Real-World Examples + +### 1. Team Members Plugin + +```javascript +// plugins/plugin-content-team/index.js +const fs = require("fs-extra"); +const path = require("path"); +const yaml = require("js-yaml"); + +module.exports = function teamPlugin(context, options) { + const { teamDataPath = "data/team.yml" } = options; + + return { + name: "docusaurus-plugin-content-team", + + async loadContent() { + const dataPath = path.join(context.siteDir, teamDataPath); + const data = await fs.readFile(dataPath, "utf-8"); + const team = yaml.load(data); + + return team.members; + }, + + async contentLoaded({ content, actions }) { + const { createData, addRoute, setGlobalData } = actions; + + setGlobalData({ members: content }); + + // Team list page + const dataPath = await createData("team.json", JSON.stringify(content)); + + addRoute({ + path: "/team", + component: "@site/src/components/Team.js", + exact: true, + modules: { members: dataPath }, + }); + + // Individual member pages + await Promise.all( + content.map(async (member) => { + const memberData = await createData( + `team-${member.id}.json`, + JSON.stringify(member), + ); + + addRoute({ + path: `/team/${member.id}`, + component: "@site/src/components/TeamMember.js", + exact: true, + modules: { member: memberData }, + }); + }), + ); + }, + }; +}; +``` + +### 2. External API Content Plugin + +```javascript +// plugins/plugin-content-api/index.js +const fetch = require("node-fetch"); + +module.exports = function apiContentPlugin(context, options) { + const { apiUrl, apiKey } = options; + + return { + name: "docusaurus-plugin-content-api", + + async loadContent() { + // Fetch data from external API + const response = await fetch(apiUrl, { + headers: { Authorization: `Bearer ${apiKey}` }, + }); + + const data = await response.json(); + + return data.items.map((item) => ({ + id: item.id, + title: item.title, + description: item.description, + category: item.category, + updatedAt: item.updated_at, + })); + }, + + async contentLoaded({ content, actions }) { + const { createData, addRoute, setGlobalData } = actions; + + // Make data globally available + setGlobalData({ items: content }); + + // Create category pages + const categories = [...new Set(content.map((item) => item.category))]; + + await Promise.all( + categories.map(async (category) => { + const categoryItems = content.filter( + (item) => item.category === category, + ); + const dataPath = await createData( + `category-${category}.json`, + JSON.stringify(categoryItems), + ); + + addRoute({ + path: `/items/${category}`, + component: "@site/src/components/CategoryPage.js", + exact: true, + modules: { items: dataPath }, + }); + }), + ); + }, + }; +}; +``` + +### 3. Generated API Documentation Plugin + +```javascript +// plugins/plugin-api-docs/index.js +const fs = require("fs-extra"); +const path = require("path"); +const { parseTypeScript } = require("./parser"); + +module.exports = function apiDocsPlugin(context, options) { + const { srcDir = "src", include = ["**/*.ts"] } = options; + + return { + name: "docusaurus-plugin-api-docs", + + async loadContent() { + const srcPath = path.join(context.siteDir, srcDir); + + // Parse TypeScript files + const files = await fs.readdir(srcPath); + const apiDocs = []; + + for (const file of files) { + if (!file.endsWith(".ts")) continue; + + const filePath = path.join(srcPath, file); + const content = await fs.readFile(filePath, "utf-8"); + + // Extract functions, classes, interfaces + const parsed = parseTypeScript(content); + apiDocs.push(...parsed); + } + + return apiDocs; + }, + + async contentLoaded({ content, actions }) { + const { createData, addRoute } = actions; + + // Create API reference pages + await Promise.all( + content.map(async (apiItem) => { + const dataPath = await createData( + `api-${apiItem.name}.json`, + JSON.stringify(apiItem), + ); + + addRoute({ + path: `/api/${apiItem.name}`, + component: "@site/src/components/ApiDoc.js", + exact: true, + modules: { apiItem: dataPath }, + }); + }), + ); + + // Create API index page + const indexPath = await createData( + "api-index.json", + JSON.stringify(content), + ); + + addRoute({ + path: "/api", + component: "@site/src/components/ApiIndex.js", + exact: true, + modules: { items: indexPath }, + }); + }, + }; +}; +``` + +## Using Plugin Data in Components + +### usePluginData Hook + +```javascript +import React from "react"; +import usePluginData from "@docusaurus/usePluginData"; + +export default function ChangelogWidget() { + const { entries, latestVersion } = usePluginData( + "docusaurus-plugin-content-changelog", + ); + + return ( +
+

Latest Release: v{latestVersion}

+
    + {entries.slice(0, 3).map((entry) => ( +
  • + {entry.title} +
  • + ))} +
+
+ ); +} +``` + +### useGlobalData Hook + +```javascript +import React from "react"; +import useGlobalData from "@docusaurus/useGlobalData"; + +export default function AllPluginData() { + const globalData = useGlobalData(); + + // Access data from all plugins + const changelogData = globalData["docusaurus-plugin-content-changelog"]; + const teamData = globalData["docusaurus-plugin-content-team"]; + + return ( +
+

Latest version: {changelogData.latestVersion}

+

Team members: {teamData.members.length}

+
+ ); +} +``` + +## Package Structure + +```json +{ + "name": "@org/docusaurus-plugin-content-changelog", + "version": "1.0.0", + "main": "index.js", + "keywords": ["docusaurus", "plugin", "content", "changelog"], + + "dependencies": { + "gray-matter": "^4.0.3", + "fs-extra": "^11.0.0" + }, + + "peerDependencies": { + "@docusaurus/core": "^3.0.0" + } +} +``` + +## TypeScript Support + +```typescript +// index.d.ts +import { Plugin, LoadContext } from "@docusaurus/types"; + +export interface ChangelogEntry { + id: string; + slug: string; + title: string; + version: string; + date: string; + type: "feature" | "fix" | "breaking"; + body: string; +} + +export interface PluginOptions { + changelogPath?: string; + routeBasePath?: string; + include?: string[]; +} + +export interface PluginContent { + entries: ChangelogEntry[]; + latestVersion: string; +} + +declare const plugin: ( + context: LoadContext, + options: PluginOptions, +) => Plugin; + +export default plugin; +``` + +## Best Practices + +1. **Validate frontmatter** - Ensure required fields exist +2. **Sort content logically** - By date, priority, or category +3. **Cache external data** - Avoid repeated API calls during dev +4. **Provide TypeScript types** - Better DX with autocomplete +5. **Handle errors gracefully** - Missing files, invalid data +6. **Create indexes** - List pages for browsing content +7. **Generate feeds** - RSS, JSON APIs for external consumption + +## Common Patterns + +### Pagination + +```javascript +async contentLoaded({ content, actions }) { + const { createData, addRoute } = actions; + const pageSize = 10; + const pageCount = Math.ceil(content.length / pageSize); + + for (let i = 0; i < pageCount; i++) { + const pageContent = content.slice(i * pageSize, (i + 1) * pageSize); + const dataPath = await createData( + `page-${i + 1}.json`, + JSON.stringify({ + items: pageContent, + page: i + 1, + totalPages: pageCount, + }) + ); + + addRoute({ + path: i === 0 ? '/items' : `/items/page/${i + 1}`, + component: '@site/src/components/ItemList.js', + exact: true, + modules: { data: dataPath }, + }); + } +} +``` + +### Tagging/Categories + +```javascript +// Group content by tags +const tagMap = new Map(); + +content.forEach((item) => { + item.tags.forEach((tag) => { + if (!tagMap.has(tag)) { + tagMap.set(tag, []); + } + tagMap.get(tag).push(item); + }); +}); + +// Create tag pages +for (const [tag, items] of tagMap) { + const dataPath = await createData(`tag-${tag}.json`, JSON.stringify(items)); + + addRoute({ + path: `/tags/${tag}`, + component: "@site/src/components/TagPage.js", + modules: { items: dataPath }, + }); +} +``` + +### Search Integration + +```javascript +// Generate search index +async postBuild({ outDir, content }) { + const searchIndex = content.map((item) => ({ + id: item.id, + title: item.title, + content: item.body, + url: `/changelog/${item.slug}`, + })); + + await fs.writeFile( + path.join(outDir, 'search-index.json'), + JSON.stringify(searchIndex) + ); +} +``` diff --git a/.claude/skills/docusaurus-plugins/references/lifecycle-plugins.md b/.claude/skills/docusaurus-plugins/references/lifecycle-plugins.md new file mode 100644 index 000000000..ae91334da --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/lifecycle-plugins.md @@ -0,0 +1,704 @@ +# Lifecycle Plugins for Docusaurus + +Lifecycle plugins are the most flexible type of Docusaurus plugin. They implement **lifecycle hooks** that execute at different stages of the build process. + +## When to Use Lifecycle Plugins + +- Add custom routes and pages +- Modify webpack configuration +- Inject scripts or styles into HTML +- Generate static files during build +- Add global data accessible across the site +- Integrate third-party services +- Custom content processing + +## Complete Plugin Structure + +```javascript +// plugins/my-plugin/index.js +const path = require("path"); + +module.exports = function myPlugin(context, options) { + const { siteConfig, siteDir } = context; + const { customOption = "default" } = options; + + return { + name: "my-custom-plugin", + + // Load content from files + async loadContent() { + // Read files, fetch data, etc. + const data = await fetchData(); + return data; + }, + + // Make content available globally + async contentLoaded({ content, actions }) { + const { setGlobalData, addRoute, createData } = actions; + + // Add global data (accessible via useGlobalData hook) + setGlobalData({ myData: content }); + + // Add custom route + addRoute({ + path: "/custom-page", + component: "@site/src/components/CustomPage.js", + exact: true, + }); + + // Create data file for component props + const dataPath = await createData( + "my-data.json", + JSON.stringify(content), + ); + + addRoute({ + path: "/data-page", + component: "@site/src/components/DataPage.js", + modules: { + data: dataPath, + }, + exact: true, + }); + }, + + // Run after build completes + async postBuild({ outDir, content }) { + // Generate additional files + await generateSitemap(outDir); + }, + + // Inject HTML tags + injectHtmlTags() { + return { + headTags: [ + { + tagName: "link", + attributes: { + rel: "preconnect", + href: "https://fonts.googleapis.com", + }, + }, + { + tagName: "script", + attributes: { + src: "https://analytics.example.com/script.js", + async: true, + }, + }, + ], + preBodyTags: [], + postBodyTags: [ + { + tagName: "script", + innerHTML: ` + console.log('Page loaded'); + `, + }, + ], + }; + }, + + // Modify webpack config + configureWebpack(config, isServer, utils) { + return { + resolve: { + alias: { + "@components": path.resolve(__dirname, "../../src/components"), + }, + }, + plugins: [ + // Custom webpack plugins + ], + }; + }, + + // Provide client modules (run in browser) + getClientModules() { + return [path.resolve(__dirname, "./clientModule.js")]; + }, + + // Get theme path + getThemePath() { + return path.resolve(__dirname, "./theme"); + }, + }; +}; +``` + +## Configuration in docusaurus.config.js + +```javascript +// Local plugin +module.exports = { + plugins: [ + "./plugins/my-plugin", + // Or with options + [ + "./plugins/my-plugin", + { + customOption: "value", + }, + ], + ], +}; + +// npm package +module.exports = { + plugins: [ + "@org/docusaurus-plugin-name", + // Or with options + [ + "@org/docusaurus-plugin-name", + { + apiKey: "xxx", + enabled: true, + }, + ], + ], +}; +``` + +## Lifecycle Hooks + +### loadContent() + +Load data from files, APIs, or databases. + +```javascript +async loadContent() { + const posts = await fs.readdir('./blog'); + const data = await Promise.all( + posts.map(async (file) => { + const content = await fs.readFile(`./blog/${file}`, 'utf-8'); + return parseMarkdown(content); + }) + ); + return data; +} +``` + +### contentLoaded({ content, actions }) + +Process loaded content and create routes/global data. + +```javascript +async contentLoaded({ content, actions }) { + const { setGlobalData, addRoute, createData } = actions; + + // Add global data + setGlobalData({ posts: content }); + + // Create route for each post + await Promise.all( + content.map(async (post) => { + const dataPath = await createData( + `post-${post.id}.json`, + JSON.stringify(post) + ); + + addRoute({ + path: `/blog/${post.slug}`, + component: '@site/src/components/BlogPost.js', + modules: { post: dataPath }, + exact: true, + }); + }) + ); +} +``` + +### postBuild({ outDir, content }) + +Run after build completes. Generate additional files. + +```javascript +async postBuild({ outDir, content }) { + // Generate RSS feed + const feed = generateRSS(content); + await fs.writeFile(`${outDir}/rss.xml`, feed); + + // Generate sitemap + const sitemap = generateSitemap(content); + await fs.writeFile(`${outDir}/sitemap.xml`, sitemap); +} +``` + +### injectHtmlTags() + +Add scripts, styles, meta tags to HTML. + +```javascript +injectHtmlTags({ content }) { + return { + headTags: [ + // Analytics + { + tagName: 'script', + attributes: { + async: true, + src: 'https://www.googletagmanager.com/gtag/js?id=GA_ID', + }, + }, + // Meta tags + { + tagName: 'meta', + attributes: { + name: 'description', + content: 'Site description', + }, + }, + // Preconnect + { + tagName: 'link', + attributes: { + rel: 'preconnect', + href: 'https://fonts.googleapis.com', + }, + }, + ], + postBodyTags: [ + // Inline script + { + tagName: 'script', + innerHTML: ` + window.customConfig = ${JSON.stringify({ key: 'value' })}; + `, + }, + ], + }; +} +``` + +### configureWebpack(config, isServer, utils) + +Modify webpack configuration. + +```javascript +configureWebpack(config, isServer, utils) { + const { getStyleLoaders } = utils; + + return { + resolve: { + alias: { + '@components': path.resolve(__dirname, '../../src/components'), + }, + }, + module: { + rules: [ + { + test: /\.custom$/, + use: ['custom-loader'], + }, + ], + }, + plugins: [ + new CustomWebpackPlugin(), + ], + }; +} +``` + +### getClientModules() + +Provide modules that run in the browser. + +```javascript +// plugins/my-plugin/index.js +getClientModules() { + return [path.resolve(__dirname, './analytics.js')]; +} + +// plugins/my-plugin/analytics.js +export function onRouteUpdate({ location }) { + // Track page view + if (typeof window !== 'undefined' && window.gtag) { + window.gtag('config', 'GA_ID', { + page_path: location.pathname, + }); + } +} +``` + +## Real-World Examples + +### 1. Analytics Plugin + +```javascript +// plugins/analytics/index.js +module.exports = function analyticsPlugin(context, options) { + const { trackingId, anonymizeIP = true } = options; + + return { + name: "docusaurus-plugin-analytics", + + injectHtmlTags() { + return { + headTags: [ + { + tagName: "script", + attributes: { + async: true, + src: `https://www.googletagmanager.com/gtag/js?id=${trackingId}`, + }, + }, + { + tagName: "script", + innerHTML: ` + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', '${trackingId}', { + anonymize_ip: ${anonymizeIP} + }); + `, + }, + ], + }; + }, + + getClientModules() { + return [path.resolve(__dirname, "./trackPageViews.js")]; + }, + }; +}; + +// plugins/analytics/trackPageViews.js +export function onRouteUpdate({ location }) { + if (typeof window !== "undefined" && window.gtag) { + window.gtag("event", "page_view", { + page_path: location.pathname, + }); + } +} +``` + +### 2. RSS Feed Generator + +```javascript +// plugins/rss/index.js +const fs = require("fs-extra"); +const RSS = require("rss"); + +module.exports = function rssPlugin(context, options) { + const { siteConfig } = context; + const { feedPath = "rss.xml", limit = 20 } = options; + + return { + name: "docusaurus-plugin-rss", + + async postBuild({ outDir }) { + // Read blog posts from content + const posts = await loadBlogPosts(); + + // Create RSS feed + const feed = new RSS({ + title: siteConfig.title, + description: siteConfig.tagline, + feed_url: `${siteConfig.url}/${feedPath}`, + site_url: siteConfig.url, + language: "en", + }); + + // Add posts to feed + posts.slice(0, limit).forEach((post) => { + feed.item({ + title: post.title, + description: post.description, + url: `${siteConfig.url}${post.permalink}`, + date: post.date, + }); + }); + + // Write RSS file + await fs.writeFile( + path.join(outDir, feedPath), + feed.xml({ indent: true }), + ); + }, + }; +}; +``` + +### 3. Dynamic Route Creator + +```javascript +// plugins/custom-pages/index.js +const fs = require("fs-extra"); +const matter = require("gray-matter"); + +module.exports = function customPagesPlugin(context, options) { + const { pagesDir = "custom-pages" } = options; + + return { + name: "docusaurus-plugin-custom-pages", + + async loadContent() { + const pagesPath = path.join(context.siteDir, pagesDir); + const files = await fs.readdir(pagesPath); + + const pages = await Promise.all( + files + .filter((file) => file.endsWith(".md")) + .map(async (file) => { + const content = await fs.readFile( + path.join(pagesPath, file), + "utf-8", + ); + const { data, content: body } = matter(content); + + return { + id: file.replace(".md", ""), + ...data, + body, + }; + }), + ); + + return pages; + }, + + async contentLoaded({ content, actions }) { + const { addRoute, createData } = actions; + + await Promise.all( + content.map(async (page) => { + const dataPath = await createData( + `page-${page.id}.json`, + JSON.stringify(page), + ); + + addRoute({ + path: `/${page.slug || page.id}`, + component: "@site/src/components/CustomPage.js", + modules: { page: dataPath }, + exact: true, + }); + }), + ); + }, + }; +}; + +// src/components/CustomPage.js +import React from "react"; +import Layout from "@theme/Layout"; +import MDXContent from "@theme/MDXContent"; + +export default function CustomPage({ page }) { + return ( + +
+

{page.title}

+ {page.body} +
+
+ ); +} +``` + +### 4. Sitemap Generator + +```javascript +// plugins/sitemap/index.js +const fs = require("fs-extra"); +const { SitemapStream, streamToPromise } = require("sitemap"); + +module.exports = function sitemapPlugin(context, options) { + const { siteConfig } = context; + const { changefreq = "weekly", priority = 0.7 } = options; + + return { + name: "docusaurus-plugin-sitemap", + + async postBuild({ routes, outDir }) { + const sitemap = new SitemapStream({ + hostname: siteConfig.url, + }); + + // Add routes to sitemap + routes.forEach((route) => { + if (!route.path.includes("*") && !route.path.includes(":")) { + sitemap.write({ + url: route.path, + changefreq, + priority, + }); + } + }); + + sitemap.end(); + + // Write sitemap + const xml = await streamToPromise(sitemap); + await fs.writeFile(path.join(outDir, "sitemap.xml"), xml.toString()); + }, + }; +}; +``` + +### 5. Environment Variables Plugin + +```javascript +// plugins/env-vars/index.js +const webpack = require("webpack"); + +module.exports = function envVarsPlugin(context, options) { + const { allowedVars = [] } = options; + + return { + name: "docusaurus-plugin-env-vars", + + configureWebpack() { + const envVars = {}; + + allowedVars.forEach((varName) => { + if (process.env[varName]) { + envVars[`process.env.${varName}`] = JSON.stringify( + process.env[varName], + ); + } + }); + + return { + plugins: [new webpack.DefinePlugin(envVars)], + }; + }, + }; +}; + +// Usage in docusaurus.config.js +plugins: [ + [ + "./plugins/env-vars", + { + allowedVars: ["API_URL", "ANALYTICS_ID"], + }, + ], +]; +``` + +## Package Structure + +``` +docusaurus-plugin-name/ +├── src/ +│ ├── index.js # Main plugin file +│ ├── clientModule.js # Browser-side code +│ └── theme/ # Theme components (if any) +│ └── Component.js +├── index.js # Re-export from src +├── index.d.ts # TypeScript definitions +├── package.json +└── README.md +``` + +## TypeScript Definitions + +```typescript +// index.d.ts +import { + Plugin, + LoadContext, + OptionValidationContext, +} from "@docusaurus/types"; + +export interface PluginOptions { + customOption?: string; + enabled?: boolean; +} + +export default function plugin( + context: LoadContext, + options: PluginOptions, +): Plugin; + +export function validateOptions({ + options, + validate, +}: OptionValidationContext): PluginOptions; +``` + +## Option Validation + +```javascript +const { Joi } = require("@docusaurus/utils-validation"); + +function validateOptions({ options, validate }) { + const schema = Joi.object({ + customOption: Joi.string().default("default"), + enabled: Joi.boolean().default(true), + apiKey: Joi.string().required(), + }); + + return validate(schema, options); +} + +module.exports = { validateOptions }; +``` + +## Best Practices + +1. **Validate options** - Use Joi schemas for type safety +2. **Handle errors gracefully** - Don't crash the build +3. **Document options** - Clear README with examples +4. **Use TypeScript** - Better DX and autocomplete +5. **Cache when possible** - Avoid redundant file reads +6. **Test thoroughly** - Unit and integration tests +7. **Follow naming conventions** - `docusaurus-plugin-*` or `@scope/docusaurus-plugin-*` + +## Testing + +```javascript +// __tests__/plugin.test.js +const plugin = require("../src/index"); + +describe("My Plugin", () => { + const context = { + siteDir: "/site", + siteConfig: { + url: "https://example.com", + baseUrl: "/", + }, + }; + + it("returns correct name", () => { + const instance = plugin(context, {}); + expect(instance.name).toBe("my-plugin"); + }); + + it("loads content correctly", async () => { + const instance = plugin(context, {}); + const content = await instance.loadContent(); + expect(content).toBeDefined(); + }); +}); +``` + +## Common Patterns + +### Global Data Access + +Use `useGlobalData()` hook in React components: + +```javascript +import useGlobalData from "@docusaurus/useGlobalData"; + +function MyComponent() { + const { myData } = useGlobalData()["my-plugin"]; + return
{myData}
; +} +``` + +### Plugin Data Access + +Use `usePluginData()` hook: + +```javascript +import usePluginData from "@docusaurus/usePluginData"; + +function MyComponent() { + const data = usePluginData("my-plugin"); + return
{data}
; +} +``` diff --git a/.claude/skills/docusaurus-plugins/references/package-structure.md b/.claude/skills/docusaurus-plugins/references/package-structure.md new file mode 100644 index 000000000..46df796fd --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/package-structure.md @@ -0,0 +1,666 @@ +# Package Structure and Setup for Docusaurus Plugins + +This guide covers the complete package structure, dependencies, and configuration for publishing Docusaurus plugins. + +## Standard Directory Structure + +### Remark/Rehype Plugin + +``` +docusaurus-plugin-name/ +├── src/ +│ └── index.js # Main plugin implementation +├── test/ or __tests__/ +│ ├── fixtures/ # Test fixtures +│ └── plugin.test.js # Tests +├── index.js # Re-export from src +├── index.d.ts # TypeScript type definitions +├── package.json +├── README.md +├── LICENSE +├── .gitignore +├── .npmignore +├── .editorconfig +└── .github/ + └── workflows/ + └── ci.yml # GitHub Actions CI/CD +``` + +### Lifecycle/Content Plugin + +``` +docusaurus-plugin-name/ +├── src/ +│ ├── index.js # Main plugin file +│ ├── clientModule.js # Browser-side code (optional) +│ ├── types.ts # TypeScript types +│ └── utils/ # Utility functions +│ ├── parser.js +│ └── validators.js +├── theme/ # Theme components (optional) +│ ├── Component.js +│ └── Component.css +├── test/ +│ └── index.test.js +├── index.js # Entry point +├── index.d.ts # Type definitions +├── package.json +├── tsconfig.json # TypeScript config (if using TS) +├── jest.config.js # Jest config +├── README.md +├── LICENSE +└── CHANGELOG.md +``` + +## package.json Configuration + +### Minimal package.json (Remark Plugin) + +```json +{ + "name": "docusaurus-plugin-glossary", + "version": "1.0.0", + "description": "Docusaurus remark plugin for glossary tooltips", + "main": "index.js", + "types": "index.d.ts", + + "keywords": ["docusaurus", "remark", "plugin", "glossary", "documentation"], + + "author": "Your Name ", + "license": "MIT", + + "repository": { + "type": "git", + "url": "https://github.com/username/docusaurus-plugin-glossary" + }, + + "bugs": { + "url": "https://github.com/username/docusaurus-plugin-glossary/issues" + }, + + "homepage": "https://github.com/username/docusaurus-plugin-glossary#readme", + + "files": ["index.js", "index.d.ts", "lib/", "src/"], + + "dependencies": { + "unist-util-visit": "^4.0.0" + }, + + "peerDependencies": { + "@docusaurus/core": "^3.0.0", + "remark": "^13.0.0 || ^14.0.0" + }, + + "devDependencies": { + "@types/node": "^18.0.0", + "@types/unist": "^2.0.0", + "jest": "^29.0.0", + "remark": "^14.0.0" + }, + + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint .", + "format": "prettier --write \"**/*.{js,json,md}\"" + }, + + "engines": { + "node": ">=18.0.0" + } +} +``` + +### Complete package.json (Lifecycle Plugin) + +```json +{ + "name": "@org/docusaurus-plugin-changelog", + "version": "2.1.0", + "description": "Changelog content plugin for Docusaurus", + "main": "lib/index.js", + "types": "lib/index.d.ts", + + "keywords": ["docusaurus", "plugin", "content", "changelog", "release-notes"], + + "author": "Organization Name", + "license": "MIT", + + "repository": { + "type": "git", + "url": "https://github.com/org/docusaurus-plugin-changelog", + "directory": "packages/plugin-changelog" + }, + + "publishConfig": { + "access": "public" + }, + + "files": ["lib/", "theme/", "index.js", "index.d.ts"], + + "dependencies": { + "fs-extra": "^11.0.0", + "gray-matter": "^4.0.3", + "js-yaml": "^4.1.0", + "rss": "^1.2.2" + }, + + "peerDependencies": { + "@docusaurus/core": "^3.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + + "devDependencies": { + "@docusaurus/core": "^3.0.0", + "@docusaurus/types": "^3.0.0", + "@types/jest": "^29.0.0", + "@types/node": "^18.0.0", + "@types/react": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^9.0.0", + "jest": "^29.0.0", + "prettier": "^3.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "typescript": "^5.0.0" + }, + + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint src --ext .ts,.tsx --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,js,json}\"", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build && npm test" + }, + + "engines": { + "node": ">=18.0.0" + } +} +``` + +## TypeScript Configuration + +### tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020", "DOM"], + "jsx": "react", + + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./lib", + "rootDir": "./src", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "test", "**/*.test.ts"] +} +``` + +## TypeScript Type Definitions + +### Remark Plugin Types + +```typescript +// index.d.ts +import { Plugin } from "unified"; +import { Root } from "mdast"; + +/** + * Options for the glossary plugin + */ +export interface RemarkGlossaryOptions { + /** + * Directory containing term definitions + * @default './docs/terms' + */ + termsDir?: string; + + /** + * Root documentation directory + * @default './docs' + */ + docsDir?: string; + + /** + * Output path for generated glossary + * @default './docs/glossary.md' + */ + glossaryFilepath?: string; + + /** + * Custom component path for tooltips + */ + componentPath?: string; +} + +/** + * Remark plugin for adding glossary tooltips to documentation + */ +declare const remarkGlossary: Plugin<[RemarkGlossaryOptions?], Root>; + +export default remarkGlossary; +``` + +### Lifecycle Plugin Types + +```typescript +// index.d.ts +import { + Plugin, + LoadContext, + OptionValidationContext, +} from "@docusaurus/types"; + +/** + * Changelog entry data structure + */ +export interface ChangelogEntry { + id: string; + slug: string; + title: string; + version: string; + date: string; + type: "feature" | "fix" | "breaking" | "improvement"; + body: string; + filePath: string; +} + +/** + * Plugin options + */ +export interface PluginOptions { + /** + * Path to changelog directory + * @default 'changelog' + */ + changelogPath?: string; + + /** + * Base URL path for changelog routes + * @default 'changelog' + */ + routeBasePath?: string; + + /** + * Glob patterns for files to include + * @default ['**\/*.md'] + */ + include?: string[]; + + /** + * Generate RSS feed + * @default true + */ + generateRss?: boolean; +} + +/** + * Plugin content loaded in browser + */ +export interface PluginContent { + entries: ChangelogEntry[]; + latestVersion: string; +} + +declare const plugin: ( + context: LoadContext, + options: PluginOptions, +) => Plugin; + +export default plugin; + +/** + * Validate plugin options + */ +export function validateOptions( + context: OptionValidationContext, +): PluginOptions; +``` + +## Jest Configuration + +### jest.config.js + +```javascript +module.exports = { + testEnvironment: "node", + testMatch: ["**/__tests__/**/*.js", "**/*.test.js"], + collectCoverageFrom: [ + "src/**/*.js", + "!src/**/*.test.js", + "!**/node_modules/**", + ], + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + }, + transform: { + "^.+\\.jsx?$": "babel-jest", + }, +}; +``` + +## ESLint Configuration + +### .eslintrc.js + +```javascript +module.exports = { + root: true, + env: { + node: true, + es2020: true, + }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, + rules: { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/explicit-module-boundary-types": "off", + "no-console": "warn", + }, +}; +``` + +## Prettier Configuration + +### .prettierrc + +```json +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false +} +``` + +## .npmignore + +``` +# Source files +src/ +test/ +__tests__/ + +# Config files +.github/ +.vscode/ +*.config.js +tsconfig.json +.eslintrc.js +.prettierrc + +# Documentation +*.md +!README.md +!LICENSE + +# Misc +coverage/ +.DS_Store +*.log +``` + +## .gitignore + +``` +# Dependencies +node_modules/ + +# Build output +lib/ +dist/ +*.tsbuildinfo + +# Testing +coverage/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Environment +.env +.env.local +``` + +## GitHub Actions CI/CD + +### .github/workflows/ci.yml + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run tests + run: npm test -- --coverage + + - name: Type check + run: npm run typecheck + + - name: Build + run: npm run build + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage/coverage-final.json + + publish: + needs: test + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "18.x" + registry-url: "https://registry.npmjs.org" + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Publish to npm + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} +``` + +## README.md Template + +````markdown +# docusaurus-plugin-name + +> Brief description of what the plugin does + +## Installation + +```bash +npm install docusaurus-plugin-name +# or +yarn add docusaurus-plugin-name +``` +```` + +## Usage + +Add to your `docusaurus.config.js`: + +```javascript +module.exports = { + plugins: [ + [ + "docusaurus-plugin-name", + { + // Options + option1: "value1", + option2: true, + }, + ], + ], +}; +``` + +## Options + +| Option | Type | Default | Description | +| --------- | --------- | ----------- | ----------- | +| `option1` | `string` | `'default'` | Description | +| `option2` | `boolean` | `false` | Description | + +## Examples + +### Basic Example + +```javascript +// Example code +``` + +### Advanced Example + +```javascript +// Advanced usage +``` + +## API + +### Plugin Methods + +#### `loadContent()` + +Description... + +#### `contentLoaded({ content, actions })` + +Description... + +## License + +MIT + +```` + +## Versioning and Publishing + +### Semantic Versioning + +- **MAJOR** (1.0.0 → 2.0.0): Breaking changes +- **MINOR** (1.0.0 → 1.1.0): New features (backward compatible) +- **PATCH** (1.0.0 → 1.0.1): Bug fixes + +### Publishing to npm + +```bash +# 1. Login to npm +npm login + +# 2. Test build +npm run build +npm test + +# 3. Update version +npm version patch # or minor, major + +# 4. Publish +npm publish + +# For scoped packages +npm publish --access public +```` + +## Best Practices + +1. **Use TypeScript** - Better DX, fewer bugs +2. **Write tests** - Aim for 80%+ coverage +3. **Document thoroughly** - README, JSDoc, examples +4. **Semantic versioning** - Follow SemVer strictly +5. **CI/CD** - Automate testing and publishing +6. **Peer dependencies** - Mark Docusaurus as peer dep +7. **Keep minimal** - Only include necessary files in package +8. **Changelog** - Maintain CHANGELOG.md diff --git a/.claude/skills/docusaurus-plugins/references/rehype-plugins.md b/.claude/skills/docusaurus-plugins/references/rehype-plugins.md new file mode 100644 index 000000000..7db38cf37 --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/rehype-plugins.md @@ -0,0 +1,541 @@ +# Rehype Plugins for Docusaurus + +Rehype plugins transform **HTML content** after markdown has been converted to HTML. They operate on the HAST (HTML Abstract Syntax Tree). + +## When to Use Rehype Plugins + +- HTML post-processing and cleanup +- Adding wrapper divs or containers +- Accessibility improvements (ARIA attributes) +- Modifying HTML attributes +- Syntax highlighting customization +- Lazy loading images +- Adding classes to elements + +## Remark vs Rehype + +| Aspect | Remark | Rehype | +| ------------ | -------------------------- | --------------------- | +| **Input** | Markdown | HTML | +| **AST** | MDAST | HAST | +| **Timing** | Before HTML conversion | After HTML conversion | +| **Use Case** | Markdown syntax extensions | HTML manipulation | +| **Example** | Custom `[[term]]` syntax | Add wrapper divs | + +## Complete Plugin Structure + +```javascript +// index.js - Rehype plugin +const { visit } = require("unist-util-visit"); +const { h } = require("hastscript"); + +module.exports = function rehypeCustomPlugin(options = {}) { + const { + wrapperClass = "content-wrapper", + addLazyLoading = true, + externalLinkIcon = true, + } = options; + + return function transformer(tree, file) { + // Add lazy loading to images + if (addLazyLoading) { + visit(tree, "element", (node) => { + if (node.tagName === "img") { + node.properties.loading = "lazy"; + node.properties.decoding = "async"; + } + }); + } + + // Add icon to external links + if (externalLinkIcon) { + visit(tree, "element", (node) => { + if (node.tagName === "a" && node.properties.href) { + const href = node.properties.href; + + if ( + href.startsWith("http") && + !href.includes(options.siteUrl || "") + ) { + // Add external link class + node.properties.className = [ + ...(node.properties.className || []), + "external-link", + ]; + + // Add rel attributes for security + node.properties.rel = "noopener noreferrer"; + node.properties.target = "_blank"; + + // Add icon element + node.children.push({ + type: "element", + tagName: "span", + properties: { className: ["external-icon"] }, + children: [{ type: "text", value: " ↗" }], + }); + } + } + }); + } + + // Wrap content sections + visit(tree, "element", (node, index, parent) => { + if ( + node.tagName === "div" && + node.properties.className?.includes("markdown") + ) { + // Wrap in custom container + const wrapper = h(`div.${wrapperClass}`, {}, [node]); + parent.children[index] = wrapper; + } + }); + + return tree; + }; +}; +``` + +## Configuration in docusaurus.config.js + +```javascript +// Basic configuration +module.exports = { + presets: [ + [ + "@docusaurus/preset-classic", + { + docs: { + rehypePlugins: [require("./plugins/my-rehype-plugin")], + }, + }, + ], + ], +}; + +// With options +module.exports = { + presets: [ + [ + "@docusaurus/preset-classic", + { + docs: { + rehypePlugins: [ + [ + require("./plugins/my-rehype-plugin"), + { + wrapperClass: "custom-wrapper", + addLazyLoading: true, + externalLinkIcon: true, + siteUrl: "https://mysite.com", + }, + ], + ], + }, + }, + ], + ], +}; +``` + +## Common HTML Node Types + +### Element Nodes + +```javascript +{ + type: 'element', + tagName: 'div', + properties: { + className: ['my-class'], + id: 'my-id', + dataCustom: 'value' + }, + children: [...] +} +``` + +### Text Nodes + +```javascript +{ + type: 'text', + value: 'Text content' +} +``` + +### Common Elements + +```javascript +// Link +{ + type: 'element', + tagName: 'a', + properties: { + href: 'https://example.com', + rel: 'noopener', + target: '_blank' + }, + children: [{ type: 'text', value: 'Link text' }] +} + +// Image +{ + type: 'element', + tagName: 'img', + properties: { + src: '/img/photo.jpg', + alt: 'Description', + loading: 'lazy' + }, + children: [] +} + +// Heading +{ + type: 'element', + tagName: 'h2', + properties: { id: 'heading-id' }, + children: [{ type: 'text', value: 'Heading' }] +} +``` + +## Using hastscript + +The `hastscript` library (`h()` function) makes creating HTML nodes easier: + +```javascript +const { h } = require("hastscript"); + +// Create elements +const div = h("div", { className: "container" }, [ + h("p", "Paragraph text"), + h("a", { href: "#" }, "Link"), +]); + +// Shorthand with classes and IDs +const header = h("div.header#main", [h("h1.title", "Page Title")]); + +// Result: +//
+//

Page Title

+//
+``` + +## Real-World Examples + +### 1. Add Wrapper Divs to Code Blocks + +```javascript +const { visit } = require("unist-util-visit"); +const { h } = require("hastscript"); + +module.exports = function rehypeCodeWrapper() { + return function transformer(tree) { + visit(tree, "element", (node, index, parent) => { + if (node.tagName === "pre") { + // Get language from code element + const codeNode = node.children[0]; + const language = + codeNode?.properties?.className?.[0]?.replace("language-", "") || + "text"; + + // Create wrapper with copy button + const wrapper = h( + "div.code-block-wrapper", + { dataLanguage: language }, + [ + h("div.code-header", [ + h("span.language-label", language), + h("button.copy-button", { type: "button" }, "Copy"), + ]), + node, + ], + ); + + parent.children[index] = wrapper; + } + }); + + return tree; + }; +}; +``` + +### 2. Lazy Load Images with Blur Placeholder + +```javascript +const { visit } = require("unist-util-visit"); + +module.exports = function rehypeLazyImages(options = {}) { + const { blurDataURL = "data:image/..." } = options; + + return function transformer(tree) { + visit(tree, "element", (node) => { + if (node.tagName === "img") { + // Add lazy loading + node.properties.loading = "lazy"; + node.properties.decoding = "async"; + + // Add blur placeholder + node.properties.style = `background-image: url('${blurDataURL}'); background-size: cover;`; + + // Wrap in picture element for responsive images + const picture = { + type: "element", + tagName: "picture", + properties: {}, + children: [ + // WebP source + { + type: "element", + tagName: "source", + properties: { + srcset: node.properties.src.replace(/\.(jpg|png)$/, ".webp"), + type: "image/webp", + }, + children: [], + }, + // Original img + node, + ], + }; + + return picture; + } + }); + + return tree; + }; +}; +``` + +### 3. Add Accessibility Improvements + +```javascript +const { visit } = require("unist-util-visit"); + +module.exports = function rehypeA11y() { + return function transformer(tree) { + visit(tree, "element", (node) => { + // Add ARIA labels to links without text + if (node.tagName === "a") { + const hasText = node.children.some( + (child) => + child.type === "text" || + (child.type === "element" && child.tagName !== "img"), + ); + + if (!hasText) { + node.properties.ariaLabel = node.properties.href; + } + + // Mark external links + if (node.properties.href?.startsWith("http")) { + node.properties.ariaLabel = + `${node.properties.ariaLabel || ""} (opens in new tab)`.trim(); + } + } + + // Ensure images have alt text + if (node.tagName === "img" && !node.properties.alt) { + console.warn(`Image missing alt text: ${node.properties.src}`); + node.properties.alt = "Image"; // Fallback + } + + // Add role to nav elements + if (node.tagName === "nav" && !node.properties.role) { + node.properties.role = "navigation"; + } + }); + + return tree; + }; +}; +``` + +### 4. Add Reading Time Meta + +```javascript +const { visit } = require("unist-util-visit"); +const { h } = require("hastscript"); + +module.exports = function rehypeReadingTime() { + return function transformer(tree, file) { + let wordCount = 0; + + // Count words + visit(tree, "text", (node) => { + wordCount += node.value.split(/\s+/).length; + }); + + // Calculate reading time (average 200 words per minute) + const readingTime = Math.ceil(wordCount / 200); + + // Add to frontmatter or tree + file.data.readingTime = readingTime; + + // Insert reading time element at the beginning + if (tree.children[0]) { + tree.children.unshift( + h("div.reading-time", { dataMinutes: readingTime }, [ + h("span", `${readingTime} min read`), + ]), + ); + } + + return tree; + }; +}; +``` + +### 5. Enhance Tables + +```javascript +const { visit } = require("unist-util-visit"); +const { h } = require("hastscript"); + +module.exports = function rehypeTables() { + return function transformer(tree) { + visit(tree, "element", (node, index, parent) => { + if (node.tagName === "table") { + // Wrap table in responsive container + const wrapper = h("div.table-wrapper", [h("div.table-scroll", [node])]); + + // Add sortable classes to headers + visit(node, "element", (headerNode) => { + if (headerNode.tagName === "th") { + headerNode.properties.className = [ + ...(headerNode.properties.className || []), + "sortable", + ]; + headerNode.properties.tabIndex = 0; + } + }); + + parent.children[index] = wrapper; + } + }); + + return tree; + }; +}; +``` + +## Package Dependencies + +```json +{ + "dependencies": { + "unist-util-visit": "^4.0.0", + "hastscript": "^7.0.0" + }, + "peerDependencies": { + "@docusaurus/core": "^3.0.0", + "rehype": "^12.0.0" + }, + "devDependencies": { + "@types/hast": "^2.0.0", + "jest": "^29.0.0" + } +} +``` + +## TypeScript Definitions + +```typescript +// index.d.ts +import { Plugin } from "unified"; +import { Root } from "hast"; + +export interface RehypePluginOptions { + wrapperClass?: string; + addLazyLoading?: boolean; + externalLinkIcon?: boolean; + siteUrl?: string; +} + +declare const rehypePlugin: Plugin<[RehypePluginOptions?], Root>; +export default rehypePlugin; +``` + +## Testing + +```javascript +// __tests__/plugin.test.js +const rehype = require("rehype"); +const customPlugin = require("../index"); + +describe("Rehype Custom Plugin", () => { + const processor = rehype().use(customPlugin, { + addLazyLoading: true, + }); + + it("adds lazy loading to images", async () => { + const input = 'Photo'; + const result = await processor.process(input); + + expect(result.toString()).toContain('loading="lazy"'); + }); + + it("adds external link icons", async () => { + const input = 'Link'; + const result = await processor.process(input); + + expect(result.toString()).toContain("external-link"); + expect(result.toString()).toContain('target="_blank"'); + }); +}); +``` + +## Best Practices + +1. **Use hastscript for creating nodes** - Cleaner than manual object creation +2. **Preserve existing properties** - Don't overwrite, merge instead +3. **Handle edge cases** - Check for missing children, properties +4. **Add security attributes** - Use `rel="noopener noreferrer"` for external links +5. **Maintain accessibility** - Add ARIA labels, alt text +6. **Wrap destructive changes** - Use options to enable/disable transformations +7. **Log warnings, not errors** - Don't crash on missing attributes + +## Common Use Cases + +### Responsive Images + +Add `srcset`, lazy loading, and blur placeholders. + +### Code Block Enhancement + +Add copy buttons, language labels, line numbers. + +### Link Processing + +Add icons for external links, security attributes, analytics tracking. + +### Accessibility + +ARIA labels, semantic HTML, keyboard navigation. + +### Performance + +Lazy loading, async decoding, resource hints. + +### SEO + +Structured data, meta tags, Open Graph images. + +## Debugging + +```javascript +// Log all HTML elements +visit(tree, "element", (node) => { + console.log(node.tagName, node.properties); +}); + +// Log tree structure +console.log(JSON.stringify(tree, null, 2)); +``` + +Use online AST explorers: + +- https://astexplorer.net/ (select "HTML" and "rehype") diff --git a/.claude/skills/docusaurus-plugins/references/remark-plugins.md b/.claude/skills/docusaurus-plugins/references/remark-plugins.md new file mode 100644 index 000000000..a4a8d8544 --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/remark-plugins.md @@ -0,0 +1,455 @@ +# Remark Plugins for Docusaurus + +Remark plugins transform **markdown content** during the parsing phase, before it's converted to HTML. They operate on the MDAST (Markdown Abstract Syntax Tree). + +## When to Use Remark Plugins + +- Custom markdown syntax (e.g., `%%term%%` for glossary terms) +- Link processing and validation +- Auto-generating content from markdown patterns +- Adding metadata or classes to markdown elements +- Content transformation before HTML rendering + +## Complete Plugin Structure + +```javascript +// index.js - Main plugin file +const { visit } = require("unist-util-visit"); +const fs = require("fs"); +const path = require("path"); + +module.exports = function remarkCustomPlugin(options = {}) { + // Validate and process options + const { + pattern = /%%(.+?)%%/g, + dataFile = "./data/terms.json", + componentName = "CustomTooltip", + } = options; + + // Load external data if needed + let glossaryData = {}; + if (fs.existsSync(dataFile)) { + glossaryData = JSON.parse(fs.readFileSync(dataFile, "utf-8")); + } + + // Return the transformer function + return async function transformer(ast, vfile) { + const filePath = vfile.path || ""; + + // Visit specific node types + visit(ast, "text", (node, index, parent) => { + const matches = [...node.value.matchAll(pattern)]; + + if (matches.length === 0) return; + + // Build replacement nodes + const newNodes = []; + let lastIndex = 0; + + matches.forEach((match) => { + const [fullMatch, termKey] = match; + const startIndex = match.index; + + // Add text before match + if (startIndex > lastIndex) { + newNodes.push({ + type: "text", + value: node.value.slice(lastIndex, startIndex), + }); + } + + // Add custom component node + const termData = glossaryData[termKey]; + if (termData) { + newNodes.push({ + type: "jsx", + value: `<${componentName} term="${termKey}" tooltip="${termData.tooltip}">${termData.display}`, + }); + } else { + // Fallback if term not found + newNodes.push({ + type: "text", + value: fullMatch, + }); + } + + lastIndex = startIndex + fullMatch.length; + }); + + // Add remaining text + if (lastIndex < node.value.length) { + newNodes.push({ + type: "text", + value: node.value.slice(lastIndex), + }); + } + + // Replace the original node + parent.children.splice(index, 1, ...newNodes); + }); + + // Visit links + visit(ast, "link", (node) => { + if (node.url.endsWith(".md")) { + // Transform internal markdown links + node.data = node.data || {}; + node.data.hProperties = { + className: "internal-link", + "data-internal": true, + }; + } + }); + + return ast; + }; +}; +``` + +## Configuration in docusaurus.config.js + +```javascript +// Basic configuration +module.exports = { + presets: [ + [ + "@docusaurus/preset-classic", + { + docs: { + remarkPlugins: [require("./plugins/my-remark-plugin")], + }, + }, + ], + ], +}; + +// With options +module.exports = { + presets: [ + [ + "@docusaurus/preset-classic", + { + docs: { + remarkPlugins: [ + [ + require("./plugins/my-remark-plugin"), + { + pattern: /\[\[(.+?)\]\]/g, + dataFile: "./glossary.json", + componentName: "GlossaryTerm", + }, + ], + ], + }, + }, + ], + ], +}; + +// Execute BEFORE default Docusaurus plugins +module.exports = { + presets: [ + [ + "@docusaurus/preset-classic", + { + docs: { + beforeDefaultRemarkPlugins: [require("./plugins/my-remark-plugin")], + }, + }, + ], + ], +}; +``` + +## Common Node Types + +### Text Nodes + +```javascript +{ + type: 'text', + value: 'Some text content' +} +``` + +### Link Nodes + +```javascript +{ + type: 'link', + url: 'https://example.com', + children: [{ type: 'text', value: 'Link text' }] +} +``` + +### Paragraph Nodes + +```javascript +{ + type: 'paragraph', + children: [...] +} +``` + +### JSX Nodes (for MDX) + +```javascript +{ + type: 'jsx', + value: 'Content' +} +``` + +### Heading Nodes + +```javascript +{ + type: 'heading', + depth: 2, // h2 + children: [{ type: 'text', value: 'Heading text' }] +} +``` + +## Using unist-util-visit + +```javascript +const { visit } = require("unist-util-visit"); + +// Visit all nodes of a specific type +visit(ast, "link", (node) => { + console.log(node.url); +}); + +// Visit multiple types +visit(ast, ["link", "image"], (node) => { + console.log(node.type, node.url); +}); + +// Visit with index and parent access +visit(ast, "text", (node, index, parent) => { + // Modify parent.children to replace nodes + parent.children[index] = newNode; +}); + +// Visit all nodes +visit(ast, (node) => { + if (node.type === "link" && node.url.startsWith("http")) { + // Process external links + } +}); +``` + +## Real-World Example: Glossary Plugin + +Based on docusaurus-plugin-glossary pattern: + +```javascript +// plugins/glossary-plugin.js +const { visit } = require("unist-util-visit"); +const fs = require("fs"); +const path = require("path"); +const yaml = require("js-yaml"); + +module.exports = function glossaryPlugin(options = {}) { + const { + termsDir = "./docs/terms", + docsDir = "./docs", + glossaryFilepath = "./docs/glossary.md", + } = options; + + // Load all term files + const loadTerms = () => { + const terms = {}; + const termFiles = fs.readdirSync(termsDir); + + termFiles.forEach((file) => { + if (!file.endsWith(".md")) return; + + const content = fs.readFileSync(path.join(termsDir, file), "utf-8"); + const [, frontmatter, body] = content.match( + /^---\n([\s\S]+?)\n---\n([\s\S]*)$/, + ); + + const meta = yaml.load(frontmatter); + terms[meta.id] = { + title: meta.title, + hoverText: meta.hoverText || body.slice(0, 200), + path: `terms/${file.replace(".md", "")}`, + }; + }); + + return terms; + }; + + return function transformer(ast, vfile) { + const terms = loadTerms(); + + // Convert [[term]] syntax to tooltipped links + visit(ast, "text", (node, index, parent) => { + const matches = [...node.value.matchAll(/\[\[(.+?)\]\]/g)]; + + if (matches.length === 0) return; + + const newNodes = []; + let lastIndex = 0; + + matches.forEach((match) => { + const [fullMatch, termKey] = match; + const term = terms[termKey]; + + if (!term) { + console.warn(`Term not found: ${termKey}`); + return; + } + + // Add text before match + if (match.index > lastIndex) { + newNodes.push({ + type: "text", + value: node.value.slice(lastIndex, match.index), + }); + } + + // Add glossary link with tooltip + newNodes.push({ + type: "jsx", + value: `${term.title}`, + }); + + lastIndex = match.index + fullMatch.length; + }); + + // Remaining text + if (lastIndex < node.value.length) { + newNodes.push({ + type: "text", + value: node.value.slice(lastIndex), + }); + } + + parent.children.splice(index, 1, ...newNodes); + }); + + return ast; + }; +}; +``` + +## Package Dependencies + +```json +{ + "dependencies": { + "unist-util-visit": "^4.0.0" + }, + "peerDependencies": { + "@docusaurus/core": "^3.0.0", + "remark": "^13.0.0" + }, + "devDependencies": { + "@types/unist": "^2.0.0", + "jest": "^29.0.0" + } +} +``` + +## TypeScript Definitions + +```typescript +// index.d.ts +import { Plugin } from "unified"; +import { Root } from "mdast"; + +export interface RemarkPluginOptions { + pattern?: RegExp; + dataFile?: string; + componentName?: string; + termsDir?: string; + docsDir?: string; + glossaryFilepath?: string; +} + +declare const remarkPlugin: Plugin<[RemarkPluginOptions?], Root>; +export default remarkPlugin; +``` + +## Testing + +```javascript +// __tests__/plugin.test.js +const remark = require("remark"); +const remarkMdx = require("remark-mdx"); +const glossaryPlugin = require("../index"); + +describe("Glossary Plugin", () => { + const processor = remark().use(remarkMdx).use(glossaryPlugin, { + termsDir: "./__fixtures__/terms", + }); + + it("transforms glossary syntax", async () => { + const input = "This is a [[test-term]] example."; + const result = await processor.process(input); + + expect(result.toString()).toContain(" { + const input = "This is a [[missing-term]] example."; + const result = await processor.process(input); + + // Should leave unmatched terms as-is or show warning + expect(result.toString()).toBeTruthy(); + }); +}); +``` + +## Best Practices + +1. **Keep dependencies minimal** - Only use `unist-util-visit` for traversal +2. **Cache external data** - Load files once, not per-transform +3. **Handle missing data gracefully** - Don't crash on missing terms/files +4. **Preserve node position** - Maintain `position` property for error reporting +5. **Use JSX nodes for components** - Type `jsx` integrates with MDX +6. **Validate options** - Provide sensible defaults +7. **Support async operations** - Use `async/await` when reading files +8. **Add TypeScript definitions** - Improves developer experience + +## Common Patterns + +### Auto-linking Terms + +Transform text patterns into links automatically. + +### Custom Syntax + +Add markdown extensions like `::note[text]` or `%%term%%`. + +### Content Generation + +Generate tables, lists, or summaries from markdown structure. + +### Link Validation + +Check internal links exist, add attributes to external links. + +### Metadata Injection + +Add frontmatter data as HTML attributes or classes. + +## Debugging + +```javascript +// Add logging to see AST structure +visit(ast, (node) => { + console.log(JSON.stringify(node, null, 2)); +}); + +// Log only specific types +visit(ast, "link", (node) => { + console.log("Link:", node.url); +}); +``` + +Use online AST explorers: + +- https://astexplorer.net/ (select "Markdown" and "remark") diff --git a/.claude/skills/docusaurus-plugins/references/theme-plugins.md b/.claude/skills/docusaurus-plugins/references/theme-plugins.md new file mode 100644 index 000000000..46696f5b3 --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/theme-plugins.md @@ -0,0 +1,578 @@ +# Theme Plugins for Docusaurus + +Theme plugins provide custom **themes** and allow **component swizzling** to override default Docusaurus components. + +## When to Use Theme Plugins + +- Create custom themes +- Override default components (Navbar, Footer, etc.) +- Provide reusable component sets +- Brand customization across sites +- Custom layouts + +## Theme Plugin Structure + +```javascript +// plugins/theme-custom/index.js +const path = require("path"); + +module.exports = function themePlugin(context, options) { + return { + name: "docusaurus-theme-custom", + + getThemePath() { + // Return path to theme components + return path.resolve(__dirname, "./theme"); + }, + + getTypeScriptThemePath() { + // Return path to TypeScript theme types + return path.resolve(__dirname, "./theme"); + }, + + getClientModules() { + // Global CSS and client-side code + return [ + path.resolve(__dirname, "./theme/global.css"), + path.resolve(__dirname, "./theme/prism-theme.js"), + ]; + }, + }; +}; +``` + +## Theme Directory Structure + +``` +plugins/theme-custom/ +├── index.js # Plugin definition +└── theme/ + ├── global.css # Global styles + ├── Layout.js # Main layout component + ├── Navbar.js # Navigation bar + ├── Footer.js # Footer + ├── DocPage.js # Documentation page layout + ├── BlogPostPage.js # Blog post layout + ├── MDXComponents.js # Custom MDX components + └── hooks/ + └── useCustomHook.js # Custom React hooks +``` + +## Configuration + +```javascript +// docusaurus.config.js +module.exports = { + themes: [ + "./plugins/theme-custom", + // Or npm package + "@org/docusaurus-theme-custom", + ], + + // Or with options + themes: [ + [ + "./plugins/theme-custom", + { + customColors: { + primary: "#007bff", + secondary: "#6c757d", + }, + }, + ], + ], +}; +``` + +## Component Swizzling + +### What is Swizzling? + +Swizzling allows you to **override** or **wrap** default Docusaurus components. + +### Swizzle Commands + +```bash +# List all swizzleable components +npm run swizzle @docusaurus/theme-classic -- --list + +# Eject a component (full control, breaks with updates) +npm run swizzle @docusaurus/theme-classic Navbar -- --eject + +# Wrap a component (safer, preserves original) +npm run swizzle @docusaurus/theme-classic Navbar -- --wrap + +# Swizzle TypeScript version +npm run swizzle @docusaurus/theme-classic Navbar -- --typescript +``` + +### Ejecting vs Wrapping + +**Ejecting (--eject)** + +- Full control over component +- Copy entire component to your src/theme +- ⚠️ Breaks with Docusaurus updates +- Use for major customizations + +**Wrapping (--wrap)** + +- Wraps original component +- Preserve default behavior +- ✅ Safer, updates compatible +- Use for minor additions + +## Example: Custom Theme Components + +### 1. Custom Navbar + +```javascript +// theme/Navbar.js +import React from "react"; +import Link from "@docusaurus/Link"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import "./Navbar.css"; + +export default function Navbar() { + const { siteConfig } = useDocusaurusContext(); + + return ( + + ); +} +``` + +### 2. Custom Footer + +```javascript +// theme/Footer.js +import React from "react"; +import Link from "@docusaurus/Link"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; + +export default function Footer() { + const { siteConfig } = useDocusaurusContext(); + const currentYear = new Date().getFullYear(); + + return ( +
+
+
+

Documentation

+
    +
  • + Getting Started +
  • +
  • + API Reference +
  • +
  • + Guides +
  • +
+
+ +
+

Community

+ +
+ +
+

More

+
    +
  • + Blog +
  • +
  • + Changelog +
  • +
  • + Privacy Policy +
  • +
+
+
+ +
+

+ © {currentYear} {siteConfig.organizationName}. All rights reserved. +

+
+
+ ); +} +``` + +### 3. Custom MDX Components + +```javascript +// theme/MDXComponents.js +import React from "react"; +import MDXComponents from "@theme-original/MDXComponents"; +import Highlight from "@site/src/components/Highlight"; +import CodeBlock from "@theme/CodeBlock"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +// Custom components available in MDX +export default { + ...MDXComponents, + + // Override default components + code: CodeBlock, + + // Add custom components + Highlight, + Callout: ({ type = "info", children }) => ( +
{children}
+ ), + + YouTube: ({ id }) => ( +
+