---
title: "Configuration"
description: "Every setting lives in src/docs.config.ts, in five exports for identity, search, theme, navigation, and agent surfaces."
canonical: "https://duvlify.dev/reference/configuration"
updated: "2026-08-10"
---

# Configuration

Everything you change to make this site yours lives in
[`src/docs.config.ts`](https://github.com/DuvInc/duvlify/blob/main/src/docs.config.ts). You do not need to edit
`src/components/`, `src/styles/`, or `worker/` to rebrand the site, change its
navigation, or turn features on and off. If you find yourself editing those
files to change a string, treat that as a bug in this configuration file, not
something to work around.

The configuration file has five exports.

## `site` — identity

This export sets the name, title suffix, description, header and footer
links, and the assistant menu. The description is the site-level meta
description and the blockquote in `llms.txt`. Write one real sentence for it,
not a slogan.

`basePath` decides where the documentation sits under its domain. Use `''`
for `docs.example.com/page`, or `'/docs'` for `example.com/docs/page`. This
one setting moves every generated URL. The Worker strips the prefix back off
before it looks anything up. `basePath` must stay in step with Astro's `base`
in `astro.config.ts`, so both settings read from the same constant. See
[`DEPLOYMENT.md`](/guides/deployment) for the topologies, the Cloudflare
route, and the three things a subpath cannot have.

One rule comes with `basePath`. An internal link written by hand _in this
file_ must carry the prefix itself (`` `${basePath}/getting-started` ``).
Links built from a page id go through `hrefFor`, which applies the prefix for
you. An authored string without the prefix is indistinguishable from a link
to another site.

`homePageId` is the one subtle field. Leave it `undefined` for a docs-first
site whose home page is its first page. Set it to a content id to serve that
page at `/`, outside the navigation. That page stays published, indexed, and
in the corpus, but it is not a sidebar item and takes no part in
previous/next navigation. A page named here must _not_ also appear in
`navigation`. The build refuses that combination, because the page would be
reachable at two URLs.

`banner` is a site-wide announcement fixed above the navbar. It takes
`content`, a `tone` (`info`, `warning`, or `critical`), `dismissible`, and an
optional `color` per colour mode. Leave it `undefined` for no strip. The site
stores a dismissal against the exact text, so publishing new wording shows
the banner again to everyone who dismissed the previous version. This same
behaviour makes the strip unsuitable for anything a reader must not be able
to hide.

Turning the banner on changes the shell's geometry, and nothing in this file
has to account for that. The height lives in `--banner-h`
([`src/styles/tokens.css`](https://github.com/DuvInc/duvlify/blob/main/src/styles/tokens.css)). Every fixed bar, the
document's top padding on both desktop and mobile, and `scroll-padding-top`
all derive from that one value.

## `seo` — search and generative engines

This export sets the publisher identity for the JSON-LD graph, the locale,
the AI crawler allowlist, and Cloudflare Content Signals.

Write `agentInstructions` with care. The build emits it as prose at the top
of `llms.txt` and `llms-full.txt`, in the space the llmstxt.org format leaves
between the summary and the links. Limit it to facts a model cannot infer
from the pages: which document wins when two disagree, what the site does
not cover, and what it must not guess at. Restating the product description
adds no value, because `site.description` already sits directly above it.

The build deliberately does _not_ prepend `agentInstructions` to every
page's Markdown. That would put the same paragraph in front of a reader who
clicked "Copy page" for one answer. It would also make anything crawling the
corpus re-read the paragraph once per page.

## `theme` — brand

This export sets the font, accent, and radius. The build injects these as CSS
custom properties, so changing the accent is one line. The neutral palette
and component styling live in `src/styles/`.

`accent.strong` exists because `accent.base` usually cannot clear a 4.5:1
contrast ratio against the accent _tints_ used behind badges and method
pills. It can only clear that ratio against the page background. Check both
contrasts when you change the accent.

### Changing the page background

`--bg` in [`src/styles/tokens.css`](https://github.com/DuvInc/duvlify/blob/main/src/styles/tokens.css) is the one colour a
site most often wants to move, for example to a light grey canvas that
matches a marketing site. Change only this value. The raised surfaces follow
it automatically.

`--surface-2` (hover) and `--surface-active` (the current item) are not fixed
colours. Each is `--bg` mixed a measured distance toward `--text` in OKLab.
This holds the same perceptual separation from whatever background you set,
in both colour modes. The percentages differ by mode because perceived
contrast differs by mode: dark mode needs roughly twice the lightness step to
read the same as light mode.

These values used to be absolute colours, which worked only while `--bg`
stayed white. One site set `--bg: #f5f5f5` and got a `--surface-2` of
`#f5f5f5` too. The selected sidebar item then rendered in exactly the page
colour and disappeared. Nothing was misconfigured. The tokens simply did not
know the background had moved. If you find yourself hand-picking a second
colour after changing `--bg`, that is the same bug returning.

## `navigation` — structure

This export lists tabs, groups, and page ids in sidebar order. Labels,
descriptions, icons, and badges come from each page's own frontmatter. This
export never restates them.

`navigationLayout.mode` switches between `category-tabs` (large categories in
a bar under the navbar) and `unified-sidebar` (everything in one left rail).
Changing this mode never changes routes, content, or navigation data.

The build validates this export against the content collection. A page id
with no matching file fails the build. A published page that belongs to no
group also fails the build. A page id that names a draft is skipped silently,
and it takes any group or tab it would have emptied with it. This last case
is what lets you leave work-in-progress listed here while it stays unpublished.
See [Drafts](/guides/authoring#drafts).

## `agents` — the MCP server, HTTP API and WebMCP

```ts
export const agents = {
  enabled: true,            // master switch for all three surfaces
  http: true,               // also serve /api/docs/*
  retrieval: 'lexical',     // or 'ai-search'
  aiSearchInstance: '…',
  defaultLimit: 8,
  maxLimit: 25,
  feedback: { webhook: undefined, fields: [ … ], context: [ … ] },
  webmcpBridge: 'off',      // 'on' where there is no Cloudflare zone
  rateLimit: { search: 30, read: 90, write: 2 },
};
```

Setting `enabled: false` registers no routes, provisions nothing, and skips
the index upload. A site that wants plain documentation carries none of that
cost. Full detail in [`AGENTS.md`](/agents/overview).

## Also configurable

- [`src/openapi.config.ts`](https://github.com/DuvInc/duvlify/blob/main/src/openapi.config.ts): the OpenAPI document the
  endpoint pages render from. Leave the empty stub, and the build emits no
  `/openapi.json` and advertises nothing. A site with no API should not
  publish a spec with no paths, because an agent may read that as evidence
  that the API has no endpoints.
- [`src/icon-aliases.ts`](https://github.com/DuvInc/duvlify/blob/main/src/icon-aliases.ts): short names for icons.
- [`public/_headers`](https://github.com/DuvInc/duvlify/blob/main/public/_headers): caching and security headers.
- [`wrangler.jsonc`](https://github.com/DuvInc/duvlify/blob/main/wrangler.jsonc): the Worker, its asset routing, and the
  rate limiting bindings.

## The values every site must set

A fresh clone runs as is. `npm run dev`, `npm run build`, and `npm test` all
pass with nothing configured, because every service integration defaults to
off and search falls back to the built lexical index. This is intentional.
The alternative is a template that fails on first contact until you set up an
account somewhere.

The cost of that choice is that a fresh clone carries **this** site's values.
Those values are wrong for your site, often in ways that are easy to miss.
The list below orders them by how soon each one will cause a problem.

| Where                                            | What a clone starts with   | What happens if you leave it                                                                                                                        |
| ------------------------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `astro.config.ts` → `site`                       | `https://docs.example.com` | Canonical tags, the sitemap, `llms.txt` and the agent manifest all name a host that is not yours. The site looks fine and the SEO points elsewhere. |
| `wrangler.jsonc` → `name`                        | `duvlify-docs`             | Your first deploy creates a Worker under that name, or collides with one.                                                                           |
| `src/docs.config.ts` → `site`, `seo`, `theme`    | Duvlify's own identity     | Cosmetic, and obvious on first render.                                                                                                              |
| `src/docs.config.ts` → `agents.aiSearchInstance` | `duvlify-docs`             | The build reads this only when `retrieval` is `'ai-search'`. It stays inert until then.                                                             |
| `src/openapi.config.ts`                          | an empty stub              | The build emits nothing and advertises no spec, which is the right behaviour for a site with no API of its own.                                     |
| `content/`                                       | Duvlify's documentation    | You publish someone else's docs. Delete the folder and write your own.                                                                              |

Set the origin before anything else. Export `SITE_URL` in the deploy
environment rather than editing the fallback. This way, a preview build and a
production build cannot disagree.

## Secrets

There is one file, [`.env.example`](https://github.com/DuvInc/duvlify/blob/main/.env.example). Copy it to `.env` and fill
in what you need. `.env` is gitignored.

Only two values are real secrets. Both are used only by
`scripts/index-sync.mjs`, and neither reaches the Worker. The Worker queries
the index through its `AI_SEARCH` binding instead:

- `CLOUDFLARE_ACCOUNT_ID`
- `AI_SEARCH_TOKEN`: **not** the token `wrangler` holds. Create a separate
  token under AI → AI Search → Tokens with `AI Search:Edit` and
  `AI Search:Run`.

`agents.feedback.webhook` is notably _not_ a secret. The build reads it at
build time and compiles it into the Worker bundle, so a URL that carries a
token would be served to anyone who asks for it. Use a receiver that
authenticates some other way.

Switching semantic retrieval on takes three edits, and all three must agree.
This is the usual way it goes wrong. Uncomment the `ai_search` binding in
`wrangler.jsonc`, set `agents.retrieval` to `'ai-search'`, and set
`agents.aiSearchInstance` to the same instance name used in the binding. Then
run `npm run index:sync`.

The instance name appears twice on purpose, and the two copies must agree.
The Worker queries through the binding, while `scripts/index-sync.mjs` reads
the name out of the config to build its REST URL. A mismatch causes no
visible error. The Worker keeps answering, either from the wrong index or
from none.

Once semantic retrieval is on, `npm run deploy` chains `index:sync` between
the build and `wrangler deploy`. This makes a deploy from a shell without the
two credentials fail before it ships, instead of shipping a Worker that
queries an index one version behind. [`AGENTS.md`](/agents/overview) covers
creating the instance, the settings that are not obvious from the dashboard,
and how the index is reconciled.
