Configuration Reference
Site config is passed in via absolutePress(defineSiteConfig({...})). Type definitions live in src/shared/types.ts (SiteConfig) and src/node/config.ts (AbsolutePressConfig, the build-layer extension). This page goes field by field; the implementation is the source of truth.
Basic fields
contentDir(required, string): content root of the default locale, relative to the project roottitle(required, string): site name; goes into<title>, RSS, and the sitemapdescription(required, string): site description; goes into SEO meta and the RSS channelhostname(required, string): canonical origin (no trailing slash), used for absolute links in SEO og tags, the sitemap, and RSS, e.g.https://absolute-docs.pages.devfavicon(optional, string): site favicon, resolved against the site public root (same semantics asnav.logo), e.g./favicon.svg; at build time emits<link rel="icon">with thetypederived from the extension (ico/png/svg/webp, other extensions ship withouttype), and the href gets the page-depth relative prefix so subpath deploys never break it
locales
locales: {
en: { lang: 'en', label: 'English' },
}- The default locale always occupies the contentDir root.
<html lang>defaults tozh-CNand the switcher label defaults to简体中文; override with the top-levellang/label - Other locales put their content in
<contentDir>/<key>/, and their routes get an/en-style prefix - See Internationalization
iconProvider
iconProvider: 'fontawesome',Built-in icon source, layered under the custom icons map (custom keys win on collision). 'fontawesome' registers every Font Awesome free glyph under its <pack>/<name> key (solid / regular / brands, e.g. solid/rocket, brands/github), so frontmatter icon references need no per-icon setup. Without it, no icons are registered.
icons
icons: {
'my-logo': '<svg xmlns="..." viewBox="0 0 16 16" fill="currentColor">...</svg>',
}Custom icon entries: the key is a legal value for frontmatter icon (validated at build time; unregistered keys fail the build). The value is a full <svg>...</svg> string, or bare SVG inner markup (the client wraps it in a 24x24 svg). The navbar, sidebar, and article cards consume these icons. On a key collision with iconProvider, the entry here wins.
islands
islands: {
Counter: 'docs/islands/Counter.tsx',
}Custom islands added by the site: PascalCase tag name → module path relative to the project root. This is the extension point beyond the built-in list (Giscus/PasswordGate/ZoomedImg/Mermaid/G2Plot/ExpandableList). See Islands.
buildComponents
buildComponents: {
disable: ['RecentArticles'],
}Master switch over the built-in build components — markdown tags rendered to final static HTML at build time (inventory and usage under Markdown extensions). disable turns built-ins off by tag name: a disabled tag renders nothing and warns at build time; an unknown name fails config resolution listing every available component.
refs
refs: ['reference', '../glossary'],The term-library roots (optional; a single root may be written as a plain string): data source of the [[id]] term-reference syntax, see Markdown extensions.
- Each entry resolves against every locale's content root (the default locale reads
<contentDir>/<entry>, others<contentDir>/<key>/<entry>, falling back to the default locale's terms on a miss); entries may contain..to point outside the content tree (e.g. a separate repo next to the project root) .mdfiles inside never become routes; an id is the extension-less posix path relative to its root, and frontmatter recognizestitleonly (the id's last segment is the fallback term text)- Roots are collected in config order; ids must stay unique across them — duplicates fail the build
- The library often lives in its own repository (a nested git repo): missing roots warn once, every
[[...]]degrades to plain text, and the build still passes - Relative links inside term articles resolve against the locale content root (same semantics as pages), so terms embedded at any page depth never break
encrypt
encrypt: [
{
match: '/guide/secret', // string equality on the route, or RegExp.test
passwords: ['docs-demo'],
hint: 'optional hint text',
},
];An array of client-side password-gate rules: pages whose route matches match (exact string or regex; avoid the /g flag) get their content wrapped in the PasswordGate island, and unlock after entering a password whose sha256 matches. The unlocked state is remembered per route in sessionStorage. Write non-ASCII routes (e.g. Chinese) as plain decoded paths (matching runs against the decoded route, and regexes see the decoded form too), not percent-encoded. Plaintext passwords never enter the page payload (only sha256 hex is emitted). See Encryption.
algolia
algolia: {
appId: 'XXX',
apiKey: 'xxx',
indexName: 'xxx',
}When configured, the theme chrome mounts an Algolia DocSearch box, and the credentials go into the page payload.
giscus
giscus: {
repo: 'owner/repo',
repoId: 'R_xxx',
category: 'General',
categoryId: 'DIC_xxx',
}When configured, a Giscus comment island is mounted automatically at the end of article pages (no manual tag in markdown). The comment iframe theme follows site light/dark switching without reloading the iframe.
googleAnalytics
googleAnalytics: 'G-XXXXXXX',A measurement ID string; the GA script is injected at build time. The framework sends a page_view after every SPA soft navigation (the initial load is counted by GA itself); disable "Page views based on browser history events" in the GA4 data stream's enhanced measurement, or soft navigations are double-counted.
nav
nav: {
exclude: ['/hide'], // route prefixes excluded from navbar/sidebar (pages are still built)
logo: '/logo.jpg', // brand image for the navbar (and mobile drawer)
align: 'center', // top-level navbar alignment: 'left' (default) | 'center'
order: ['coding', 'hobbies'], // order top-level entries by content directory name
tweaks: { /* per-top-level-directory navbar tweaks, see below */ },
social: [
{ icon: 'github', url: 'https://github.com/owner/repo', title: 'GitHub' },
],
}The single entry point for navbar-related config: the nav tree is generated from the content directories, and nav.* only reshapes the generated result.
exclude: string[]: route prefixes excluded from navbar/sidebar (pages are still built). The frontmatter contract has no hide key; "exists but not in nav" is a site-config concernlogo: string: brand image for the navbar (and mobile drawer), resolved against the site public root ('/logo.jpg'->public/logo.jpg); the client prefixes it with the depth-based relative prefixorder: string[]: orders top-level navbar entries by content directory name (same key space astweaks); directories not listed and root-level loose pages keep their generated order; unknown names are ignoredalign: 'left' | 'center': alignment of the top-level navbar strip; with'center'the remaining space between the brand area and the right icon area is split evenly (default'left')tweaks: Record<string, NavbarDirTweak>: tweaks navbar entries per top-level content directory.labelrewrites the nav text (the sidebar group title follows it).groupslays out the dropdown panel by groups (a group withtextrenders a static subheading; a group withouttextonly fixes member order; unlisted members are appended in generated order).itemsspecifies the panel entries wholesale (e.g. a section tree derived from a site data module); every internal link is validated at build time to point to a page under that directory; generated members not covered by any item link are still appended; takes precedence overgroups. Entries without an expliciticoninherit the linked page's frontmatter icon. Folder rows (at any depth) navigate to the directory index page (the section name is the overview): the overview row carrying theindexmarker renders as the first row of the panel (the client sets anap-nav-index-rowhook; badge/divider styling belongs to site CSS) — the overview row of a top-level panel is generated at build time, the overview row of a nested folder is synthesized by the client as its panel's first row, and the folder row itself only expands. An index page with frontmatteroverview: falseopts out of the overview row (the folder row still points to that page; the sidebar is unaffected)social: { icon, url, title }[]: social icon buttons on the right of the navbar, rendered before the RSS button, always opened in a new tab.icontakes a registered icon key (iconProvider/icons), or a built-in brand key (github/telegram/bilibili); on name collision the registry wins.titleis used as aria-label and hover tooltip
The navbar also always renders an RSS button (linking /rss.xml, base-aware, title from i18n nav.rss); RSS is always generated, no config switch.
sidebar
sidebar: {
order: ['blog', 'guide'], // top-level groups ordered by content directory name
tweaks: {
guide: ['getting-started', 'writing', 'markdown'], // member order inside one directory
'guide/advanced': ['deep'], // nested directories are keyed by their content-root-relative path
},
}The sidebar is generated complete from the content directory tree; sidebar.* only reorders the generated result — entries can be rearranged, never dropped (hiding pages is nav.exclude).
order: string[]: top-level groups ordered by content directory name, same semantics asnav.order; loose pages join directories as orderable entries (a page takes its file stem). Listed entries come first (config order), unknown names are skipped, the rest keep their generated ordertweaks: Record<string, string[]>: member order inside one directory. The key is the directory's path from the content root — a top-level directory is its bare name, nested directories join segments with/(e.g.guide/advanced); the value is an ordered list of member names, extension-less and relative to that directory (a page takes its file stem, a subdirectory its name; the directory index page is not a member — the folder row already links there). Listed members come first (config order), unknown names are skipped, unlisted members keep their generated order — an order list reorders, it can never make an entry disappear
seo
seo: {
image: '/og.png', // share-card image, resolved against the site public root or an absolute URL
author: { name: 'Alice', url: 'https://example.com/about' }, // JSON-LD author, url optional
exclude: ['/hide'], // route prefixes kept out of sitemap.xml and disallowed in robots.txt
}SEO head options: image is the share-card image. At build time it produces og:image (a relative value like '/og.png' resolves against the site public root and is joined with hostname into an absolute URL, same semantics as nav.logo; an absolute https:// URL passes through unchanged) and upgrades twitter:card to summary_large_image. Without it, only twitter:card: summary is emitted (card without image), and no og:image. author is the author of the article-page BlogPosting JSON-LD (schema.org Person); when unset or blank-named, no author field is emitted. exclude keeps route prefixes uncrawled: out of sitemap.xml plus one Disallow each in robots.txt; the pages are still built. Gated pages need no entry here — encrypt already excludes and disallows them. Each page's meta description comes from the content excerpt (see SEO); no per-page config needed.
head
head: ['<meta name="algolia-site-verification" content="ADD39C0998F22200">'],An array of raw HTML fragments injected verbatim into every page <head>, in config order, after the framework's own tags (before the speculation rules script); blank entries are dropped. Built for site-ownership verification metas (Algolia DocSearch, Google Search Console, Bing...), third-party analytics snippets, and extra <link>/<script> tags. Resource URLs inside a fragment must be absolute (https://-prefixed): pages carry depth-dependent relative base prefixes, so bare paths resolve wrongly on deeper pages.
related
related: {
depth: 1, // 1 | 2 | 3, default 1
maxNodes: 60, // per-page graph node cap (including the current page), default 60
maxEdges: 240, // per-page member edge cap, default 240
twoHopNodeLimit: 48, // hub-page fallback threshold, default 48
}Aggregation scope of the related-articles graph: depth is the BFS depth over the cross-link graph — 1 collects only first-degree mutual neighbors (default); 2/3 also collect articles within two/three hops together with the real edges among them. Values outside 1/2/3 fail the build.
- "Cross-linked" means there is an internal link in either direction between two articles; repeated links between the same pair accumulate into
refs, which decides edge thickness and neighbor ordering - The payload has protective caps (per page, 60 nodes and 240 member edges by default, adjustable via
maxNodes/maxEdges, which must be integers no less than 2/1); overflow is truncated by reference strength, so raisingdepthwill not blow up the payload. On large, densely linked sites, raise both caps together twoHopNodeLimitis the hub fallback threshold for multi-hop graphs: when a page's related node count (including itself) exceeds it, only first-degree neighbors render, anddepthhops are not expanded; if the first-degree star itself exceeds the cap, only the strongest mutual references are kept — a dense hub page would otherwise cram labels together, and the fallback keeps a readable star (it is effectively the render node cap of the graph)- The client graph supports zoom/pan/node drag/click-to-navigate; it auto-fits on init (with a zoom cap so the default view keeps text compact and readable) and hides labels when zoomed out. See Islands for interactions
code
code: {
lineNumbers: true, // code block line numbers, default true
collapsedLines: 15, // collapse blocks longer than N lines; null explicitly disables collapsing, default 15
wrap: true, // soft wrap, default true
copyLabel: 'Copy code', // aria-label/title of the copy button, for UI localization
}Site-level defaults for code block rendering (consumed by the markdown pipeline): collapsedLines uses ??= semantics to distinguish "not configured" from "explicit null" — only writing null turns off collapsing. Each code block can override the site defaults with fence meta: {1,3-5} line highlights, title="...", :collapsed-lines[=N], :no-collapsed-lines, :wrap=true|false. Syntax and demos in Markdown extensions.
xlist
xlist: {
externalize: 16, // KiB threshold; false keeps every list inline, default 16
}Body-externalization threshold for ExpandableList (and any built-in entry-list island): when one list's rendered entry bodies exceed the value (KiB), the bodies leave the page HTML for a content-hashed asset module (assets/xlist-<hash>.js) while the static page keeps title rows, meta rows and data-ap-slot markers; the client island fetches the asset after mount and fills the slots. Body-heavy list pages stop inflating their first document. The trade-off: no-JS visitors see only titles and meta (anchors survive), and bodies stay out of the search index. Set false to keep everything inline.
home
home: {
feed: true, // default true
feedPerPage: 3, // articles per page in the home feed, default 3
}Home page options: feed controls whether the paginated article feed renders at the top of each locale home page. Blog home pages keep the default; docs/landing-style home pages can set false so the content intro (hero/features/reading path) becomes the visual focus. feedPerPage is the page size of the article feed (client-side pagination, must be an integer no less than 1); it is only written into the page payload when non-default.
profile
profile: {
name: 'Alice', // card display name, defaults to the site title
avatar: '/me.jpg', // avatar image, defaults to nav.logo (no avatar when both unset)
link: '/about', // link wrapping the avatar+name, optional
articlesLink: '/timeline/', // link of the posts stat number, optional; defaults to the built-in timeline page
exclude: ['/hide/'], // route prefixes excluded from counts (total + categories/tags)
drawer: true, // drawer stats-row counts, default true once profile is set
}Author profile card, strictly opt-in: unconfigured sites see zero behavior change. Once configured, every locale home gets a static card injected above the body at build time (avatar / stats row / social icon row / category+tag chips — final HTML, zero client JS, visible to crawlers and no-JS clients), and that home no longer emits the TOC container (the card owns the outline lane; the client router re-syncs the container across soft navigations).
- Stats row: the three numbers link to the built-in listing pages by default (posts→
/timeline/, categories→/categories/, tags→/tags/); with a page disabled inpagesthe entry falls back — categories/tags anchor into the card's own groups (flashing once via:target), posts stay plain text unlessarticlesLinkis set (it always wins) - The social icon row reuses
nav.social(builtin brand glyphs included), opened in new tabs - The taxonomy derives from the locale's article list (locale homes already excluded), then filters the
excludeprefixes; grouping and order match the archive pages (case merging, lowercase sort); chips link to/category/<name>//tag/<name>with encodeURIComponent'd routes prefixed by the per-page relative base - Responsive: below 1024px the card hides and the mobile drawer takes over: below the drawer's social row sits a three-cell "Posts / Categories / Tags" stats strip (count over label, matching the card's stats shape) where each cell links its built-in listing page (see pages); with
profileconfigured the cells carry counts (same source as the card, always in sync with the archives), anddrawer: falsedrops the counts, keeping the labels as plain entry links. At 1024–1280px the card flows at the top of the body; at 1280px+ site CSS can skin it into the TOC lane
archive
archive: {
perPage: 10, // articles per page on category/tag archive pages, default 10
}Archive page options: the page size of each category/tag archive page (/category/*, /tag/*); client-side pagination, must be an integer no less than 1; only written into the page payload when non-default.
pages
pages: {
timeline: true, // timeline page (/timeline/), default true
categories: true, // category overview (/categories/), default true
tags: true, // tag overview (/tags/), default true
}Built-in listing page switches. All three are synthetic static pages, one per locale (final HTML, zero client JS — readable by crawlers and no-JS clients), at the fixed URLs <locale>/timeline, <locale>/categories, <locale>/tags (canonical shape follows urls.directoryIndex). Generated by the framework at build time; they never appear in the navbar/sidebar.
- Timeline: every article as a
date · titlerow, sorted by date desc and grouped by year. The dataset shares its source with the profile card's posts count (profile.excludeapplied, everything when no profile is configured, locale homes excluded), so the card's posts count always equals the timeline's row count; undated articles land in an "Undated" group at the end - Category / tag overview: a chip cloud of the locale's categories/tags (case-merged, count badges, derived like the archives); a chip links to its archive page
- Entry points: the drawer's three-cell stats strip below the social row (with counts once
profileis configured); the profile card's stat numbers link here by default; desktop reaches the pages via the card or the URL - Disabling: a
falsepage emits nothing (its URL 404s), loses the drawer stats cell and the card stat link, and drops out of the sitemap - A content page occupying the same route (e.g.
timeline/index.mdvs/timeline/) fails the build — remove the content page or disable the built-in one
Docs/landing sites that need none of these pages can set each key to false.
urls
urls: {
directoryIndex: 'slash', // 'slash' | 'bare', default 'slash'
}URL shape options. One key for now: the canonical route of a directory index page (guide/index.md).
'slash'(default):/guide/. The native GitHub Pages form — zero redirects'bare':/guide. The native Cloudflare Pages form (CF 308s/guide/to/guide)
Each platform redirects the other's form once (GH 301s /guide to /guide/, CF the reverse); no shape is redirect-free on both, so pick by hosting platform. Emitted file names are unaffected — always guide/index.html.
feed
feed: {
rssLimit: 20, // number of latest articles included in rss.xml, default 20
}Feed options: the cap on how many latest articles rss.xml collects across all locales (pages with feed: false are always excluded); must be an integer no less than 1. Full-text RSS would require rewriting internal resource links to absolute URLs, which the framework deliberately does not do (excerpt + link is the practical shape of a feed); the only knob is the item count.
footer
footer: {
credit: '© 2026 Someone',
}Footer: on desktop a single two-column row, with a fixed "Powered by absolute-press" link to the framework repository on the right. The left credit line (shared by the desktop footer and the mobile drawer bottom) defaults to a CC icon plus the framework name; when configured, it is replaced wholesale with custom plain text (the component does not parse HTML; use site CSS or a custom island if you need icons). A whitespace-only string counts as not configured.
strictLinks
strictLinks: 'warn', // 'off' | 'warn' | 'error', default 'warn'Policy for bare relative links: an internal markdown link in content that lacks the ./ ../ prefix (e.g. guide/a.md) is neither rewritten to a route nor covered by dead-link checking — it is emitted as-is pointing at .md, which is almost always a forgotten prefix.
'warn'(default): lists file, line number, and link at build time; the build still passes'error': fails the build like a dead link; suited for strict CI'off': no report at all
Only affects build-time reporting; the dev server does not check. Syntax details in Markdown extensions.
readingTime
readingTime: true, // default trueReading-time estimate, computed from the content at build time — Chinese by character count (300 chars/min), English by word count (200 words/min), mixed text merged linearly; frontmatter, code fences, and inline code are excluded. The result goes into the page payload as page.readingTime (integer minutes, rounded up, minimum 1), available for display spots such as archive cards; the article top meta row currently does not render it. Set to false to drop the field from the payload.
onScan
import type { SiteScanContext } from 'absolute-press';
onScan: (ctx: SiteScanContext) => ({
pages: ctx.pages.map(page => ({
route: page.route,
date: page.createdAt,
tags: page.frontmatter.tag ?? [],
})),
}),The site-data hook: the framework scans the content tree in a single pass and runs the hook once per scan, after the scan and before rendering — dev startup, builds, dev structural resyncs, and content edits all trigger it. The argument SiteScanContext is { config, pages }: config is the fully resolved site config (ResolvedConfig); pages is the page inventory of all locales, in config locale order. Both types are importable from absolute-press.
The return value is JSON-serialized into the virtual:absolute-press/site-data virtual module (its default export), consumed by site islands — see Islands. The return value must therefore be JSON-serializable (a bare date in rawFrontmatter is a Date object; call toISOString yourself when you need a string); a returned promise is awaited.
Motivation: locale directory ownership, README/index semantics, frontmatter normalization and unknown-key warnings, and git times are all owned by the framework scan. Consumers no longer walk directories, parse frontmatter, or query git themselves — derive site data from pages in onScan and the semantics match the framework by construction; in dev, content edits trigger a fresh scan so the data refreshes without a dev-server restart (the old approach of scanning inside vite.config.ts needed a restart to pick up changes).
SiteScanPage fields (import type { SiteScanPage } from 'absolute-press'):
filePath: string: absolute path of the markdown sourceroute: string: locale-prefixed clean route — leaf/guide/a, directory index/guide/(/guidewithurls.directoryIndex: 'bare'), locale home/en/, site home/relPath: string: path relative to the locale content root, posix separatorslocale: LocaleInfo: the page's locale (key/lang/label/prefix)frontmatter: PageFrontmatter: the normalization result over the framework-recognized keys (the six keys; semantics in per-page config)rawFrontmatter: Record<string, unknown>: the frontmatter as the yaml parser produced it; custom keys live here, bare dates areDateobjectscreatedAt: string | null: frontmatterdateas an ISO string; null when absent or unparsableupdatedAt: string | null: last git commit time as an ISO string; null when unavailable (e.g. not a git repo)
Build-layer extension fields (AbsolutePressConfig)
nav: see aboveprofile: see aboveonScan: see aboveentryListIslands: string[]: the list of site islands that reuse the@@@entry pipeline (names must already be registered inislands) — at build time these islands' children are split into a static table skeleton of "title + meta + body" by the same rules as ExpandableList, and the island client fills data viachildrenHtml. See Islandscode/xlist/readingTime: see aboveislands/lang/label: see above
Vite layer
Outside absolutePress() this is still a standard vite config: build.target should be esnext; the output directory is controlled by build.outDir (this site outputs to dist). UnoCSS and vite-plugin-solid need explicit wiring; template in Getting started.
Per-page config (frontmatter)
Frontmatter recognizes only six keys: date / category / tag / icon / feed / overview. Semantics in the Writing guide. Pages with feed: false leave RSS (the article feed still includes them).