Configuration
The site
import { defineConfig, markdown } from "sourcey";
export default defineConfig({
name: "Cheese Store",
siteUrl: "https://docs.cheese.example",
prettyUrls: "slash",
repo: "https://github.com/cheese/docs",
editBranch: "main",
editBasePath: "docs",
navigation: {
tabs: [{ tab: "Documentation", slug: "", source: markdown({ groups: [{ group: "Start", pages: ["introduction"] }] }) }],
},
});navigation.tabs is the one setting a site needs; Navigation describes its tabs, groups and pages. name is shown beside the logo and ends each page's title, after the page's own title and titleSeparator (- by default). siteUrl is the site's public origin, which its canonical URLs, social cards, language alternates and sitemap name; baseUrl is the path it is served under when that is not the origin's root, such as /docs. prettyUrls chooses how pages are written and linked: false, the default, writes guide.html and links it as written; "slash" writes guide/index.html and links /guide/; "strip" writes guide.html and links /guide, for a host that serves a page without its extension.
repo, editBranch and editBasePath give each page its "Edit this page" link: the repository's address on GitHub, its branch, and the folder within it that holds the config, when that is not the repository's root.
Social cards
Each page gets a social card drawn when the site builds, with its title, its description and the site's logo and colours. ogImage: "./social.png" uses one image for every page instead: a local path is published with the site, relative to the config, and an address is used as given.
Documentation exports and discovery
Markdown views, documents.json, llms.txt and llms-full.txt are generated by default. Control them with exports; opt into public source contracts and discovery with:
exports: { markdown: true, llms: { index: true, full: true }, openapi: true },
discovery: { ard: true, apiCatalog: true },Discovery requires siteUrl. A build mounted beneath another website emits scoped manifests; wellKnown: true is reserved for a build that owns the origin root. Full configuration and deployment guide.
Sourcey reads sourcey.config.ts from your project and keeps the structure explicit. Each tab has one source, usually created with markdown(), openapi(), mcp(), doxygen(), godoc(), or rustdoc().
import { defineConfig, doxygen, godoc, markdown, mcp, openapi, rustdoc } from "sourcey";
export default defineConfig({
name: "My API",
navigation: {
tabs: [
{
tab: "Docs",
slug: "",
source: markdown({
groups: [
{
group: "Getting Started",
pages: ["introduction", "quickstart"],
},
],
}),
},
{
tab: "API Reference",
slug: "api",
source: openapi("./openapi.yaml"),
},
{
tab: "MCP Reference",
slug: "mcp",
source: mcp("./mcp.json"),
},
{
tab: "C++ Reference",
slug: "cpp",
source: doxygen({
xml: "./build/doxygen/xml",
language: "cpp",
index: "rich",
sourceUrl: [
{ prefix: "third_party/private/" },
{ prefix: "", url: "https://github.com/acme/project/blob/main/{fullPath}" },
],
}),
},
{
tab: "Rust API",
slug: "rust-api",
source: rustdoc({
manifest: "../crates/Cargo.toml",
crates: ["my-crate", "my-other-crate"],
snapshot: "./snapshots/rustdoc.json",
mode: "auto",
features: { list: ["full"] },
sourceBasePath: "crates",
doctestsIndex: true,
}),
},
],
},
});Sources
Each tab reads one source:
| Source | Reads | See |
|---|---|---|
markdown({ groups }) | Markdown pages | Navigation |
openapi(spec) | An OpenAPI 3.x or Swagger 2.0 spec, a file or a URL | OpenAPI |
mcp(spec) | An MCP server snapshot | MCP |
doxygen(config) | Doxygen XML for C, C++ or Java | Doxygen guide |
godoc(config) | A Go module, live or from a snapshot | Go guide |
rustdoc(config) | Rust crates, live or from a snapshot | Rustdoc adapter |
doxygen()
doxygen().sourceUrl accepts a base URL string, a resolver function, or a
route map. Route maps use longest-prefix matching. {path} expands to the path
after the matched prefix, {fullPath} expands to the original Doxygen path,
and {line} expands to the source line. A route without url suppresses the
public link while generated pages still show Defined in path:line.
doxygen().language is "cpp", the default, for C and C++, or "java".
Each group, namespace and type gets a page, and the functions, variables and
macros at global scope that no group claims share a Globals page beside the
overview. A namespace that only wraps others gets no page of its own.
Logo and favicon
logo: { light: "./brand/light.svg", dark: "./brand/dark.svg" },
favicon: "./brand/favicon.svg",Local images are published once as content-addressed files shared across pages. Browsers can cache them while readers move through the site; the HTML contains their URLs. Editing an image updates its URL and live preview automatically. The same frozen bytes feed generated social images. HTTP URLs and explicitly authored data URLs remain supported; a social image fetches a logo named by URL once per build. Social images are dark, so they draw the dark logo when one is set, at its own proportions.
The logo is taken as a full wordmark, so the site's name is not repeated beside it. For a mark without the name, set logo: { light: "./brand/mark.svg", mark: true } and the name shows beside it in every theme and in social images.
Without a favicon, a site's icon is its logo where that is a mark, or else a square of its primary colour, as its social images draw beside the name. A wordmark is too wide to be an icon.
Navbar
navbar: {
links: [
{ type: "link", href: "https://example.com/integrations", label: "Integrations" },
{ type: "menu", label: "Community", links: [
{ type: "link", href: "https://forum.example.com", label: "Forums" },
{ type: "discord", href: "https://discord.gg/example" },
] },
{ type: "github", href: "https://github.com/example/docs" },
],
primary: { type: "button", label: "Sign in", href: "https://app.example.com" },
},navbar.links sit at the header's end: a network's icon (github, npm, twitter for X, discord, discourse, slack, linkedin, youtube, mastodon, bluesky or reddit, its label the icon's accessible name, the network's own name by default), a text link (link) with its label, or a menu, links under one label that open below it, one menu at a time. A menu holds links only, one level deep; the mobile drawer lists them under its label. navbar.primary is the call to action, a button after the links and at the foot of the drawer.
A github link to a repository can show its latest release, stars and forks beside its mark: { type: "github", href: "https://github.com/example/docs", stats: true }. The build reads them from GitHub's API, two requests per build, so they are as fresh as the site's last build and no reader's browser calls GitHub. Give the build GITHUB_TOKEN (in GitHub Actions, env: { GITHUB_TOKEN: ${{ github.token }} }) for GitHub's higher limit. The build reads them while the pages load; when GitHub does not answer within three seconds, the build warns and the link shows without them.
Breadcrumbs
navigation: {
breadcrumbs: true,
tabs: [/* ... */],
},Each documentation and reference page then opens with the way to it above its title: the site's home, its tab where the site has several, and each section the page sits in, every step a link where the section has a page of its own. The page itself is not a step, since its title follows. The trail is drawn when the site builds, wraps on a phone, and is the same trail search engines read from the page's structured data. Off by default; without it, each theme names the page's section as it always has.
Footer
footer: {
text: "© Example Ltd. Licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).",
links: [{ type: "github", href: "https://github.com/example/docs" }],
},footer.text is one line of inline Markdown at the foot of every page, such as a copyright or licence notice; the reader theme shows it in place of the site name unless reader.footer.text is set. footer.links take the same form as navbar.links: a network's icon, or a text link with its label. The footer sets text links first and the networks' icons together after them. Keep it to places the header and sidebar do not already lead: the site's own pages are in the sidebar, and its repository is usually in the header. "Edit this page" is the page's own: it ends each page, above previous and next, and opens the page's file on GitHub, which anyone can read and whoever can edit opens in GitHub's editor from there. A page edited somewhere other than the site's repository names its own address in frontmatter, editUrl: https://github.com/example/agent/edit/main/docs/guide.md, and editUrl: false leaves the link off.
Last updated
lastUpdated: true,Each documentation page and changelog shows when it last changed, beside "Edit this page": the day of the last commit to its file, in the page's language. The sitemap names the same date as the page's lastmod. A file no commit holds yet has no date. Off by default.
The build reads the repository's history, so a CI checkout needs all of it. A shallow checkout stops the build, since it would date every page by its one commit:
- uses: actions/checkout@v7
with:
fetch-depth: 0Page feedback
feedback: true,Each documentation, reference and changelog page then ends by asking "Was this page helpful?" in the page's language, answered with a thumbs up or a thumbs down from the site's icon set. Choosing an answer shows its note and dispatches a sourcey:feedback event from the question, with the answer's value and the page's path. Sourcey sends the answer nowhere itself; the site's own script, in its Head component, sends it where the site keeps feedback. The question stays hidden until the page's script runs, so a reader without JavaScript sees no buttons that do nothing. Frontmatter feedback: false leaves it off a page. Off by default.
<!-- components/Head.html -->
<script>
document.addEventListener("sourcey:feedback", (event) => {
navigator.sendBeacon("https://example.com/feedback", JSON.stringify(event.detail));
});
</script>A site can ask its own question. Each rating has a label, the value it sends, an icon from the site's icon set (without one, the label is the button) and a note, in Markdown:
feedback: {
title: "Did this page help?",
ratings: [
{ label: "It helped", value: 1, icon: "face-smile", note: "Thanks!" },
{ label: "It could be better", value: 0, icon: "face-frown", note: "Tell us [on GitHub](https://github.com/example/docs/discussions)." },
],
},Typography
markdown: { smartypants: true },markdown.smartypants sets prose with typographer's punctuation: straight quotes become curly ("quoted" reads “quoted”, it's reads it’s), -- becomes an em dash and ... an ellipsis. It applies to guide pages and to the Markdown in references, such as descriptions. Code, preformatted text, page titles and navigation labels keep every character as written, as do the Markdown and agent exports, and heading anchors do not change. It is off by default; sourcey migrate astro turns it on, because Astro sets Markdown this way unless its markdown.smartypants is false.
Themes
theme chooses one of four themes and sets its colours, fonts, layout widths, light and dark appearance, stylesheets and icons. See Themes.
Interactive requests
OpenAPI references include an inline request console by default. Set tryIt: false to disable it site-wide or on one tab. Use tryIt: { credentials: "include" } on a tab that needs browser-managed sessions across origins. The default is same-origin; omit disables session credentials.
See Try it for authentication, local drafts, browser requirements, and global CSS overrides.
Code samples
codeSamples chooses the languages of each operation's request examples, in the order its picker shows them. See Code samples.
Search
search.featured names the pages search offers before anything is typed. See Search.
Publishing checks
sourcey build and sourcey check report what publishing needs: a siteUrl, without which pages name no canonical URL or language alternates and the site has no sitemap, as each needs absolute URLs; pages with no summary of their own, or a one-word one (their metadata falls back to the title); canonical or language links to no published page; and links to a fragment the target page does not have. Each kind is reported once, with the pages it affects. With a siteUrl, every page carries its place in the navigation as schema.org BreadcrumbList data, and its description is plain text drawn from its own summary.
Documentation releases and languages
Versions declares current and historical content roots, independent API releases and their URLs. Languages covers existing translations, fallback pages, direction and UI messages. These settings use the same native sources and navigation as a single-language site.
Public files
Set publicDir: "./public" to copy an explicitly public directory to the output root with its paths intact. A file at public/files/manual.pdf is served at /files/manual.pdf, or beneath the configured baseUrl. Public files participate in development updates and output collision checks. A public file cannot replace a generated page or asset.
For Markdown-relative images and downloads, Sourcey publishes the referenced bytes automatically. Use publicDir for paths that must stay fixed. Keep source code, credentials and private inputs outside that directory. In an Astro project, Astro continues to own its own public/; do not configure both tools to publish the same files.
Site pages
Set pages: "./pages" for the site's own pages, outside the documentation: a home page, a product or pricing page, a page of your own design. Each publishes at the address its path names, pages/index.md at the site root and pages/pricing.md at /pricing/. A site page belongs to no tab and no version, so no section is current on it and no sidebar lists it. Names starting with _ or . are partials, not pages.
Site pages are written in the same Markdown, with the same components, as documentation. They take frontmatter title and description, are searched and exported like any page, and link operations by name. In a translated site each language keeps its own under its content folder (pages/fr/ beside pages/), and with fallback on the default language's page stands in for one a language lacks.
Page layouts
A page's frontmatter layout says how it is drawn:
layout | Drawn with |
|---|---|
doc | The documentation frame: header, sidebar, contents and footer. The default for documentation. |
page | The site's header and footer around the page's own body, with no sidebar or contents: its text at a reading measure, its HTML the page's full width. The default for site pages. |
blank | Nothing but the page's body. Sourcey keeps the page's <head>: its title, description, social card, theme colours and fonts. |
changelog | A changelog, as for CHANGELOG.md. |
The site's own head markup, banner, header, footer and the end of each page's content come from its components folder. A page or blank page draws its own heading and opening; Sourcey adds no title above them. Prose keeps the theme's typography; wrap a block in <div class="not-prose"> to style it yourself. A page can bring its own stylesheets with frontmatter css: ./home.css (or a list), resolved from the page's folder: Sourcey publishes each once, with its relative fonts and images, and links it from that page alone.
---
title: Pets for everyone
description: Adopt, foster and care for a pet.
layout: blank
css: ./home.css
---
<section class="hero">
<h1>Pets for everyone</h1>
</section>Broken links
Each build reports a link to a path on the site that no page, file or redirect answers, beside links to a heading a page does not have. Set brokenLinks: "error" to have sourcey build stop instead, naming each link and writing nothing, as Docusaurus does by default; the dev server reports them either way, so a page being written can link ahead. A site published inside a host, such as an Astro site with pages of its own, reports none, since the host may answer them.
Root and missing pages
A page that publishes at the site root, pages/index.md or a documentation page at /, is the home page, and the logo leads there. When nothing publishes there, a standalone build redirects the root to the first section's first page.
Sourcey generates 404.html for standalone output; pages/404.md replaces it with a page of your own. A not-found page names its assets and links from the site root, because the host serves it at any address, and marks its root <html data-sourcey-page="not-found">, so analytics in the site's Head component can count missing pages. Configure your static host to use that file with an HTTP 404 status. An HTML file alone cannot set a response status. Use notFound: false when the host already owns its missing-page route.
Redirects
Use absolute public paths, including your baseUrl:
redirects: {
"/docs/old-guide": "/docs/installation/",
"/docs/preview": { to: "/docs/installation/", status: 302 },
}A string declares permanent 301 intent. Explicit statuses can be 301, 302, 303, 307 or 308. Local destinations must exist; fragment targets, cycles and collisions with content are checked during compilation. Wildcard and query-matching rules are not supported.
Standalone builds emit HTML fallback pages and a _redirects file for hosts that support that format. An authored public _redirects file cannot also own those rules. Static HTML does not issue an HTTP redirect status. Its replacement script keeps an incoming query and fragment unless the destination supplies its own; without JavaScript, the meta refresh and link use the exact configured destination. Dev issues the declared status and retains the incoming query when the destination has none. Astro registers the same records with its host router; the adapter and hosting platform determine production HTTP handling.
sourcey check compiles the same inputs and checks routes, imported reference fragments and output collisions without writing a site. Normal builds perform those checks too.
Reference pages
An OpenAPI or MCP reference publishes in one of three shapes:
reference.split | Publishes |
|---|---|
false | One page holding everything |
"sections" | A page for each section, an OpenAPI tag or MCP group, holding its operations in full |
"operations" | A page for each operation |
Unset, Sourcey chooses by size and says which it chose when it builds. A page holds at most 50 operations and 1,000 field rows, counting each parameter, body field and response a page draws. A reference within that is one page. A larger one with several sections, each within it, gets a page per section: an MCP server divided into groups usually does, as do APIs with modest, light tags. Otherwise each operation gets its own page, as a large API, or one whose operations carry large schemas, needs. Field rows matter as much as operations: a hundred light tools read well a section at a time, while thirty operations returning large schemas make a page too heavy to open.
A split reference also publishes a page for each model, beside an overview: split by section, it lists the sections, each with its description or the operations it holds; split by operation, it lists every operation under its section. Every shape uses the same reference components, examples and schema links, and OpenAPI operations keep their request console. On a split reference's overview, each operation and model keeps the anchor its section has on the single page, so a link from before the split lands on its entry, and an operation's own address leads wherever its shape puts it, so a reference can grow from one shape to another without breaking a link. Each theme frames those pages as it frames a guide: the section's name over an operation's title, previous and next in navigation order, and an overview at reading width whose "On this page" lists its introduction's headings, as the sidebar lists the sections. A reference split by section with only one section to split by stays one page, and the build says so; one held on one page with more than 100 operations builds with a warning, as it is slow to open and to search.
An MCP server's sections are its kinds, Tools, Resources and Prompts, unless its groups divide it as its authors do; see Sections.
Schema alternatives use a native picker, labelled from their titles, reference names or discriminator values. Without JavaScript every alternative remains available. A field link opens the matching alternative and its parent disclosures; readable Markdown retains all alternatives. Typed map values use the same field rows as named properties, including descriptions, constraints and nested values.
A named type never opens inside itself: a field of the type it sits in reads "Same fields as" and links up to where that type is drawn. A schema that would draw more than 500 field rows, as large Kubernetes types do, draws each named type once, at its shallowest place, and its other fields link there. Generated examples follow the same rule: a property that would repeat its own type is left out (empty where it is required), and an example longer than 400 lines shows each named object type once.
Set reference.operations to give every operation a permanent address. {operation} uses the authored operation ID, or the path when no ID exists. {operationId} requires an ID; {method} and {path} are also available. Values use the native heading slug rules. A reference split by operation publishes each operation's page there; one page, or a page per section, redirects the address to the operation where it is, so the reference can change shape without breaking a link. Explicit tab routes for operations and models work the same way and win over the pattern. Duplicate physical paths fail the build.
Link an operation from any page by name, as navigation names it: [List keys](<operation:GET /keys>), or operation:TOOL search for an MCP tool. The link lands wherever the operation is published, on its page or its section, and follows a split. When two references publish the same operation, name the reference's tab ID: operation:rest:GET /keys. A link to an operation no reference publishes fails the build. Cards take the same href.
A converter can retain previous fragment spellings with anchors: { "POST /items": { "old-body": "request-body" } }. Each alias must target a real section or field on that document. It is placed beside that content, so an old link reaches the field, including inside a disclosure. Field identities follow the owning operation or schema, property path, media type and response status; they stay distinct across repeated names and remain stable between reference layouts. Converters write the original spellings as data, and Sourcey checks their destinations during compilation.
Missing alias destinations fail the build by default. Set anchorUnmatched: "warn" on the tab for imported link history that follows a changing spec: Sourcey reports removed documents or fields and omits only their stale aliases. "ignore" omits them silently. Invalid alias spellings and collisions remain errors under every policy. The public build result exposes warnings as anchorDiagnostics.
Large generated navigation, route and anchor tables can live in JSON files imported by the config. Use a default import (import anchors from "./api.anchors.json"); Sourcey loads the file as data and tracks it for live reload. This avoids compiling a large table into thousands of JavaScript exports.
Choose the reference order
Generated navigation has a group per tag, in the spec's order, labelled by the tag's summary or name. A tag with a parent (OpenAPI 3.2) sits inside its parent's group, after the parent's own operations; an overlay can add that hierarchy to an older spec. A tag with nothing to list beneath it is left out.
Give a tab its own navigation tree when the generated groups do not match how people use the API:
{
id: "rest",
tab: "API Reference",
source: openapi("./openapi.json"),
reference: { split: "operations" },
routes: { "GET /keys": "keys/list" },
navigation: [
{ group: "Start here", pages: [{ page: "introduction" }] },
{
group: "Keys",
pages: [{ operation: "POST /keys" }, { operation: "GET /keys" }],
},
],
},Operation entries use METHOD /path, exactly as the spec defines it. Their labels come from the effective spec's summary; label overrides the navigation text. Sourcey resolves each entry to its operation page, or its section on a single page. Missing and unpublished targets fail the build.
Markdown groups can also link to a reference owned by another tab with { operation: "GET /keys", reference: "rest" }. Use a stable tab id for reference. A page in another source uses { page: "installation", tab: "guides" }; the page can be named by its source reference or stable document ID. Cross-source links select the matching language and content version. Readers follow the canonical document in its owning tab.
The source controls which operations are published. Navigation controls their order and grouping. An operation can appear in several groups while keeping one URL. The sidebar lists every section, with the one around the current page open; a section of more than 80 entries without a page of its own gets a generated page listing them, which its link leads to where it is folded. Navigation data can live in a separate TypeScript file, typed with GroupConfig[], and be imported by the config.
