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

locales

ts
locales: {
  en: { lang: 'en', label: 'English' },
}

iconProvider

ts
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

ts
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

ts
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

ts
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

ts
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.

encrypt

ts
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

ts
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

ts
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

ts
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.

ts
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.

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.

ts
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).

seo

ts
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.

ts
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.

ts
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.

code

ts
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

ts
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

ts
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

ts
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).

archive

ts
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

ts
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.

Docs/landing sites that need none of these pages can set each key to false.

urls

ts
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).

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

ts
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.

ts
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.

ts
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.

Only affects build-time reporting; the dev server does not check. Syntax details in Markdown extensions.

readingTime

ts
readingTime: true, // default true

Reading-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

ts
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'):

Build-layer extension fields (AbsolutePressConfig)

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).