Skip to content
Blume
Esc
↑↓navigate↵open⌘Jpreview
Guides

Hosting

Choose static or server-rendered documentation

Map each docs feature to static files or a live endpoint, build the same site both ways, and check what each build ships and what you would have to run.

By 7 min read

Most of a docs site never needs a backend. Pages, navigation, local search,llms.txt, Markdown copies, redirects, and social cards are files a build writes once and any static host serves. In Blume, four features need a live endpoint on your own site: the built-in assistant, the MCP server, the API playground's built-in proxy, and Mixedbread semantic search. If you don't need any of them, the static build Blume makes by default covers you.

This guide maps common requirements to static files or live endpoints, then builds the same Acme docs twice, once static and once as a Node server, with a checklist that shows what each build shipped. It ends with what you take on when you run a server.

Server output adds endpoints, not dynamic pages: Blume prerenders every docs page in both modes, and it has no sign-in of its own. If you need pages that change per reader, such as content gated by a customer's plan, that's an application, and Blume isn't the right tool for it.

Which features need a server

RequirementStatic buildServer output
Pages, OG images, llms.txt, .md copies, the JSON page indexFilesStill prerendered files
Local search: Orama (default), FlexSearch, PagefindAn index file the browser searchesSame
Hosted search: Algolia, Orama Cloud, TypesenseThe browser queries the service with a public keySame
Semantic search: MixedbreadNot available/api/search holds the key
The assistant, built inNot availablePOST /api/ask calls your model
The assistant, on your own backendThe chat panel posts to your endpointNot needed
MCP serverNot available/mcp
JSON API searchNot available/api/docs/search
API playground, direct or through your own proxy URLThe browser calls your API or proxySame
API playground with proxy: trueNot available/_api-proxy relays requests
Markdown at a page's own URL (Accept: text/markdown)Agents fetch the .md URL insteadOn Vercel and Cloudflare server builds
RedirectsRedirect pages, plus _redirects or vercel.json for hosts that read themAnswered per request with your status code

PDF and EPUB export, narration, analytics, and page feedback need no endpoint on your site: they run in the reader's browser, or, for generated narration voices, at build time. They work on either build. Four distinctions in that table trip people up:

  • Local and hosted search both stay static. Hosted search moves the backend to the provider, or to your own Typesense cluster, and blume build syncs the index with an admin key from the environment. Only Mixedbread proxies queries through your site, because its key can't go to the browser.
  • MCP needs a server, reading pages doesn't. Agents can read llms.txt, .md copies, and /api/docs/pages.json from any static host. The MCP endpoint and the JSON API's search have to answer requests.
  • The assistant has two shapes. The built-in one is a route on your site that holds your model key. With endpoint, the panel talks to a backend you run, and the docs stay static.
  • The playground proxy is optional. Requests from the Try it panel go straight from the browser to your API, which has to allow the docs origin with CORS. Only proxy: true adds a server route. See Fix CORS errors in an API documentation playground for that choice.

Build the same docs both ways

Scaffold a project, then replace the pages in docs/ with these two:

npx blume init acme-docs --yes
cd acme-docs
---
title: Acme docs
description: Send transactional email and SMS with the Acme Messages API.
---

Acme sends transactional email and SMS through one API. Start with the
[quickstart](/quickstart).
---
title: Quickstart
description: Send your first message with the Acme Messages API.
---

Create an API key in the Acme dashboard, then send a message with
POST /messages. Each message returns an ID you can look up later.

The static build

The plain deployment object sets the site URL and keeps the build static. The assistant points at a backend you run, so this build has no server route:

import { defineConfig } from "blume";

export default defineConfig({
  title: "Acme",
  deployment: { site: "https://docs.acme.example" },
  ai: {
    assistant: {
      enabled: true,
      endpoint: "https://api.acme.example/v1/docs/ask",
    },
  },
});

Build it and serve the result the way a static host would:

npx blume build
npx blume preview

The server build

Naming a host adapter switches the build to server output. This one turns on the built-in assistant and the MCP server, and uses node(), which ships with Blume, so there's nothing to install:

import { defineConfig } from "blume";
import { node } from "blume/deploy";

export default defineConfig({
  title: "Acme",
  deployment: node({ site: "https://docs.acme.example" }),
  ai: {
    assistant: { enabled: true },
  },
  agents: {
    mcp: { enabled: true },
  },
});

With no provider, the assistant answers through the Vercel AI Gateway and reads its key from AI_GATEWAY_API_KEY. Build, then start the server:

npx blume build
node dist/server/entry.mjs

It listens on localhost:4321. npx blume preview serves the same build.

Check what each build shipped

Run these checks against each build in turn.

  1. Read the build summary. blume build ends with a summary box. The static build reports Output as static and Server features as none. The server build reports server, adapter node, and Assistant, MCP server. An assistant with an endpoint isn't a server feature, so it isn't listed.
  2. Ask blume doctor. Without building, it prints the same output and adapter, plus an Assistant line: external endpoint for the static config and gateway for the server one. On the server config, it warns with BLUME_MISSING_SECRET until AI_GATEWAY_API_KEY is set.
  3. Fetch the files both builds serve. Every line should start with 200:
