Duvlify
GitHubGet started
The frameworkArchitecture
The framework

How the build works

Astro with static output and no framework runtime: the join between content and config, what publication means, and every file the build emits.

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

plaintext
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

OutputPurpose
dist/**/index.htmlone fully rendered page each
dist/_astro/*fingerprinted CSS, one JS bundle, self-hosted fonts, lazy Mermaid
dist/<page>.mdeach page as Markdown, with a canonical header
dist/og/<page>.pnga generated 1200×630 card per page
dist/search-index.jsonfull-text index, fetched on first search
dist/agent-manifest.jsonper-page outline with line offsets
dist/llms.txt, llms-full.txtcorpus map and corpus
dist/sitemap.xml, robots.txt, updates.xmldiscovery
dist/openapi.jsonwhen a spec is configured
dist/_headers, _redirectshost-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 and AGENTS.md.

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.

Use these docs with your AI tools

An AI agent can read this documentation directly. You do not need an account or an API key. Everything here is public and read-only.

Query these docs via MCP

Recommended

Add this server to Claude, Claude Code, Cursor, Mistral, or any tool that supports MCP. Your agent can then search Duvlify documentation and read it in full, instead of answering from memory.

https://duvlify.dev/mcp
  • searchFind the passages that answer a question.
  • fetchRead one page in full, as Markdown.
  • list_pagesSee every page in this documentation.

Query these docs over HTTP

The same tools also work as plain web requests. Use this for scripts, or for any tool that does not support MCP. There is one endpoint per tool. Arguments go in the query string, and the answer comes back as JSON.

https://duvlify.dev/api/docs/search?query=custom+domain

Read the OpenAPI description. It is built from the same definitions as the tools, so it always matches what the endpoints do.

Read these docs as Markdown

Add .md to any page URL to get its Markdown source. You can also send the headerAccept: text/markdown to the page URL itself.

To read the whole documentation in one file, open llms-full.txt. For a short index of every page, open llms.txt.