Files
v10/site/CLAUDE.md
T

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 server
  • pnpm 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 standard text-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>
<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-bright-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
│   └── llms-markdown.ts     # LLM-optimized markdown generation
├── 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.mdx generates:
    • /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 sidebar constant of type Sidebar
  • Hierarchical: Sections contain Guides
  • Each Guide has:
    • slug: Path relative to src/content/docs/ (without .mdx)
    • sidebarLabel (optional): Override display name
    • frameworks (optional): Restrict to specific frameworks
    • devOnly (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 sections
  • findFirstGuide(): Get first available guide for framework
  • findGuideBySlug(): Search sidebar recursively for a guide
  • getAdjacentGuides(): Get prev/next guides for navigation
  • getSectionsForGuide(): Get breadcrumb trail to a guide

routing.ts — URL building and redirect logic:

  • buildDocsUrl(): Construct docs URLs from framework and slug
  • resolveIndexRedirect(): 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:

  1. URL params (framework)
  2. User preferences (framework from cookies)
  3. 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
  ogImage?: ImageMetadata | string;   // Local image or external URL
  twitterImage?: ImageMetadata | string; // Falls back to ogImage
}

OG images go in src/assets/blog/{date-slug}/og.png. Reference from frontmatter with a relative path:

ogImage: '../../assets/blog/2026-03-10-my-post/og.png'

Docs Collection (src/content/docs/)

Subdirectories:

  • how-to/ — Outcome-focused guides (per Diátaxis framework)
  • concepts/ — Understanding-focused guides
  • reference/ — 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: pubDate from filename date prefix
  • All content: updatedDate from 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 state
  • tabs.ts: Tab component state
  • installation.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

API Reference Generation

Source of truth: internal/design/site/api-docs-builder.md

The 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 referencessrc/content/generated-component-reference/{name}.json
  • Util referencessrc/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" />

Search: Algolia DocSearch v4

Config: src/search.config.ts — Algolia app ID, API key, and index names.

Component: src/components/Search.tsx — React component loaded via client:load in the NavBar.

Styles: src/styles/docsearch.css — CSS variable overrides mapping DocSearch theming to site tokens (imported via globals.css).

How it works:

  • DocSearch React component provides the trigger button, modal, and keyboard shortcut (Cmd+K)
  • Two indexes queried via the indices prop: videojs_docs (filtered by current framework) and videojs_blog (unfiltered)
  • No build-time indexing — search queries hit the Algolia API directly

Data attributes used by the crawler:

  • data-search-content — marks searchable content regions
  • data-search-ignore — excludes elements from search indexing
  • data-framework, data-site, data-category — facet/filter attributes
  • data-llms-* attributes are for the LLM markdown integration (unrelated to search)

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 via iron-session
  • src/utils/auth.ts — Session encryption, JWKS verification, token refresh
  • src/pages/api/auth/callback.ts — OAuth callback endpoint
  • src/actions/auth.tsinitiateLogin(), logout() server actions
  • src/actions/mux.tscreateDirectUpload(), getUploadStatus(), getAssetStatus(), listAssets(), getAsset() server actions

How sessions work:

  1. Middleware decrypts session cookie, verifies access token via JWKS
  2. If expired, automatically refreshes using the refresh token
  3. Populates context.locals.user (safe to render) and context.locals.accessToken (server-only, never expose to client)
  4. 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:

  1. Scans all built HTML pages for elements with [data-llms-content]
  2. Strips elements with [data-llms-ignore] from the content
  3. Converts remaining HTML to markdown via Turndown
  4. Writes .md files alongside built HTML
  5. Generates llms.txt index 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: true
  • exactOptionalPropertyTypes: 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 .mdx files 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.ts sidebar
  • Use devOnly: true for internal documentation

Search is powered by Algolia DocSearch v4 and queries the Algolia API directly — no build-time indexing step needed. Search works in both dev and production as long as the Algolia account is provisioned. See the "Search: Algolia DocSearch v4" section above for details.

Adding Framework/Style Support

To add a new framework or style:

  1. Update FRAMEWORK_STYLES in src/types/docs.ts
  2. Update type definitions (SupportedFramework, AnySupportedStyle)
  3. Add corresponding page routes in src/pages/docs/framework/[framework]/
  4. Update sidebar filtering logic if needed (usually automatic)
  5. 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

  1. Create MDX file in src/content/docs/{how-to|concepts|reference}/your-guide.mdx
  2. Add frontmatter with title and description
  3. Add to sidebar in src/docs.config.ts
  4. Optional: Restrict to specific frameworks/styles
  5. Test with pnpm dev and 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.