for path in / /quickstart /quickstart.md /llms.txt /blume-search.json /api/docs/pages.json; do
  printf '%s %s\n' "$(curl -sL -o /dev/null -w '%{http_code}' "http://localhost:4321$path")" "$path"
done

On the server build, call the three endpoints the static build doesn't have. The MCP server lists its tools (search_docs, get_page, list_pages, and get_navigation), and the JSON API's search returns matching pages:

curl -s http://localhost:4321/mcp \
  -H "content-type: application/json" \
  -H "accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

curl -s "http://localhost:4321/api/docs/search?q=quickstart"

Then ask the assistant a question:

curl -s -i http://localhost:4321/api/ask \
  -H "content-type: application/json" \
  -d '{"messages":[{"role":"user","content":"How do I send an SMS?"}]}'

If the server started without the key, the route answers 503 with The assistant is not configured: set AI_GATEWAY_API_KEY, which still proves the route exists. Start the server with the key in its environment and the answer streams back as plain text.

On the static build, open the assistant on any page and ask a question. The browser's network panel shows a POST to https://api.acme.example/v1/docs/ask. It fails here, because that host doesn't exist. With your real backend, that's the request to test.

Keep the docs static with an external assistant

If you want the assistant but not a server on the docs host, endpoint is the middle path. The panel sends the conversation and the reader's page:

{
  "messages": [{ "role": "user", "content": "How do I send an SMS?" }],
  "page": { "path": "/quickstart" }
}

Your backend answers with a plain UTF-8 text stream. On another origin, it also handles CORS: accept OPTIONS and POST, allow the content-type request header, and send the CORS headers on both the preflight and the stream. A root-relative path works as the endpoint too, for a backend served on the docs origin.

Everything the built-in route did becomes that backend's job: retrieval from your docs, authentication, rate limiting, model access, and citations. If you set a bot check with captcha, the panel still runs it and sends the token as captcha in the body for your backend to verify. For the built-in version, see Add an AI assistant that answers from your documentation.

What you run in each mode

ResponsibilityStatic buildServer build
What you deployThe dist/ folderOn Node, the project with its installed dependencies; on Vercel, Netlify, or Cloudflare, the platform's functions or Worker
Where it runsAny static host, bucket, or CDNVercel, Netlify, Cloudflare, or a Node process you keep running
Secrets per requestNoneThe assistant's model key, MIXEDBREAD_API_KEY
Abuse controlNothing to limitRate limiting, on by default; a shared store for exact limits on serverless hosts; a bot check for the assistant
Page requestsServed as filesStill served as prerendered files
Local previewblume previewblume preview for node() and cloudflare(); blume dev or a platform preview deploy for vercel() and netlify()

Some things don't change. Build-time secrets, like a content source's token or a hosted search admin key, are read where the site builds in both modes. Private docs rely on your host's access protection either way. To deploy the server build, see Docker and Node.js or Cloudflare Workers; for the static one, S3 and CloudFront or GitHub Pages. For what each mode costs, see pricing, and Specific's story shows one team running the Node server with MCP on and one secret for the assistant.

Troubleshooting

The build fails with BLUME_SERVER_FEATURE_REQUIRED

A static build has a feature that needs a server, and the message names each one, such as Assistant, MCP server require server output. Name a host adapter from blume/deploy, or, if you already did, drop its output: "static". To stay static instead, give the assistant an endpoint, give the playground a proxy URL of your own, or pick a client-side search adapter. The MCP server has no static form; agents can read llms.txt and the .md copies instead.

blume preview stops after a Vercel or Netlify server build

Those adapters have no local preview server. Try the site with blume dev, or deploy a preview with vercel deploy or netlify deploy.

The assistant answers 503

The server can't see the model key, and the message names the variable it reads. .env.local covers blume dev; set the variable in the server's own environment, or your host's settings, for the deployed site.

The Node server isn't reachable from other machines

It binds to localhost:4321. Set HOST and PORT when you start it, as in HOST=0.0.0.0 PORT=8080 node dist/server/entry.mjs. The server also resolves its packages through your project's node_modules, so deploy the installed project, not dist/ alone.

Redirects land on a page instead of a real redirect

A static build writes redirect pages plus the file your host reads. A Git-connected Vercel project never reads the vercel.json in dist/: copy its redirects into the vercel.json at your project root, or use a vercel() server build. On Netlify, name the host with netlify({ output: "static" }) so the rules are forced.

Agents get HTML at a page's URL

Content negotiation on Accept: text/markdown works in blume dev and on Vercel and Cloudflare server builds. Other deployments serve prerendered pages with no request-time hook, so agents there fetch the .md URL. See Serve HTML and Markdown from the same documentation URL.

Next step

Check your own config

Run doctor in your project. It prints the output mode and adapter, and names any feature that needs server output before you build.

npx blume doctor
Read the server rendering docs

A step here not working for you? Report a broken step.

Keep going.More guides.

Upgrade your docs with Blume.

Install today and ship a production-grade docs site in minutes. Free and open source, forever.

npx blume init