Files
SlipItIn/.github/skills/astro-sites-manager/references/starlight-and-patterns.md
Tim Krampitz 01046b01e4 Neue Skills, Referenzen & OpenWiki-Doku integriert
Umfangreiche Erweiterung der Skill-Bibliothek: Neue Skills für Humanisierung (Englisch/PT-BR), Design-Validierung, AI-SEO und Coolify-Deployment inkl. Regelwerke, Presets, Pattern-Referenzen, Testfälle und Automatisierungsskripte. Zusätzliche Skills für Revenue-Centric Design, Pier Cloud, OKF, Lebenslauf- und LinkedIn-Optimierung sowie zahlreiche Referenzdateien, Checklisten und YAML/JSON/Markdown-Templates. Einführung einer vollständigen OpenWiki-Dokumentation mit Architektur-, Domain- und Workflow-Beschreibungen, zentralem Index und automatisierten Updates. Modularer Aufbau, restriktive Lizenzen und umfassende Qualitäts- und Evaluationsmechanismen für alle neuen Inhalte.
2026-07-26 14:00:58 +02:00

14 KiB

Starlight & Common Patterns

1. Starlight Documentation Sites

Setup

npm create astro@latest -- --template starlight

Or add to an existing Astro project:

npx astro add starlight

Configuration

// astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';

export default defineConfig({
  site: 'https://docs.example.com',
  integrations: [
    starlight({
      title: 'My Docs',
      defaultLocale: 'en',
      locales: {
        en: { label: 'English' },
        pt: { label: 'Português', lang: 'pt-BR' },
      },
      sidebar: [
        { label: 'Home', link: '/' },
        {
          label: 'Guides',
          items: [
            { slug: 'guides/getting-started' },
            { slug: 'guides/configuration' },
          ],
        },
        {
          label: 'Reference',
          autogenerate: { directory: 'reference' },
        },
      ],
      customCss: ['./src/styles/custom.css'],
    }),
  ],
});

Sidebar Gotchas

link and items are mutually exclusive. A sidebar item is ONE of:

  • link — a single URL (requires label)
  • slug — reference to internal page (uses page title as label)
  • items — array of child links/groups (requires label)
  • autogenerate — auto-generates from a directory
// ❌ WRONG — cannot mix link with items
{ label: 'Guides', link: '/guides/', items: [...] }

// ✅ CORRECT — group with items
{ label: 'Guides', items: [{ slug: 'guides/intro' }] }

// ✅ CORRECT — single link
{ label: 'Guides', link: '/guides/' }

Autogenerate limitations:

  • Only generates from files in src/content/docs/<directory>/
  • Sorted alphabetically by filename (use numeric prefixes like 01-intro.md to control order)
  • Cannot filter files — all .md/.mdx in the directory are included
  • Subfolders become nested groups automatically

Built-in Components: Card vs LinkCard

Component Purpose Required Props Has href? Accepts children?
Card Display content in a styled box title NO Yes
LinkCard Prominent clickable link title, href YES No
import { Card, LinkCard, CardGrid } from '@astrojs/starlight/components';

{/* Card — displays content, NOT a link */}
<Card title="Feature A" icon="star">
  Description of feature A goes here.
</Card>

{/* LinkCard — entire card is a clickable link */}
<LinkCard
  title="Getting Started"
  href="/guides/getting-started/"
  description="Learn how to set up your project."
/>

{/* Group in a grid */}
<CardGrid stagger>
  <Card title="Fast" icon="rocket">Built for speed.</Card>
  <Card title="Simple" icon="pencil">Easy to use.</Card>
</CardGrid>

Component Overrides

Override any built-in Starlight UI component:

// astro.config.mjs
starlight({
  components: {
    // Replace the SocialIcons component
    SocialIcons: './src/components/MyLinks.astro',
    // Replace the Header
    Header: './src/components/CustomHeader.astro',
  },
});

Reuse the built-in component inside your override:

---
// src/components/CustomHeader.astro
import Default from '@astrojs/starlight/components/Header.astro';
---
<Default><slot /></Default>
<div class="announcement-bar">New release available!</div>

Full list of overridable components: see Overrides Reference.

Theming

Starlight uses a semantic color system via CSS custom properties. The naming is counter-intuitive:

Variable Meaning
--sl-color-white Foreground (text) color
--sl-color-black Background color
--sl-color-gray-1 to --sl-color-gray-6 Gray scale (1 = lightest in dark mode)
--sl-color-accent-low Accent background
--sl-color-accent Accent mid (links, highlights)
--sl-color-accent-high Accent foreground

You MUST define both :root (dark) and :root[data-theme='light'] (light):

/* src/styles/custom.css */

/* Dark mode (default) */
:root {
  --sl-color-white: #ffffff;
  --sl-color-black: #181818;
  --sl-color-gray-1: #eee;
  --sl-color-gray-2: #c2c2c2;
  --sl-color-gray-3: #8b8b8b;
  --sl-color-gray-4: #585858;
  --sl-color-gray-5: #383838;
  --sl-color-gray-6: #272727;
  --sl-color-accent-low: #1a1047;
  --sl-color-accent: #8b5cf6;
  --sl-color-accent-high: #c4b5fd;
}

