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.
This commit is contained in:
604
.github/skills/astro-sites-manager/references/starlight-and-patterns.md
vendored
Normal file
604
.github/skills/astro-sites-manager/references/starlight-and-patterns.md
vendored
Normal file
@@ -0,0 +1,604 @@
|
||||
# Starlight & Common Patterns
|
||||
|
||||
## 1. Starlight Documentation Sites
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
Or add to an existing Astro project:
|
||||
|
||||
```bash
|
||||
npx astro add starlight
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```js
|
||||
// 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
|
||||
|
||||
```ts
|
||||
// ❌ 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 |
|
||||
|
||||
```mdx
|
||||
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:
|
||||
|
||||
```js
|
||||
// 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:
|
||||
|
||||
```astro
|
||||
---
|
||||
// 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](https://starlight.astro.build/reference/overrides/).
|
||||
|
||||
### 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):**
|
||||
|
||||
```css
|
||||
/* 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:
|
||||
|
||||
```css
|
||||
@layer my-reset, starlight, my-overrides;
|
||||
|
||||
@layer my-overrides {
|
||||
:root {
|
||||
--sl-content-width: 50rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Versioned Docs with starlight-utils multiSidebar
|
||||
|
||||
```bash
|
||||
npm install @lorenzo_lewis/starlight-utils
|
||||
```
|
||||
|
||||
```js
|
||||
// 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:
|
||||
|
||||
```bash
|
||||
npm install -D pagefind
|
||||
```
|
||||
|
||||
Add to your build script in `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"build": "astro build && npx pagefind --site dist"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Indexing Controls
|
||||
|
||||
```html
|
||||
<!-- 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
|
||||
|
||||
```astro
|
||||
---
|
||||
// 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](seo-full-stack.md) — 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.
|
||||
|
||||
```js
|
||||
// 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
|
||||
|
||||
```js
|
||||
// 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:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```astro
|
||||
---
|
||||
// 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
|
||||
|
||||
```astro
|
||||
---
|
||||
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
|
||||
|
||||
```astro
|
||||
---
|
||||
// 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
|
||||
|
||||
```astro
|
||||
---
|
||||
// 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:
|
||||
|
||||
```astro
|
||||
---
|
||||
// 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:**
|
||||
|
||||
```astro
|
||||
<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:**
|
||||
|
||||
```astro
|
||||
<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
|
||||
|
||||
```astro
|
||||
---
|
||||
// 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.
|
||||
Reference in New Issue
Block a user