From 3f198e641c8d81166f5aeb11734ec84fd3d7b133 Mon Sep 17 00:00:00 2001 From: Max Clayton Clowes Date: Mon, 15 Dec 2025 14:27:01 +0000 Subject: [PATCH 1/3] Tidy up actions, and various other improvements --- .github/dependabot.yml | 19 +++-- .github/workflows/check-links.yml | 6 +- .github/workflows/check-spelling.yml | 2 +- .gitignore | 72 ++++++++++++------- CLAUDE.md | 39 ++++++++++ CONTRIBUTING.md | 2 +- docs/using-the-api/authentication.md | 2 +- docs/using-the-api/testing.md | 2 +- .../using-the-api/webhooks/create-consumer.md | 2 +- .../using-the-api/webhooks/troubleshooting.md | 6 +- 10 files changed, 110 insertions(+), 42 deletions(-) create mode 100644 CLAUDE.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8be7d430e..286f3ebaa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,13 +5,20 @@ version: 2 updates: - - package-ecosystem: "npm" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "npm" + directory: "/" schedule: interval: "weekly" assignees: - - "mcclowes" - - "dcoplowe" + - "pmckinney-codat" reviewers: - - "mcclowes" - - "dcoplowe" + - "pmckinney-codat" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + assignees: + - "pmckinney-codat" + reviewers: + - "pmckinney-codat" diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 10d9cb091..936b758ef 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -12,10 +12,10 @@ jobs: if: github.event_name != 'pull_request' || github.actor != 'codatbot' steps: - name: Checkout code - uses: actions/checkout@v2 - + uses: actions/checkout@v4 + - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: '20' diff --git a/.github/workflows/check-spelling.yml b/.github/workflows/check-spelling.yml index 38a480a46..0c725bbd8 100644 --- a/.github/workflows/check-spelling.yml +++ b/.github/workflows/check-spelling.yml @@ -22,4 +22,4 @@ jobs: run: npm install -g cspell - name: Run spell check - run: cspell "**/*.md" "**/*.mdx" --words-only cspell-words.txt \ No newline at end of file + run: cspell "**/*.md" "**/*.mdx" --config cspell.json \ No newline at end of file diff --git a/.gitignore b/.gitignore index 26775670f..5aa0d13ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,57 @@ # Dependencies /node_modules +static/**/node_modules/ -# Production +# Build outputs /build - -# Generated files +/dist .docusaurus .cache-loader -src/components/page/reference/ReleaseNotes/release-notes.json + +# Environment +.env +.env.* +!.env.example + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +*.log + +# OS files +.DS_Store +Thumbs.db + +# IDEs +.idea/ +*.sln.iml /.vs +.vscode/ +*.sublime-project +*.sublime-workspace +*.swp +*.swo +*~ + +# Test coverage +coverage/ + +# Temp files +*.tmp +*.temp +.cache/ + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo + +# Generated files +src/components/page/reference/ReleaseNotes/release-notes.json # Code utilities - generated files code_utils/files_with_code.txt @@ -22,24 +65,3 @@ __pycache__/ .venv/ venv/ env/ -dist/ -build/ - - -# Misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local -.env - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -static/**/node_modules/ - -# JetBrains Rider -.idea/ -*.sln.iml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..873ec6e31 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,39 @@ +# Claude Code Project Instructions + +This is the Codat documentation site, built with Docusaurus. + +## Project Structure + +- `docs/` - Main documentation content (Markdown/MDX) +- `blog/` - Blog posts +- `src/` - React components and custom pages +- `static/` - Static assets (images, files) +- `.github/workflows/` - CI/CD workflows + +## Development + +```sh +npm install +npm start +``` + +Requires Node.js v20+. + +## Code Style + +- Documentation is written in Markdown/MDX +- Use Prettier for formatting (`npm run format:js:check`, `npm run format:mdx:check`) +- Follow existing patterns for component structure + +## Testing + +- `npm run build` - Build the site +- Spell check runs via cspell +- Link checking via linkinator +- Readability checks via Lexi + +## Key Files + +- `docusaurus.config.js` - Main Docusaurus configuration +- `sidebars.js` - Sidebar navigation structure +- `cspell.json` - Spell check dictionary and settings diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6627ab4e..f662bc4df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ $ npm install $ npm start ``` -We recommend npm -v 8+ and Node.js -v 16+. +We recommend npm v10+ and Node.js v20+. --- diff --git a/docs/using-the-api/authentication.md b/docs/using-the-api/authentication.md index 3a3e145b2..9c30da6f2 100644 --- a/docs/using-the-api/authentication.md +++ b/docs/using-the-api/authentication.md @@ -10,7 +10,7 @@ API keys are tokens used to control access to the API. Codat expects the API key Authorization: Basic YOUR_ENCODED_API_KEY // Replace *YOUR_ENCODED_API_KEY* with your API key, Base64 encoded ``` -When using API keys in your application, you can either store the raw API key and encode it yourself, or just store the pre-encoded authroization header we expose. +When using API keys in your application, you can either store the raw API key and encode it yourself, or just store the pre-encoded authorization header we expose. ## Managing keys diff --git a/docs/using-the-api/testing.md b/docs/using-the-api/testing.md index 1bc7423d4..b51aeb1b5 100644 --- a/docs/using-the-api/testing.md +++ b/docs/using-the-api/testing.md @@ -4,7 +4,7 @@ sidebar_label: "Test your solution" description: "Review our suggestions, best practices, and strategies for testing your Codat build" --- -Testing is a key component of any software development process, both during the implementation and after go-live if changes are made to the solution. Here is what Codat recommentd for testing your Codat build. +Testing is a key component of any software development process, both during the implementation and after go-live if changes are made to the solution. Here is what Codat recommends for testing your Codat build. ## Using a test instance diff --git a/docs/using-the-api/webhooks/create-consumer.md b/docs/using-the-api/webhooks/create-consumer.md index 119f1e108..1a9d454b5 100644 --- a/docs/using-the-api/webhooks/create-consumer.md +++ b/docs/using-the-api/webhooks/create-consumer.md @@ -127,7 +127,7 @@ Follow the steps below to configure mTLS for a webhook consumer in Codat: 2. Select the webhook consumer you want to configure mTLS for. 3. In the detailed endpoint view, click **Advanced**, then **Configure mTLS**. ![A fragment of the webhook UI that directs the user to the mTLS configuration page](/img/use-the-api/webhook-advanced-mTLS.png) -4. In the displayed text box, enter your **PEM-enconded private key** and the **X.509 certificate**, separating them by a blank line. +4. In the displayed text box, enter your **PEM-encoded private key** and the **X.509 certificate**, separating them by a blank line. ![A fragment of the webhook UI that allows you to configure mTLS on your webhook consumers](/img/use-the-api/webhook-mTLS-configuration.png) 5. Click **Save** to apply the configuration. diff --git a/docs/using-the-api/webhooks/troubleshooting.md b/docs/using-the-api/webhooks/troubleshooting.md index f58e24cff..37ea2921e 100644 --- a/docs/using-the-api/webhooks/troubleshooting.md +++ b/docs/using-the-api/webhooks/troubleshooting.md @@ -41,7 +41,7 @@ Our webhooks service can recover two types of messages: - **Failed messages** occur when the message wasn't delivered even after all attempts to deliver the message have been exhausted. You can **recover** such messages. - **Missed messages** occur when the endpoint has been disabled, the endpoint didn't exist at the time of sending the message (but created afterward), or the endpoint initially configured to listen to other event types and has been updated to include additional ones. You can **replay** such messages. -For each message to recover, we will attempt to send a new message, irrespetive of whether or not there are further attempts scheduled as part of the retry policy. +For each message to recover, we will attempt to send a new message, irrespective of whether or not there are further attempts scheduled as part of the retry policy. If you want to replay or recover one or more messages in case of your app's downtime or incorrect configuration, you can do so in the [Codat Portal](https://app.codat.io/monitor/events). @@ -67,11 +67,11 @@ Then, click the triple-dot menu on the right and choose one of the applicable op For more granular date control, you can scroll to the endpoint's message attempts, click the triple-dot options menu of a specific message, and choose **Replay > Replay all failed messages since this time**. -During the recovery of mutiple messages, all messages will be sent at once with some jitter applied in order to prevent overloading the webhook consumer endpoint. If your system has rate-limiting in place, the number of messages to recover may be an important consideration to avoid further failures. Please reach out to Codat Support if unsure. +During the recovery of multiple messages, all messages will be sent at once with some jitter applied in order to prevent overloading the webhook consumer endpoint. If your system has rate-limiting in place, the number of messages to recover may be an important consideration to avoid further failures. Please reach out to Codat Support if unsure. ### Idempotency -Whilst the Codat system's webhook functionality aims for exactly once delivery of a message, due to the fact messages can be resent, this isn't always possible to guarantee. If idempotency is important for your system, we reccomend making use of the HTTP request's webhook-id header, which functions as an idempotency key for a given message, (i.e remains constant across all attempts to deliver that message), and can therefore be used by your system to ensure messages are not reprocessed. +Whilst the Codat system's webhook functionality aims for exactly once delivery of a message, due to the fact messages can be resent, this isn't always possible to guarantee. If idempotency is important for your system, we recommend making use of the HTTP request's webhook-id header, which functions as an idempotency key for a given message, (i.e remains constant across all attempts to deliver that message), and can therefore be used by your system to ensure messages are not reprocessed. ## Endpoint failures From 70aa1ad25354d13730d2dfbe75399862d029683b Mon Sep 17 00:00:00 2001 From: Max Clayton Clowes Date: Mon, 15 Dec 2025 14:28:11 +0000 Subject: [PATCH 2/3] useful skills --- .claude/skills/docusaurus-config/README.md | 14 + .claude/skills/docusaurus-config/SKILL.md | 60 ++ .../references/detailed-guide.md | 314 ++++++++ .../skills/docusaurus-documentation/README.md | 14 + .../skills/docusaurus-documentation/SKILL.md | 72 ++ .claude/skills/docusaurus-migration/README.md | 14 + .claude/skills/docusaurus-migration/SKILL.md | 53 ++ .../references/breaking-changes.md | 395 ++++++++++ .claude/skills/docusaurus-plugins/README.md | 14 + .claude/skills/docusaurus-plugins/SKILL.md | 65 ++ .../references/content-plugins.md | 624 ++++++++++++++++ .../references/lifecycle-plugins.md | 683 ++++++++++++++++++ .../references/package-structure.md | 651 +++++++++++++++++ .../references/rehype-plugins.md | 522 +++++++++++++ .../references/remark-plugins.md | 453 ++++++++++++ .../references/theme-plugins.md | 560 ++++++++++++++ .claude/skills/docusaurus-themes/README.md | 14 + .claude/skills/docusaurus-themes/SKILL.md | 62 ++ .../docusaurus-themes/references/commands.md | 113 +++ .../references/components.md | 171 +++++ .claude/skills/google-style-guide/README.md | 14 + .claude/skills/google-style-guide/SKILL.md | 77 ++ .../references/formatting.md | 233 ++++++ .../references/inclusive-language.md | 118 +++ .../references/language-grammar.md | 71 ++ 25 files changed, 5381 insertions(+) create mode 100644 .claude/skills/docusaurus-config/README.md create mode 100644 .claude/skills/docusaurus-config/SKILL.md create mode 100644 .claude/skills/docusaurus-config/references/detailed-guide.md create mode 100644 .claude/skills/docusaurus-documentation/README.md create mode 100644 .claude/skills/docusaurus-documentation/SKILL.md create mode 100644 .claude/skills/docusaurus-migration/README.md create mode 100644 .claude/skills/docusaurus-migration/SKILL.md create mode 100644 .claude/skills/docusaurus-migration/references/breaking-changes.md create mode 100644 .claude/skills/docusaurus-plugins/README.md create mode 100644 .claude/skills/docusaurus-plugins/SKILL.md create mode 100644 .claude/skills/docusaurus-plugins/references/content-plugins.md create mode 100644 .claude/skills/docusaurus-plugins/references/lifecycle-plugins.md create mode 100644 .claude/skills/docusaurus-plugins/references/package-structure.md create mode 100644 .claude/skills/docusaurus-plugins/references/rehype-plugins.md create mode 100644 .claude/skills/docusaurus-plugins/references/remark-plugins.md create mode 100644 .claude/skills/docusaurus-plugins/references/theme-plugins.md create mode 100644 .claude/skills/docusaurus-themes/README.md create mode 100644 .claude/skills/docusaurus-themes/SKILL.md create mode 100644 .claude/skills/docusaurus-themes/references/commands.md create mode 100644 .claude/skills/docusaurus-themes/references/components.md create mode 100644 .claude/skills/google-style-guide/README.md create mode 100644 .claude/skills/google-style-guide/SKILL.md create mode 100644 .claude/skills/google-style-guide/references/formatting.md create mode 100644 .claude/skills/google-style-guide/references/inclusive-language.md create mode 100644 .claude/skills/google-style-guide/references/language-grammar.md 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..13eb2874f --- /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..15a64eddc --- /dev/null +++ b/.claude/skills/docusaurus-config/references/detailed-guide.md @@ -0,0 +1,314 @@ +# 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..401014c0a --- /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..e8a2a4f1d --- /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..f7044b42c --- /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..2a3e1e230 --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/content-plugins.md @@ -0,0 +1,624 @@ +# 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..ae98523f9 --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/lifecycle-plugins.md @@ -0,0 +1,683 @@ +# 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..ec61b2c0f --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/package-structure.md @@ -0,0 +1,651 @@ +# 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..0331e6dad --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/rehype-plugins.md @@ -0,0 +1,522 @@ +# 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..7682004ea --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/remark-plugins.md @@ -0,0 +1,453 @@ +# 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..9357cd607 --- /dev/null +++ b/.claude/skills/docusaurus-plugins/references/theme-plugins.md @@ -0,0 +1,560 @@ +# 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 }) => ( +
+