Integration Config
The integration accepts a single config object combining site metadata and optional features. site must be set at the top level of defineConfig — Astro uses it to construct absolute URLs and the integration enforces this:
// astro.config.mjs
export default defineConfig({
site: "https://your-name.github.io", // required
integrations: [docsTheme({ ... })],
});
docsTheme({
// Required: project metadata shown in header/footer
project: {
name: "my-project",
description: "What it does",
license: { name: "MIT", url: "https://..." },
// GitHub repo info. Used to derive site URL and GitHub link.
github: {
user: "psd-coder", // one of user/organization required
// organization: "my-org",
repository: "my-project",
},
},
// Optional: author link in header/footer. Omit to render no author.
author: {
name: "Your Name",
url: "https://x.com/your_handle",
// icon: path to SVG file rendered inline. Overrides the x.com auto-icon
// when set, and is required when url is not an x.com URL.
// icon: "src/assets/author.svg",
},
// Optional: additional credits rendered as "& Name" after author in footer
credits: [{ name: "Evil Martians", url: "https://evilmartians.com/" }],
// Optional: path to SVG file rendered as the header logo.
// Replaces the default project name text. The logo slot in Layout still overrides this.
// logo: "src/assets/logo.svg",
// Optional: show hue slider in header to pick a theme hue.
// Use it to find the right value, then set theme.hue in config and remove this.
huePicker: true,
// Optional: enable Astro ViewTransitions. Default: true.
// clientRouter: false,
// Optional: enable full-text search UI in header. Default: true.
// search: false,
// Optional: theme tokens. Hue feeds CSS variables and the auto OG template.
theme: {
// hue: 220,
// Syntax highlighting themes (overrides adaptive hue-based theme)
// shiki: { light: "github-light", dark: "github-dark" },
// Inject bundled Martian Grotesk + Mono fonts. Default: true.
// fonts: false,
// CSS files to inject into every page. Paths relative to project root.
// customCss: ["src/styles/custom.css"],
},
// Optional: SEO / meta settings
meta: {
// HTML lang attribute. Default: "en".
// lang: "fr",
// Appended as " | {suffix}" to every sub-page <title>. false = no suffix.
// Default: project.name.
// titleSuffix: "My Project — Docs",
// titleSuffix: false,
// Full <title> for the root/index page, bypassing the normal "{page} | {suffix}" pattern.
// Default: "{project.name} Documentation".
// mainPageTitle: "My Project — Fast & Simple",
// Source icon(s) for favicons, apple-touch-icon, and webmanifest.
// String form: single 512x512 PNG/SVG used for all sizes.
icon: "src/assets/icon.svg",
// Object form: separate sources. `favicon` is used for tiny renders
// (favicon.svg, favicon.ico); `manifest` is used for 96px and up.
// icon: {
// favicon: "src/assets/favicon.svg",
// manifest: "src/assets/icon-detailed.svg",
// },
// Open Graph image. Defaults to true when unset. Three forms:
// string -> path to PNG, served at /og.png
// true -> built-in template, uses top-level `logo` if set
// object -> override any of: template, logo, title, description
// Object fields (all optional):
// logo: string path / false (opt out) / absent (uses top-level `logo`)
// template: path to .ts default-exporting OgTemplateFn
// title: replace project.name shown on the card
// description: replace project.description shown on the card
// og: { image: { logo: "src/assets/og-logo.svg", title: "My App" }, imageAlt: "Alt" },
// Twitter card. Image defaults to og.image.
// twitter: { site: "@my_org", creator: "@me" },
},
// Optional: docs collection settings (all fields optional, sensible defaults applied)
docs: {
directory: "src/content/docs", // default; also controls defineDocsCollections() glob base
renderDefaultPage: true, // default; set false to ship your own [...slug].astro
// Optional: header navigation links (href accepts "/api" or "api")
navLinks: [
{ href: "/", label: "Overview" },
{ href: "/api", label: "API" },
],
// Optional: extra entries for search index + llms.txt from non-collection pages.
// Path to a module that default-exports ExtraEntry[] or () => Promise<ExtraEntry[]>.
// extraEntries: "./src/extra-entries.ts",
},
});
What the integration does
- Stores config in a virtual module (
virtual:pigment-config) so components read it automatically - Requires
siteinastro.config.mjs; auto-setsbasefrom GitHub config (/repo/in CI,/in dev) - Injects rehype-slug + rehype-autolink-headings
- Injects an adaptive Shiki theme that derives syntax colors from
--theme-hue(based on Catppuccin, hue-rotated via OKLch). Override withtheme.shikito use fixed themes instead. - Injects PostCSS preset-env (nesting, custom-media, media-query-ranges)
- Injects sitemap +
llms.txt,llms-full.txt,[slug].mdroutes - Injects
/[...slug]page that renders docs from the content collection (opt out withdocs.renderDefaultPage: false) - When
search: true(default): injects/search-index.jsonand renders a search input in the Layout header - When
clientRouter: true(default): enables Astro View Transitions via<ClientRouter /> - When
meta.iconis configured: generates favicons (svg, ico, 96x96 png), apple-touch-icon, webmanifest + manifest icons (192x192, 512x512) - Always serves
/robots.txt(permissive, withSitemap:pointing at/sitemap-index.xml) - Always serves
/og.png(built-in template by default, or whatevermeta.og.imagespecifies) and emits<meta property="og:image">at 1200x630 - Twitter falls back to the OG image; emits
summary_large_imageTwitter card tags. Override withmeta.twitter.imageto use a different mode/path
CSS Customization
The theme uses CSS variables with fallback defaults. Override them in your own CSS:
:root {
--layout-width-override: 1280px; /* wider layout */
--layout-sidebar-width-override: 280px;
}
For hue, use theme.hue in the integration config — it’s the single source of truth for both the site CSS and the auto-generated OG image. All color tokens derive from --theme-hue using OKLch, so changing the hue recolors the entire site, including syntax highlighting in code blocks.
Picking a hue
Enable huePicker: true in the integration config to show a hue slider in the header. Drag it to find the right value, then set theme.hue in config and remove huePicker:
docsTheme({
// ...
theme: { hue: 135 },
});
The slider persists its value to localStorage, so you can test across page loads. Once you’ve settled on a hue, turn it off: the slider is a setup tool, not a production feature.
Color tokens
| Token | Light | Dark |
|---|---|---|
--color-surface-1 | 99% lightness | 12% lightness |
--color-surface-2 | 98% | 18% |
--color-surface-3 | 96% | 21% |
--color-accent | 55% | 65% |
--color-text-primary | 15% | 90% |
--color-text-secondary | 40% | 75% |
--color-border | 90% | 25% |
Typography and spacing
Text sizes from --text-xxs (0.625rem) to --text-2xl (2rem). Spacing base --spacing is 4px. Border radii: --radius-sm (4px), --radius-md (8px).
Responsive breakpoints
@custom-media --mobiles (max-width: 48rem); /* <768px */
@custom-media --tablets (max-width: 64rem); /* <1024px */
@custom-media --laptops (max-width: 80rem); /* <1280px */
Import astro-pigment/styles/media.css to use these in your own CSS.
Fonts
The integration auto-injects bundled Martian Grotesk (variable weight sans) and Martian Mono (400 monospace), setting --font-sans and --font-mono CSS variables. Pass theme.fonts: false to opt out and provide your own via Astro’s top-level fonts field.
// Opt out of bundled fonts and use your own
docsTheme({
// ...
theme: { fonts: false },
}),
// then in defineConfig:
fonts: [
{ provider: fontProviders.google(), name: "Inter", cssVariable: "--font-sans" },
{ provider: fontProviders.google(), name: "Fira Code", cssVariable: "--font-mono" },
],
Stores
Reactive state stores available for client-side code:
// Theme store
import { $themeSetting, $resolvedTheme, cycleTheme } from "astro-pigment/stores/theme";
$themeSetting.get(); // "auto" | "light" | "dark"
$resolvedTheme.get(); // "light" | "dark"
cycleTheme(); // auto -> light -> dark -> auto
// Media queries
import { $prefersDarkScheme, $prefersReducedMotion } from "astro-pigment/stores/media";
$prefersDarkScheme.get(); // boolean
$prefersReducedMotion.get(); // boolean
Both stores are powered by nanostores. Theme preference persists to localStorage.
Package manager store
From astro-pigment/stores/pkgManager:
import { $pkgManager } from "astro-pigment/stores/pkgManager";
$pkgManager.get(); // "pnpm" | "npm" | "yarn" | "bun"
Used by InstallPackage internally. Also available for custom CodePanels-based tab switchers via defineCodePanels:
import { defineCodePanels } from "astro-pigment/utils/defineCodePanels";
import { $pkgManager } from "astro-pigment/stores/pkgManager";
defineCodePanels("x-my-switcher", $pkgManager);
SEO and LLM Endpoints
When docs is configured in the integration, four endpoints are auto-generated:
| Endpoint | Description |
|---|---|
/llms.txt | Structured index: project name, description, per-doc ## sections |
/llms-full.txt | All docs concatenated into a single markdown file |
/[slug].md | Individual markdown for each doc (and extra entries with body) |
/sitemap-index.xml | Standard sitemap via @astrojs/sitemap |
Docs are sorted by the order field in frontmatter. Frontmatter and import statements are stripped from the output.
When docs.extraEntries is configured, those entries are appended after docs in llms.txt and llms-full.txt. Entries with a body field also get individual /[id].md routes.
Extra Entries
Each entry has id (URL path segment), title, description, order, and optional body (markdown). Pass a static array for simple cases, or a module path for dynamic data:
// src/extra-entries.ts
import type { ExtraEntry } from "astro-pigment";
import { getCollection } from "astro:content";
export default async function (): Promise<ExtraEntry[]> {
const examples = await getCollection("examples");
return examples.map((ex) => ({
id: `examples/${ex.id}`,
title: ex.data.title,
description: ex.data.description,
order: 101,
}));
}
The module must default-export an ExtraEntry[] or a function returning Promise<ExtraEntry[]>.
Search
Full-text search is enabled by default (search: true). When enabled:
- Injects a
/search-index.jsonendpoint built from all docs (andextraEntries) at build time - Renders a search input in the Layout header that queries the index client-side
- Extra entries with
bodyare split into sections like docs; entries withoutbodyare indexed by title and description
To disable:
docsTheme({
search: false,
// ...
});
Favicon and Webmanifest
meta.icon accepts either a single source path or an object with two sources:
// Single source (same icon for all sizes)
meta: { icon: "src/assets/icon.svg" }
// Two sources — simplified design for tiny favicons, detailed for manifest
meta: {
icon: {
favicon: "src/assets/favicon.svg",
manifest: "src/assets/icon-detailed.svg",
},
}
Use the object form when a detailed 512x512 design becomes illegible at 16-32px. Both favicon and manifest are required in the object form.
| File | Source | Size/Format |
|---|---|---|
/favicon.svg | favicon | Passthrough (SVG source only) |
/favicon.ico | favicon | 32x32 ICO |
/favicon-96x96.png | manifest | 96x96 PNG |
/apple-touch-icon.png | manifest | 180x180 PNG |
/web-app-manifest-192x192.png | manifest | 192x192 PNG |
/web-app-manifest-512x512.png | manifest | 512x512 PNG |
/site.webmanifest | — | JSON manifest with project name and icon refs |
The Layout <head> renders the corresponding <link> tags only when meta.icon is set. Uses sharp for image resizing (bundled with the theme).
Open Graph and Twitter Cards
/og.png is always served. By default it uses a built-in satori template; override via meta.og.image:
Tip: in most cases, provide a dedicated
image.logo. Your site logo is tuned for the site’s own surface colors (light/dark neutrals), but the OG card uses a theme-hue-tinted background — a logo that reads cleanly on the site can lose contrast or clash on the card. An OG-specific variant with a palette tuned for that background keeps the mark legible across anytheme.hue.
// Default: built-in template with fallback logo. No config needed.
// meta: { og: { image: true } }
// Static PNG (served as-is)
meta: { og: { image: "src/assets/og.png" } };
// Built-in template with overrides (all fields optional)
meta: {
og: {
image: {
logo: "src/assets/og-logo.png", // string path | false (skip) | absent (fallback)
title: "My App", // override project.name on the card
description: "Short pitch", // override project.description on the card
},
},
};
// Custom satori template (same override fields apply)
meta: {
og: {
image: {
template: "./src/og-template.ts",
title: "My App",
},
},
};
Built-in and template modes require sharp (same peer dep as meta.icon). The built-in template uses theme.hue for color, bundled Martian Grotesk + Mono, and project.{name,description}. Logo resolution: explicit image.logo (string) → top-level logo → nothing. The resolved logo is rendered in its original colors (no processing) and replaces the project name; pass image: { logo: false } to render only the project name even when a top-level logo is set.
The built-in OG template always uses the bundled Martian fonts — it does not pick up whatever you’ve configured in Astro’s top-level fonts option. Astro’s font pipeline emits woff2 only, but satori requires TTF/OTF, so server-side image generation is decoupled from browser-served fonts. To use different typography in your OG card, switch to a custom template (mode 2 above) and load your own TTF/OTF buffers inside it — ctx.fonts is provided as a convenience but isn’t required.
Custom templates default-export an OgTemplateFn:
// src/og-template.ts
import type { OgTemplateFn } from "astro-pigment";
const template: OgTemplateFn = (ctx) => ({
type: "div",
props: {
style: {
display: "flex",
width: "1200px",
height: "630px",
color: "white",
background: `hsl(${ctx.hue} 30% 12%)`,
},
children: ctx.projectName,
},
});
export default template;
Satori’s color parser doesn’t handle oklch() — stick to hex, rgb(), or hsl() inside templates.
ctx exposes projectName, description, siteUrl, pathname, logo ({ src: dataUri, width, height } if meta.og.logo resolves, else undefined), hue, and fonts (Martian Grotesk Std Regular/Bold, Martian Mono Regular) for satori.
Twitter falls back to the OG image when meta.twitter.image is unset; set it independently to use a different mode/path. Twitter card is always summary_large_image when an image is present.
Examples Loader
For sites with interactive code examples, import the content collection loader:
// content.config.ts
import { defineCollection } from "astro:content";
import { z } from "astro/zod";
import { examplesLoader } from "astro-pigment/loaders/examples";
const examples = defineCollection({
loader: examplesLoader("src/content/examples/"),
schema: z.object({
title: z.string(),
description: z.string(),
descriptionHtml: z.string().default(""),
files: z.array(
z.object({
name: z.string(),
type: z.enum(["html", "javascript", "css", "importmap"]),
lang: z.enum(["html", "javascript", "css"]),
content: z.string(),
}),
),
}),
});
The loader parses .html files with data-type attributes into FileEntry arrays compatible with the CodeExample playground component. An element with id="description" is extracted as descriptionHtml (rich HTML description); if absent, descriptionHtml falls back to the plain-text description. Requires linkedom (bundled with the theme).
Exportable Configs
Stylelint config with CSS standards, clean ordering, and CSS Modules support:
// stylelint.config.js
export default { extends: ["astro-pigment/stylelint.config"] };
Browserslist targeting last 2 versions:
// .browserslistrc
extends astro-pigment/browserslist