Docs Layout Conventions

Follow these conventions when creating or customizing docs layouts.

File Naming

Layout Files

src/layouts/docs/styles/{style_name}/
├── Layout.astro      # Required - main entry point
├── layout.css        # Optional - layout-specific styles
└── index.ts          # Optional - exports

Rules:

  • Style folder name uses snake_case: doc_style1, minimal_docs
  • Main component must be named Layout.astro (exact match)
  • CSS file is optional but recommended for layout-specific styles

Component Files

src/layouts/docs/components/{component}/{variant}/
├── {Component}.astro    # PascalCase component name
└── styles.css           # Associated styles

Examples:

  • sidebar/default/Sidebar.astro
  • sidebar/modern/Sidebar.astro
  • body/compact/Body.astro

Props Interface

All docs layouts must implement this exact interface:

interface Props {
  title: string;
  description?: string;
  dataPath: string;
  baseUrl: string;
  currentSlug: string;
  content: string;
  headings?: { depth: number; slug: string; text: string }[];
}

Do not:

  • Add required props not passed by the route handler
  • Rename existing props
  • Change prop types

You can:

  • Ignore props you don't need
  • Set defaults for optional props

Style Import Order

Import styles in this order for proper cascade:

---
// 1. Component styles (reusable)
import '../../components/sidebar/default/styles.css';
import '../../components/body/default/styles.css';
import '../../components/outline/default/styles.css';
import '../../components/common/styles.css';

// 2. Layout-specific styles (overrides)
import './layout.css';
---

CSS Class Naming

Use BEM-style naming for layout and component classes:

/* Block */
.docs-layout { }

/* Block with modifier */
.docs-layout--minimal { }

/* Element */
.docs-layout__sidebar { }

/* Element with modifier */
.docs-layout__sidebar--collapsed { }

Component-Specific Prefixes

Component Prefix
Sidebar .sidebar-*
Body .docs-*
Outline .outline-*
Pagination .pagination-*

CSS Variables

Use CSS variables for themeable values:

.docs-layout {
  /* Spacing */
  --docs-sidebar-width: 280px;
  --docs-outline-width: 220px;
  --docs-content-max-width: 800px;
  --docs-gap: 2rem;

  /* Colors (inherit from globals) */
  background: var(--color-bg-primary);
  color: var(--color-text-primary);
}

Responsive Design

Breakpoints

/* Mobile first */
.docs-layout {
  display: block;  /* Stack on mobile */
}

/* Tablet */
@media (min-width: 768px) {
  .docs-layout {
    display: grid;
    grid-template-columns: 1fr 220px;  /* Body + Outline */
  }
}

/* Desktop */
@media (min-width: 1024px) {
  .docs-layout {
    grid-template-columns: 280px 1fr 220px;  /* Sidebar + Body + Outline */
  }
}

Mobile Considerations

  • Hide sidebar by default, show via hamburger menu
  • Hide outline or move to collapsible section
  • Ensure touch targets are at least 44px
  • Test pagination on narrow screens

Loading Data

Always Use Error Handling

let allContent: LoadedContent[] = [];
let settings: ContentSettings = {};

try {
  const result = await loadContentWithSettings(dataPath);
  allContent = result.content;
  settings = result.settings;
} catch (error) {
  console.error('Error loading docs content:', error);
}

Check for Empty States

{sidebarNodes.length > 0 ? (
  <Sidebar nodes={sidebarNodes} currentPath={currentPath} />
) : (
  <div class="sidebar-empty">No navigation available</div>
)}

Accessibility

Semantic HTML

<!-- Good -->
<nav class="sidebar" aria-label="Documentation navigation">
<main class="docs-content">
<article class="docs-article">

<!-- Avoid -->
<div class="sidebar">
<div class="docs-content">
<div class="docs-article">

ARIA Labels

<nav class="outline" aria-label="Table of contents">
<nav class="pagination" aria-label="Page navigation">

Keyboard Navigation

Ensure all interactive elements are keyboard accessible:

<button
  class="sidebar-toggle"
  aria-expanded={isExpanded}
  aria-controls="sidebar-section-1"
>

Performance

Lazy Load Heavy Components

{/* Only render outline if there are headings */}
{outlineHeadings.length > 0 && (
  <Outline headings={outlineHeadings} />
)}

Minimize Client JavaScript

Docs layouts should work without JavaScript when possible:

<!-- Use CSS for collapse instead of JS -->
<details class="sidebar-section">
  <summary>Section Title</summary>
  <ul>...</ul>
</details>

Testing Checklist

Before shipping a new layout:

  • Renders correctly with minimal content (1 page)
  • Renders correctly with large content (50+ pages)
  • Sidebar navigation works
  • Current page is highlighted
  • Outline links jump to correct sections
  • Pagination shows correct prev/next
  • Responsive: works on mobile, tablet, desktop
  • Dark mode: colors work in both themes
  • Keyboard: all interactive elements accessible
  • Print: content is printable

Example: Complete Layout

---
/**
 * My Custom Docs Layout
 * Based on doc_style1 with custom modifications
 */
import Sidebar from '../../components/sidebar/default/Sidebar.astro';
import Body from '../../components/body/default/Body.astro';
import Outline from '../../components/outline/default/Outline.astro';
import Pagination from '../../components/common/Pagination.astro';

import '../../components/sidebar/default/styles.css';
import '../../components/body/default/styles.css';
import '../../components/outline/default/styles.css';
import '../../components/common/styles.css';
import './layout.css';

import { loadContentWithSettings, type LoadedContent, type ContentSettings } from '@loaders/data';
import { buildSidebarTree, getPrevNext } from '@/hooks/useSidebar';

interface Props {
  title: string;
  description?: string;
  dataPath: string;
  baseUrl: string;
  currentSlug: string;
  content: string;
  headings?: { depth: number; slug: string; text: string }[];
}

const { title, description, dataPath, baseUrl, currentSlug, content, headings = [] } = Astro.props;

// Load data with error handling
let allContent: LoadedContent[] = [];
let settings: ContentSettings = {};

try {
  const result = await loadContentWithSettings(dataPath);
  allContent = result.content;
  settings = result.settings;
} catch (error) {
  console.error('Error loading docs content:', error);
}

// Build navigation
const sidebarNodes = buildSidebarTree(allContent, baseUrl, dataPath);
const currentPath = `${baseUrl}/${currentSlug}`;
const { prev, next } = getPrevNext(sidebarNodes, currentPath);

// Configure features from settings
const outlineEnabled = settings.outline?.enabled !== false;
const outlineLevels = settings.outline?.levels || [2, 3];
const outlineHeadings = headings.filter(h => outlineLevels.includes(h.depth));
const paginationEnabled = settings.pagination?.enabled !== false;
---

<div class="docs-layout">
  <Sidebar nodes={sidebarNodes} currentPath={currentPath} />

  <Body title={title} description={description} content={content}>
    {paginationEnabled && (prev || next) && (
      <Pagination prev={prev} next={next} />
    )}
  </Body>

  {outlineEnabled && outlineHeadings.length > 0 && (
    <Outline headings={outlineHeadings} title={settings.outline?.title} />
  )}
</div>