/* Light mode — invert the logic */
:root[data-theme='light'] {
  --sl-color-white: #181818;
  --sl-color-black: #ffffff;
  --sl-color-gray-1: #272727;
  --sl-color-gray-2: #383838;
  --sl-color-gray-3: #585858;
  --sl-color-gray-4: #8b8b8b;
  --sl-color-gray-5: #c2c2c2;
  --sl-color-gray-6: #eee;
  --sl-color-accent-low: #c4b5fd;
  --sl-color-accent: #6d28d9;
  --sl-color-accent-high: #1a1047;
}

CSS Layer: Starlight uses @layer starlight internally. Unlayered custom CSS automatically overrides it. For explicit layer control:

@layer my-reset, starlight, my-overrides;

@layer my-overrides {
  :root {
    --sl-content-width: 50rem;
  }
}

Versioned Docs with starlight-utils multiSidebar

npm install @lorenzo_lewis/starlight-utils
// astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import starlightUtils from '@lorenzo_lewis/starlight-utils';

export default defineConfig({
  integrations: [
    starlight({
      title: 'My Docs',
      plugins: [
        starlightUtils({
          multiSidebar: {
            switcherStyle: 'dropdown',
          },
        }),
      ],
      sidebar: [
        // Each top-level group becomes a separate sidebar
        {
          label: 'v2',
          items: [{ autogenerate: { directory: 'v2' } }],
        },
        {
          label: 'v1',
          items: [{ autogenerate: { directory: 'v1' } }],
        },
      ],
    }),
  ],
});

2. Search (Pagefind)

Install and Build

Pagefind indexes static HTML after build. Starlight includes Pagefind by default. For non-Starlight Astro sites:

npm install -D pagefind

Add to your build script in package.json:

{
  "scripts": {
    "build": "astro build && npx pagefind --site dist"
  }
}

Indexing Controls

<!-- Only index content inside this element -->
<main data-pagefind-body>
  <h1>Indexed heading</h1>
  <p>This paragraph is searchable.</p>

  <!-- Exclude specific elements -->
  <nav data-pagefind-ignore>
    <p>This won't appear in search results.</p>
  </nav>

  <!-- Boost heading weight in results -->
  <h2 data-pagefind-weight="2">Important Section</h2>
</main>
Attribute Effect
data-pagefind-body Only index inside this element (page-level)
data-pagefind-ignore Exclude element from indexing
data-pagefind-ignore="all" Exclude element and all descendants
data-pagefind-weight="N" Boost ranking (default: 1, higher = more relevant)
data-pagefind-meta="key:value" Add metadata to search results

UI Component Integration

---
// src/pages/search.astro
---
<html>
<head>
  <link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
</head>
<body>
  <div id="search"></div>
  <script>
    import '/pagefind/pagefind-ui.js';
    new PagefindUI({ element: '#search', showSubResults: true });
  </script>
</body>
</html>

Pagefind vs Fuse.js Decision Table

Criteria Pagefind Fuse.js
Index size Pre-built, loads fragments on demand Entire index in memory
Best for Static sites with 50+ pages Small datasets (<100 items), dynamic data
Setup Build step required No build step, works at runtime
Fuzzy matching Limited (typo tolerance) Excellent (configurable threshold)
Performance O(1) per query chunk (WASM) Degrades with data size
Works offline Yes Yes
SSR compatible No (needs static HTML) Yes
Custom data Indexes HTML only Indexes any JSON array
Bundle size ~50KB (WASM) + on-demand chunks ~25KB + full index

Rule of thumb: Use Pagefind for documentation/blog search. Use Fuse.js for in-page filtering (command palettes, dropdown search, dynamic lists).


3. SEO

Full reference: see SEO Full Stack — covers @jdevalk/astro-seo-graph, JSON-LD graph, IndexNow, auto-generated OG images, agent discovery, performance SEO, and build-time validation.

Below is just the minimal setup for Starlight (which already includes automatic sitemap):

Starlight SEO Basics

Starlight generates a sitemap automatically — just set site in your config. For RSS and custom meta tags in non-Starlight sites, see the full reference file.

// astro.config.mjs — minimum for Starlight SEO
export default defineConfig({
  site: 'https://docs.example.com', // Required for sitemap and canonical
  integrations: [starlight({ title: 'My Docs' })],
});

4. i18n Patterns

Configuration

// astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'pt-br', 'es'],
    routing: {
      prefixDefaultLocale: false, // /about (en), /pt-br/about, /es/about
    },
    fallback: {
      'pt-br': 'en',
      es: 'en',
    },
  },
});

For Starlight, i18n is configured inside the integration:

starlight({
  defaultLocale: 'root',
  locales: {
    root: { label: 'English', lang: 'en' },
    'pt-br': { label: 'Português', lang: 'pt-BR' },
  },
});

