30 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
The Video.js v10 documentation site is an Astro-based static site generator with React islands for interactivity. The site serves documentation, blog posts, and interactive demos for the Video.js v10 library.
Key architectural feature: Multi-framework documentation — the same content generates separate routes for different framework/style combinations (e.g., HTML + CSS, React + CSS), allowing framework-specific documentation from shared MDX sources.
Commands
From site/ directory:
| Command | Purpose |
|---|---|
pnpm dev |
Start dev server at localhost:4321 |
pnpm build |
Build production site to ./dist/ |
pnpm api-docs |
Regenerate API reference JSON files |
pnpm test |
Run all tests once |
pnpm test:watch |
Run tests in watch mode |
pnpm test:ui |
Run Vitest with web UI |
pnpm test:coverage |
Generate coverage report |
pnpm astro ... |
Run Astro CLI (e.g., pnpm astro check) |
From monorepo root:
pnpm dev:site— Start site dev serverpnpm build:site— Build site
Running single test file:
pnpm test sidebar.test.ts
Astro MCP Server
An Astro MCP server is likely available. Always use Astro best practices and standard patterns for maintainability. Consult the MCP for Astro-specific guidance when working with:
- Astro components and layouts
- Content collections
- Routing patterns
- Integrations
- Build optimizations
Following Astro conventions ensures consistency and makes the codebase easier to maintain.
Tailwind v4 Configuration & Gotchas
This project uses Tailwind v4 with a custom configuration. Standard Tailwind tokens may not be available.
CRITICAL: Always Check globals.css First
Before using any Tailwind utility class, read src/styles/globals.css to verify:
- Custom color tokens (e.g.,
dark-110,light-100, not standard Tailwind colors) - Custom text size tokens (e.g.,
text-h1,text-h2, not standardtext-4xl,text-5xl) - Custom tracking values
- Custom font weight values
- Available custom variants
- And possibly more
Custom Variant: intent: (NOT hover: or focus-visible:)
Use the intent: variant instead of hover: and focus-visible::
<!-- ✅ CORRECT -->
<button class="intent:bg-dark-80">Click me</button>
<!-- ❌ WRONG -->
<button class="hover:bg-dark-80">Click me</button>
The intent: variant is defined as:
@custom-variant intent (&:hover, &:focus-within);
Arbitrary Tailwind: Last Resort Only
Avoid arbitrary variants like [&:hover] or text-[pink]. Use them only as a last resort.
Prefer inline styles when Tailwind utilities don't exist:
<!-- ✅ BETTER: Inline style -->
<div style="transform: rotate(45deg)">Content</div>
<!-- ❌ WORSE: Arbitrary variant -->
<div class="[transform:rotate(45deg)]">Content</div>
Use clsx for Class Concatenation
Always use clsx (or cn helper if available) for conditional classes:
import clsx from 'clsx';
<button class={clsx(
'text-base bg-dark-100',
isActive && 'bg-dark-80',
isPrimary ? 'text-yellow' : 'text-light-100'
)}>
Click me
</button>
Avoiding Arbitrary Values
Prefer token-based utilities or inline styles over arbitrary values:
<!-- ✅ CORRECT: Use token if available -->
<div class="min-h-30">Content</div>
<!-- ✅ CORRECT: Inline style for non-token values -->
<div style="min-height: 120px">Content</div>
<!-- ❌ WRONG: Arbitrary value -->
<div class="min-h-[120px]">Content</div>
For responsive/dark mode with non-token values, use CSS custom properties:
<!-- ✅ CORRECT: Custom property bridge -->
<div
style="--md-min-h: 120px"
class="md:min-h-(--md-min-h)"
>Content</div>
<!-- ❌ WRONG: Arbitrary value in variant -->
<div class="md:min-h-[120px]">Content</div>
This limits the classnames Tailwind must generate.
Project Structure
site/
├── src/
│ ├── components/ # Astro + React components
│ │ └── docs/
│ │ ├── api-reference/ # API reference Astro components
│ │ └── demos/ # Interactive component demos (see below)
│ ├── content/ # Content collections (blog/, docs/, authors.json)
│ ├── layouts/ # Page layouts (Base, Blog, Docs, Markdown)
│ ├── pages/ # Route pages (file-based routing)
│ ├── content/generated-component-reference/ # Generated component reference JSON (gitignored)
│ ├── content/generated-util-reference/ # Generated util reference JSON (gitignored)
│ ├── stores/ # Nanostores for cross-island state
│ ├── styles/ # Global CSS, Tailwind imports
│ ├── types/ # TypeScript type definitions
│ ├── utils/ # Utilities and helpers
│ │ └── docs/ # Documentation-specific utilities
│ │ ├── sidebar.ts # Sidebar filtering and navigation
│ │ ├── routing.ts # Docs URL building and redirects
│ │ └── __tests__/ # Tests for docs utilities
│ ├── consts.ts # Site-wide constants
│ ├── content.config.ts # Content collection schemas
│ ├── docs.config.ts # Documentation sidebar structure
│ └── test-setup.ts # Vitest setup file
├── scripts/
│ └── api-docs-builder/ # Generates API reference from TypeScript
├── public/ # Static assets (served untransformed)
├── integrations/ # Custom Astro integrations
│ ├── pagefind.ts # Pagefind search integration
│ ├── llms-markdown.ts # LLM-optimized markdown generation
│ └── check-v8-urls.ts # v8 URL migration audit
├── astro.config.mjs # Astro configuration
├── tsconfig.json # TypeScript config with path aliases
└── vitest.config.ts # Test configuration
Interactive Demos
Reference pages include live, interactive demos for each component. Demos are framework-specific (React, HTML) and style-specific (CSS).
Directory Structure
src/components/docs/demos/
├── Demo.astro # Shared shell (live preview + tabbed source code)
├── HtmlDemo.astro # Renders raw HTML via set:html
└── {component}/{framework}/{style}/
├── BasicUsage.tsx # React: component (+ .css)
├── BasicUsage.astro # HTML: Astro wrapper (renders HTML, imports CSS, bundles script)
├── BasicUsage.html # HTML: markup only, no <style> or <script>
├── BasicUsage.css # HTML: styles (imported by .astro wrapper for live demo)
└── BasicUsage.ts # HTML: side-effect imports for custom element registration
CSS Scoping with BEM
Demos use BEM class names for scoping. Block = {component}-{variant}, element = __{part}:
.play-button-basic /* block */
.play-button-basic__button /* element */
React .css and HTML .css files for the same demo should use identical BEM names.
React Demos
A .tsx component + .css file. Rendered as an Astro island via client:idle, displayed as source via ?raw:
import BasicUsageDemo from "@/components/docs/demos/play-button/react/css/BasicUsage";
import basicUsageTsx from "@/components/docs/demos/play-button/react/css/BasicUsage.tsx?raw";
import basicUsageCss from "@/components/docs/demos/play-button/react/css/BasicUsage.css?raw";
<Demo files={[
{ title: "App.tsx", code: basicUsageTsx, lang: "tsx" },
{ title: "App.css", code: basicUsageCss, lang: "css" },
]}>
<BasicUsageDemo client:idle />
</Demo>
HTML Demos
Four files per demo: .html (markup only), .css (styles), .ts (custom element registration), and .astro (wrapper that ties them together). The .astro wrapper is needed because only Astro <script> tags go through Vite's bundling pipeline — MDX <script> tags compile as JSX and aren't bundled.
.astro wrapper (imports CSS for live demo, renders HTML, bundles the .ts script):
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './BasicUsage.html?raw';
import './BasicUsage.css';
---
<HtmlDemo html={html} />
<script>
import './BasicUsage.ts';
</script>
MDX usage:
import BasicUsageDemoHtml from "@/components/docs/demos/play-button/html/css/BasicUsage.astro";
import basicUsageHtml from "@/components/docs/demos/play-button/html/css/BasicUsage.html?raw";
import basicUsageHtmlCss from "@/components/docs/demos/play-button/html/css/BasicUsage.css?raw";
import basicUsageHtmlTs from "@/components/docs/demos/play-button/html/css/BasicUsage.ts?raw";
<Demo files={[
{ title: "index.html", code: basicUsageHtml, lang: "html" },
{ title: "index.css", code: basicUsageHtmlCss, lang: "css" },
{ title: "index.ts", code: basicUsageHtmlTs, lang: "ts" },
]}>
<BasicUsageDemoHtml />
</Demo>
State Reflection in HTML Demos
HTML custom elements expose state via data-* attributes. Use CSS to toggle labels:
<media-play-button class="play-button-basic__button">
<span class="show-when-paused">Play</span>
<span class="show-when-playing">Pause</span>
</media-play-button>
.play-button-basic__button .show-when-paused { display: none; }
.play-button-basic__button .show-when-playing { display: none; }
.play-button-basic__button[data-paused] .show-when-paused { display: inline; }
.play-button-basic__button:not([data-paused]) .show-when-playing { display: inline; }
The React equivalent uses the render prop: render={(props, state) => <button {...props}>{state.paused ? 'Play' : 'Pause'}</button>}.
Video Attributes
All demo videos use autoplay muted playsinline loop (React: autoPlay muted playsInline loop).
Multi-Framework Documentation Architecture
Framework/Style Combinations
Documentation is generated for multiple framework and style combinations from the same MDX source files.
Current support (defined in src/types/docs.ts):
- Frameworks:
html,react - Styles:
css(more may be added)
URL pattern:
/docs/framework/{framework}/{...slug}/
Style handling: Style is a client-side preference stored in localStorage per-framework (vjs_docs_style_html, vjs_docs_style_react). The StyleInit.astro component reads localStorage before paint and sets html[data-style]. CSS rules control content visibility via [data-for-style] attributes on <StyleCase> wrapped content.
Example:
src/content/docs/how-to/installation.mdxgenerates:/docs/framework/html/how-to/installation//docs/framework/react/how-to/installation/
Content Restriction Mechanisms
1. Within MDX content:
Use <FrameworkCase> or <StyleCase> components to show framework/style-specific content:
<FrameworkCase for="react">
Use `useState` to manage state.
</FrameworkCase>
<FrameworkCase for="html">
Use `data-` attributes to manage state.
</FrameworkCase>
2. In sidebar config (src/docs.config.ts):
Restrict entire guides to specific frameworks:
const sidebar: Sidebar = [
{
sidebarLabel: 'Getting started',
contents: [
{ slug: 'how-to/installation' }, // Available to all frameworks
{
slug: 'how-to/react-hooks',
frameworks: ['react'] // Only visible when viewing React docs
},
],
},
];
Note: Style restrictions on sidebar items are no longer supported. All docs are visible to all styles; use <StyleCase> within docs to show style-specific content.
Sidebar Configuration
Structure (src/docs.config.ts):
- Export a
sidebarconstant of typeSidebar - Hierarchical: Sections contain Guides
- Each Guide has:
slug: Path relative tosrc/content/docs/(without.mdx)sidebarLabel(optional): Override display nameframeworks(optional): Restrict to specific frameworksdevOnly(optional): Show only in development mode
Example:
export const sidebar: Sidebar = [
{
sidebarLabel: 'Components',
contents: [
{ slug: 'reference/play-button' },
{ slug: 'reference/mute-button', sidebarLabel: 'Mute' },
],
},
];
Documentation Utilities
Key Utility Functions (src/utils/docs/)
sidebar.ts — Sidebar filtering and navigation:
filterSidebar(): Filter sidebar by framework, remove empty sectionsfindFirstGuide(): Get first available guide for frameworkfindGuideBySlug(): Search sidebar recursively for a guidegetAdjacentGuides(): Get prev/next guides for navigationgetSectionsForGuide(): Get breadcrumb trail to a guide
routing.ts — URL building and redirect logic:
buildDocsUrl(): Construct docs URLs from framework and slugresolveIndexRedirect(): Intelligent redirect for index pages- Validates framework from URL params or cookie preferences
- Falls back to defaults when invalid
Docs Routing Pattern
Nested index pages handle redirects at each level:
/docs/ → redirect to first guide
/docs/framework/ → redirect to first guide
/docs/framework/{framework}/ → redirect to first guide
/docs/framework/{framework}/{slug} → render guide
Each index page uses resolveIndexRedirect() to determine where to redirect based on:
- URL params (framework)
- User preferences (framework from cookies)
- Defaults (when invalid or missing)
Style is not part of the URL. Style preference is stored per-framework in localStorage and applied client-side via CSS.
Content Collections
Defined in src/content.config.ts using Astro's Content Collections API.
IMPORTANT: Only MDX Files Supported
We only support .mdx files, NOT .md files.
All content must be written in MDX format to support:
- React components within content
- Framework/style conditional rendering (
<FrameworkCase>,<StyleCase>) - Custom typography components
- Interactive examples
Blog Collection (src/content/blog/)
Filename convention: YYYY-MM-DD-slug.mdx
- Date prefix automatically removed from slug
- Example:
2024-01-15-new-release.mdx→ slug:new-release, URL:/blog/new-release/
Schema:
{
title: string;
description: string;
pubDate: Date; // From filename date prefix
authors: string[]; // Reference to authors.json
canonical?: string; // Canonical URL override
devOnly?: boolean; // Show only in development
}
Docs Collection (src/content/docs/)
Subdirectories:
how-to/— Outcome-focused guides (per Diátaxis framework)concepts/— Understanding-focused guidesreference/— API documentation
Schema:
{
title: string;
description: string;
frameworkTitle?: { // Per-framework title overrides
html?: string;
react?: string;
};
updatedDate?: Date; // From git history
}
Authors Collection (src/content/authors.json)
{
[key: string]: {
name: string;
shortName: string;
bio?: string;
avatar?: string;
socialLinks?: {
x?: string;
bluesky?: string;
mastodon?: string;
github?: string;
linkedin?: string;
website?: string;
};
}
}
Git Integration
src/utils/gitService.ts uses simple-git to enrich content with metadata:
- Blog posts:
pubDatefrom filename date prefix - All content:
updatedDatefrom last modification
State Management with Nanostores
Why Nanostores? Astro's island architecture means each React component with client:load is an isolated React root. React Context doesn't work across islands, so we use Nanostores for cross-island state.
Store locations (src/stores/):
preferences.ts: User framework/style preferences (persisted to localStorage)homePageDemos.ts: Home page demo statetabs.ts: Tab component stateinstallation.ts: Installation page state (renderer, skin, install method)
Usage pattern:
import { useStore } from '@nanostores/react';
import { $preferences } from '@/stores/preferences';
function MyComponent() {
const prefs = useStore($preferences);
// ...
}
Testing
Configuration (vitest.config.ts)
{
globals: true, // No import needed for describe, it, expect
environment: 'jsdom', // Browser-like environment
setupFiles: ['./src/test-setup.ts'], // Imports @testing-library/jest-dom
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/utils/**', 'src/components/**', 'src/types/**', 'scripts/api-docs-builder/src/**'],
exclude: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx', '**/test/**'],
},
}
Test Organization
Tests are colocated with source code in __tests__/ directories:
src/utils/docs/
├── sidebar.ts
├── routing.ts
└── __tests__/
├── sidebar.test.ts
└── routing.test.ts
Testing Patterns
Mock framework/style configuration:
vi.mock('@/types/docs', async () => {
const actual = await vi.importActual('@/types/docs');
return {
...actual,
FRAMEWORK_STYLES: { html: ['css'], react: ['css'] },
};
});
Test complex utilities:
- Sidebar filtering with nested sections
- Route resolution and redirect logic
- Framework/style validation
Technology Stack
- Astro 5.14.4: Static site generation with island architecture
- React 19: Client-side interactive components (
client:load) - React Compiler: Enabled via
babel-plugin-react-compilertargeting React 19 - Tailwind v4: CSS utility classes via
@tailwindcss/vite - Nanostores 1.0.1: Cross-island state
- Base UI 1.2.0: Headless accessible components
- Pagefind 1.4.0: Static search with build-time indexing
- Shiki 3.13.0: Syntax highlighting
- Vitest 3.2.4: Testing framework
- clsx: Class name concatenation utility
API Reference Generation
Source of truth:
internal/design/site/api-docs-builder.mdThe design spec is the ground-truth for the entire pipeline — discovery, extraction, JSON schemas, reference model, and rendered output. Any changes to the api-docs-builder must be reflected in the spec. When implementation diverges from the spec, the spec wins.
The builder (scripts/api-docs-builder/) extracts type information from TypeScript sources and generates JSON for two kinds of reference:
- Component references →
src/content/generated-component-reference/{name}.json - Util references →
src/content/generated-util-reference/{slug}.json
Generated files are gitignored and regenerated automatically on pnpm dev and pnpm build, or manually via pnpm api-docs.
Usage in MDX
import ComponentReference from '@/components/docs/api-reference/ComponentReference.astro';
import UtilReference from '@/components/docs/api-reference/UtilReference.astro';
<ComponentReference component="PlayButton" />
<UtilReference util="usePlayer" />
Custom Astro Integration: Pagefind
Location: integrations/pagefind.ts
Purpose: Integrates Pagefind static search into Astro build pipeline.
Development mode:
- Serves Pagefind index from previous production build
- Uses
sirvmiddleware to serve/pagefind/*routes - Warns if index doesn't exist (needs
pnpm buildfirst)
Production mode:
- Runs Pagefind CLI after Astro build completes
- Indexes all HTML files in
dist/ - Maps Astro logger levels to Pagefind CLI flags
Usage in astro.config.mjs:
import pagefind from './integrations/pagefind';
export default defineConfig({
integrations: [pagefind()],
});
Environment Variables
The site uses OAuth for authentication and Mux for video management. Required variables are needed for auth features to work; the site degrades gracefully without them.
Required for authentication:
| Variable | Purpose |
|---|---|
OAUTH_CLIENT_ID |
OAuth client ID |
OAUTH_CLIENT_SECRET |
OAuth client secret |
OAUTH_REDIRECT_URI |
OAuth callback URL |
OAUTH_URL |
OAuth provider base URL |
SESSION_COOKIE_PASSWORD |
Encryption key for iron-session cookies |
Optional:
| Variable | Purpose |
|---|---|
MUX_API_URL |
Override Mux API endpoint (defaults to https://api.mux.com) |
MUX_TOKEN_ID |
Mux API token ID (for server-side health checks) |
MUX_TOKEN_SECRET |
Mux API token secret (for server-side health checks) |
SENTRY_AUTH_TOKEN |
Sentry error tracking auth token |
Authentication & Mux Integration
OAuth and Mux integration exist to support the video uploader on the installation page (src/components/installation/MuxUploaderPanel.tsx). This is the only consumer of the auth system.
Key files:
src/middleware/index.ts— Validates and refreshes OAuth sessions on every request viairon-sessionsrc/utils/auth.ts— Session encryption, JWKS verification, token refreshsrc/pages/api/auth/callback.ts— OAuth callback endpointsrc/actions/auth.ts—initiateLogin(),logout()server actionssrc/actions/mux.ts—createDirectUpload(),getUploadStatus(),getAssetStatus(),listAssets(),getAsset()server actions
How sessions work:
- Middleware decrypts session cookie, verifies access token via JWKS
- If expired, automatically refreshes using the refresh token
- Populates
context.locals.user(safe to render) andcontext.locals.accessToken(server-only, never expose to client) - Invalid/corrupted sessions are silently cleared
Action gating: All mux.* actions (except createDirectUpload) return 401 without a valid session. createDirectUpload handles its own auth so the client can detect UNAUTHORIZED and show a login UI.
MDX Processing Plugins
Four plugins transform MDX content during build. Registered in astro.config.mjs:
remarkConditionalHeadings (src/utils/remarkConditionalHeadings.js)
Walks the MDX AST and tracks headings inside <FrameworkCase> / <StyleCase> components, attaching conditional metadata (which frameworks/styles a heading belongs to). Also reads <ComponentReference> and <UtilReference> component props, loads the generated JSON, and injects heading entries so API reference sections appear in the table of contents. Outputs to frontmatter.conditionalHeadings.
remarkReadingTime (src/utils/remarkReadingTime.mjs)
Calculates reading time and injects frontmatter.minutesRead (text) and frontmatter.readingTimeMinutes (number).
rehypePrepareCodeBlocks (src/utils/rehypePrepareCodeBlocks.js)
Tags <code> children of <pre> with a codeBlock property, and marks <pre> blocks with hasFrame: true when inside a <TabsPanel> JSX component. This controls code block styling (framed vs. standalone).
shikiTransformMetadata (src/utils/shikiTransformMetadata.js)
Shiki transformer that extracts title="..." from code fence metadata, enabling titled code blocks:
```tsx title="Example.tsx"
Custom Astro Integration: LLM Markdown
Location: integrations/llms-markdown.ts
Generates LLM-optimized markdown files and a llms.txt index after build.
How it works:
- Scans all built HTML pages for elements with
[data-llms-content] - Strips elements with
[data-llms-ignore]from the content - Converts remaining HTML to markdown via Turndown
- Writes
.mdfiles alongside built HTML - Generates
llms.txtindex grouped by framework/style
Data attributes for content authors:
| Attribute | Purpose |
|---|---|
data-llms-content |
Mark an element's content for LLM markdown extraction |
data-llms-ignore |
Exclude an element (and its children) from LLM output |
data-llms-description |
Description text for the llms.txt index entry |
data-llms-sort |
Sort key for ordering entries in the index |
TypeScript Configuration
Path aliases (tsconfig.json):
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
Import examples:
import { sidebar } from '@/docs.config';
import type { Sidebar } from '@/types/docs';
import { filterSidebar } from '@/utils/docs/sidebar';
Strict mode enabled:
noUncheckedIndexedAccess: trueexactOptionalPropertyTypes: true
Key Architecture Patterns
1. Recursive Sidebar Filtering
Sidebar filtering is recursive because sections can contain guides or nested sections:
export function filterSidebar(
framework: SupportedFramework,
sidebarToFilter?: Sidebar,
): Sidebar {
const root = sidebarToFilter ?? sidebar;
return root
.map((item) => filterItem(item, framework))
.filter(isNotFalsy);
}
2. Type Guards for Framework/Style Validation
Defined in src/types/docs.ts:
export const FRAMEWORK_STYLES = {
html: ['css'],
react: ['css'],
} as const;
export function isValidFramework(value: unknown): value is SupportedFramework {
return typeof value === 'string' && value in FRAMEWORK_STYLES;
}
export function isValidStyleForFramework(
framework: SupportedFramework,
style: unknown,
): style is AnySupportedStyle {
return typeof style === 'string'
&& FRAMEWORK_STYLES[framework].includes(style as any);
}
3. Git-Enriched Content Metadata
Content collections automatically enrich metadata from git history:
// In content.config.ts
const blog = defineCollection({
loader: globWithParser({
pattern: '**/*.mdx',
base: './src/content/blog',
async parseData(frontmatter, fileUrl) {
const filePath = fileURLToPath(fileUrl);
const updatedDate = await getLastModifiedDate(filePath);
return { ...frontmatter, updatedDate };
},
}),
});
4. Island Architecture with React
Each React component with client:load is an independent React root:
---
import Tabs from '@/components/Tabs.tsx';
import Search from '@/components/Search/Search.tsx';
---
<Tabs client:load /> <!-- Independent React root #1 -->
<Search client:load /> <!-- Independent React root #2 -->
Consequence: React Context doesn't work across islands. Use Nanostores instead.
MDX Component Typography
Location: src/components/typography/
Standard MDX elements (headings, paragraphs, lists, etc.) are defined here and used across all MDX layouts (blog, docs, markdown pages).
Usage in layouts:
---
import defaultMarkdownComponents from '@/components/typography/defaultMarkdownComponents';
---
<Content components={{ ...defaultMarkdownComponents }} />
Important Development Notes
Writing Documentation
Read src/content/docs/how-to/write-guides.mdx for comprehensive guide-writing instructions.
Key points:
- Use
.mdxfiles only (not.md) - Use
<FrameworkCase>and<StyleCase>for framework/style-specific content - Follow Diátaxis framework: how-to vs. concept guides
- Add new guides to
src/docs.config.tssidebar - Use
devOnly: truefor internal documentation
Search Indexing
Pagefind indexes HTML files after build. During development:
- Run
pnpm buildat least once to generate search index - Dev server serves the index from previous build
- Search won't include new content until next build
Adding Framework/Style Support
To add a new framework or style:
- Update
FRAMEWORK_STYLESinsrc/types/docs.ts - Update type definitions (
SupportedFramework,AnySupportedStyle) - Add corresponding page routes in
src/pages/docs/framework/[framework]/ - Update sidebar filtering logic if needed (usually automatic)
- Update tests to include new framework/style
Blog Post Naming
CRITICAL: Blog post filenames MUST be date-prefixed:
YYYY-MM-DD-slug.mdx
The date prefix is automatically removed from the slug during content collection transformation by src/utils/globWithParser.ts.
Example:
- File:
2024-01-15-new-release.mdx - Slug:
new-release - URL:
/blog/new-release/
Development-Only Content
Use devOnly: true in frontmatter or sidebar config to hide content in production:
// In docs.config.ts
{ slug: 'how-to/write-guides', devOnly: true }
---
title: Internal Documentation
devOnly: true
---
SEO Metadata
When writing content for the site, especially page titles and descriptions, follow the conventions in .claude/skills/docs/references/seo.md for the full keyword list and guidelines.
Common Tasks
Adding a New Docs Guide
- Create MDX file in
src/content/docs/{how-to|concepts|reference}/your-guide.mdx - Add frontmatter with
titleanddescription - Add to sidebar in
src/docs.config.ts - Optional: Restrict to specific frameworks/styles
- Test with
pnpm devand verify all framework/style combinations
Running Tests for Specific Utility
# Run sidebar tests
pnpm test sidebar.test.ts
# Run in watch mode
pnpm test:watch sidebar.test.ts
# With UI
pnpm test:ui
Debugging Redirect Logic
The resolveIndexRedirect() function in src/utils/docs/routing.ts returns a reason field explaining why a particular redirect was chosen:
const result = resolveIndexRedirect({ preferences, params });
console.log(result.reason); // e.g., "using preference framework and style"
Checking TypeScript
pnpm astro check
This runs Astro's built-in TypeScript checker across .astro, .ts, and .tsx files.