---
title: "How the build works"
description: "Astro with static output and no framework runtime: the join between content and config, what publication means, and every file the build emits."
canonical: "https://duvlify.dev/reference/architecture"
updated: "2026-08-10"
---

# How the build works

Duvlify uses Astro with static output and no framework runtime. The site
ships HTML and one shared JavaScript bundle for the interface, and nothing
else. There is no hydration and no client-side router. Everything below
follows from this design.

## The layout

```
content/         the pages your site publishes.
src/
  docs.config.ts identity, theme, navigation, agent settings
  content.config.ts   the frontmatter schema
  components/    the MDX component set and the page shell
  lib/           build-time logic: navigation, Markdown, manifest, SEO
  pages/         routes — HTML, .md twins, feeds, indexes, share cards
  scripts/       the interface's client-side code
  styles/        tokens, reset, shell, components
worker/          the edge Worker: headers, negotiation, agent surfaces
scripts/         build-time tooling (index-sync)
test/            assertions against a real build and a real Worker
```

## The join that holds everything together

A page declares _what it is_ in its frontmatter: its title, description, and
icon. `docs.config.ts` declares _where it sits_. `src/lib/navigation.ts`
joins the two at build time. Every consumer then reads that one resolved
tree: the sidebar, the tabs, the breadcrumb, previous/next, search, the
sitemap, `llms.txt`, and the agent manifest.

Because there is one join, a mismatch is a build failure rather than a silent
problem. The build reports the file or id at fault. Because there is one
tree, no two surfaces can disagree about what the site contains.

## Publication

`src/lib/published.ts` owns one question: does this page ship? `draft: true`
means the file stays in the repository and out of every output. There is no
route, no Markdown twin, no share card, no sidebar entry, no search result,
no sitemap row, no line in `llms.txt`, and nothing reachable through the MCP.

Every consumer reads the collection through that module, instead of
repeating the filter itself. This keeps the meaning of "published" consistent
across all eleven places that check it. The one time a filter gets written
out locally is the time a draft leaks into exactly one output, and that
output is usually one nobody checks.

`noindex` is a different switch. That page _is_ published, routed, and
linked from the sidebar. The build keeps it out of the surfaces that promote
a page: search results, the sitemap, the updates feed, and the `llms.txt`
corpus. The reasoning is that a page deliberately withheld from search should
not be handed to a model either. The page remains fetchable at its URL and as
Markdown.

## One page, several representations

The build emits each page as:

- **HTML**: `src/pages/[...slug].astro`, the canonical copy for people and
  search engines.
- **Markdown**: `src/pages/[...slug].md.ts`, with MDX components translated
  away, and a YAML header carrying `canonical`.
- **A share card**: `src/pages/og/[...slug].png.ts`, drawn as SVG and
  rasterised at build time.

A single function, `pageMarkdown()`, generates the Markdown. This matters
more than it looks. The same bytes are what `<page>.md` serves, what the
`fetch` tool returns, what the indexer uploads, and what the agent manifest
counts line numbers against. Four consumers share one source. Without this,
a passage's reported line range could silently point at the wrong paragraph.

## Build outputs

| Output                                          | Purpose                                                           |
| ----------------------------------------------- | ----------------------------------------------------------------- |
| `dist/**/index.html`                            | one fully rendered page each                                      |
| `dist/_astro/*`                                 | fingerprinted CSS, one JS bundle, self-hosted fonts, lazy Mermaid |
| `dist/<page>.md`                                | each page as Markdown, with a canonical header                    |
| `dist/og/<page>.png`                            | a generated 1200×630 card per page                                |
| `dist/search-index.json`                        | full-text index, fetched on first search                          |
| `dist/agent-manifest.json`                      | per-page outline with line offsets                                |
| `dist/llms.txt`, `llms-full.txt`                | corpus map and corpus                                             |
| `dist/sitemap.xml`, `robots.txt`, `updates.xml` | discovery                                                         |
| `dist/openapi.json`                             | when a spec is configured                                         |
| `dist/_headers`, `_redirects`                   | host-level caching, security, redirects                           |

## Performance decisions worth knowing

**The search index is a separate file.** It carries every page's body text.
The build fetches it only the first time a reader opens search; it is never
inlined. Page weight therefore stays flat as the corpus grows.

**Fonts are self-hosted.** Astro downloads the family at build time and
emits the `@font-face` rules and preloads. This avoids a render-blocking
round trip to a font CDN.

**Both Shiki themes ship as CSS variables.** `defaultColor: false` stops
Shiki from inlining one theme's colours, which would override dark mode.
Switching themes then costs no re-render.

**There is deliberately no Content-Security-Policy.** A useful policy needs
a strict `script-src` alongside a permissive `style-src-elem`. This is
because Mermaid draws a diagram by injecting a `<style>` element and setting
`style` attributes on every shape. It generates these at runtime from the
current colour mode, so nothing here can be hashed ahead of time. Astro's
CSP API cannot express that split. Shipping a policy that breaks a documented
feature is worse than shipping no policy. A policy loose enough to work would
use `unsafe-inline` throughout, which blocks nothing. This choice is worth
revisiting if diagrams are ever pre-rendered to static SVG.

## The Worker

The Worker serves every byte from `dist` through the `ASSETS` binding, and it
renders nothing itself. It adds header-based discovery, `Accept:
text/markdown` negotiation, and the agent surfaces. See
[`DEPLOYMENT.md`](/guides/deployment) and [`AGENTS.md`](/agents/overview).

## Testing

There are two test suites, and both run against a real build:

- `test/publication.test.mjs` derives its cases from `content/` and asserts
  the publication rules across every output: that a published page reaches
  all of them, a draft reaches none, and a `noindex` page is routed but
  unlisted. It runs over `dist` rather than calling the resolver directly,
  because the rule is an agreement between eleven outputs, and every past
  leak was one output disagreeing with the other ten.
- `test/agents.test.mjs` boots a Worker and exercises MCP and HTTP over the
  network, checking that the two return byte-identical responses.