Content Collections per Locale

src/content/docs/
├── index.md           ← English (root locale)
├── guides/
│   └── intro.md
└── pt-br/
    ├── index.md       ← Portuguese
    └── guides/
        └── intro.md

Fallback Strategy

Show default locale content with a banner when translation is missing:

---
// src/components/TranslationBanner.astro
import { getEntry } from 'astro:content';

const currentLocale = Astro.currentLocale ?? 'en';
const slug = Astro.params.slug;

// Check if translation exists
const localizedEntry = await getEntry('docs', `${currentLocale}/${slug}`);
const isFallback = !localizedEntry && currentLocale !== 'en';
---

{isFallback && (
  <aside class="translation-banner" role="alert">
    ⚠️ This page is not yet translated to {currentLocale}.
    Showing English version.
  </aside>
)}

In Starlight, fallback is automatic — missing translations show the defaultLocale content with a built-in notice.

getRelativeLocaleUrl Helper

---
import { getRelativeLocaleUrl } from 'astro:i18n';

const locale = Astro.currentLocale ?? 'en';
---
<nav>
  <a href={getRelativeLocaleUrl(locale, 'about')}>About</a>
  <a href={getRelativeLocaleUrl(locale, 'guides/intro')}>Guide</a>
</nav>

5. Common Recipes

Pagination

---
// src/pages/blog/[...page].astro
import { getCollection } from 'astro:content';
import type { GetStaticPaths } from 'astro';

const POSTS_PER_PAGE = 10;

export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
  const allPosts = await getCollection('blog');
  const sorted = allPosts.sort(
    (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf()
  );
  return paginate(sorted, { pageSize: POSTS_PER_PAGE });
};

const { page } = Astro.props;
---
<h1>Blog — Page {page.currentPage}</h1>

<ul>
  {page.data.map((post) => (
    <li>
      <a href={`/blog/${post.id}/`}>{post.data.title}</a>
    </li>
  ))}
</ul>

<nav>
  {page.url.prev && <a href={page.url.prev}>← Previous</a>}
  <span>Page {page.currentPage} of {page.lastPage}</span>
  {page.url.next && <a href={page.url.next}>Next →</a>}
</nav>

Tag/Category Archives

---
// src/pages/tags/[tag]/[...page].astro
import { getCollection } from 'astro:content';

export async function getStaticPaths({ paginate }) {
  const allPosts = await getCollection('blog');
  const allTags = [...new Set(allPosts.flatMap((post) => post.data.tags))];

  return allTags.flatMap((tag) => {
    const filtered = allPosts.filter((post) => post.data.tags.includes(tag));
    return paginate(filtered, {
      params: { tag },
      pageSize: 10,
    });
  });
}

const { page } = Astro.props;
const { tag } = Astro.params;
---
<h1>Posts tagged "{tag}"</h1>

<ul>
  {page.data.map((post) => (
    <li><a href={`/blog/${post.id}/`}>{post.data.title}</a></li>
  ))}
</ul>

Tag index page:

---
// src/pages/tags/index.astro
import { getCollection } from 'astro:content';

const allPosts = await getCollection('blog');
const tags = [...new Set(allPosts.flatMap((post) => post.data.tags))].sort();
---
<h1>All Tags</h1>
<ul>
  {tags.map((tag) => (
    <li><a href={`/tags/${tag}/1/`}>{tag}</a></li>
  ))}
</ul>

Static Forms

Formspree:

<form action="https://formspree.io/f/{form_id}" method="POST">
  <label>
    Email
    <input type="email" name="email" required />
  </label>
  <label>
    Message
    <textarea name="message" required></textarea>
  </label>
  <button type="submit">Send</button>
</form>

Netlify Forms:

<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field">
  <input type="hidden" name="form-name" value="contact" />
  <p class="hidden"><input name="bot-field" /></p>
  <label>
    Email
    <input type="email" name="email" required />
  </label>
  <label>
    Message
    <textarea name="message" required></textarea>
  </label>
  <button type="submit">Send</button>
</form>

Dark Mode Toggle

---
// src/components/ThemeToggle.astro
---
<button id="theme-toggle" aria-label="Toggle dark mode" type="button">
  <span class="sun">☀️</span>
  <span class="moon">🌙</span>
</button>

<script>
  const toggle = document.getElementById('theme-toggle')!;

  function getTheme(): 'light' | 'dark' {
    return (
      (localStorage.getItem('theme') as 'light' | 'dark') ??
      (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
    );
  }

  function setTheme(theme: 'light' | 'dark') {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem('theme', theme);
  }

  // Apply on load
  setTheme(getTheme());

  toggle.addEventListener('click', () => {
    setTheme(getTheme() === 'dark' ? 'light' : 'dark');
  });
</script>

<style>
  #theme-toggle {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 1.25rem;
  }
  :root[data-theme='dark'] .sun { display: none; }
  :root[data-theme='light'] .moon { display: none; }
</style>

Note: Starlight includes a built-in theme toggle. This pattern is for custom Astro sites.