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 Hayden Bleasel7 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
| Requirement | Static build | Server output |
|---|---|---|
Pages, OG images, llms.txt, .md copies, the JSON page index | Files | Still prerendered files |
| Local search: Orama (default), FlexSearch, Pagefind | An index file the browser searches | Same |
| Hosted search: Algolia, Orama Cloud, Typesense | The browser queries the service with a public key | Same |
| Semantic search: Mixedbread | Not available | /api/search holds the key |
| The assistant, built in | Not available | POST /api/ask calls your model |
| The assistant, on your own backend | The chat panel posts to your endpoint | Not needed |
| MCP server | Not available | /mcp |
| JSON API search | Not available | /api/docs/search |
| API playground, direct or through your own proxy URL | The browser calls your API or proxy | Same |
API playground with proxy: true | Not available | /_api-proxy relays requests |
Markdown at a page's own URL (Accept: text/markdown) | Agents fetch the .md URL instead | On Vercel and Cloudflare server builds |
| Redirects | Redirect pages, plus _redirects or vercel.json for hosts that read them | Answered 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 buildsyncs 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,.mdcopies, and/api/docs/pages.jsonfrom 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: trueadds 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 previewThe 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.mjsIt listens on localhost:4321. npx blume preview serves the same build.
Check what each build shipped
Run these checks against each build in turn.
- Read the build summary.
blume buildends with a summary box. The static build reportsOutputasstaticandServer featuresasnone. The server build reportsserver, adapternode, andAssistant, MCP server. An assistant with anendpointisn't a server feature, so it isn't listed. - Ask
blume doctor. Without building, it prints the same output and adapter, plus anAssistantline:external endpointfor the static config andgatewayfor the server one. On the server config, it warns withBLUME_MISSING_SECRETuntilAI_GATEWAY_API_KEYis set. - 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"
doneOn 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
| Responsibility | Static build | Server build |
|---|---|---|
| What you deploy | The dist/ folder | On Node, the project with its installed dependencies; on Vercel, Netlify, or Cloudflare, the platform's functions or Worker |
| Where it runs | Any static host, bucket, or CDN | Vercel, Netlify, Cloudflare, or a Node process you keep running |
| Secrets per request | None | The assistant's model key, MIXEDBREAD_API_KEY |
| Abuse control | Nothing to limit | Rate limiting, on by default; a shared store for exact limits on serverless hosts; a bot check for the assistant |
| Page requests | Served as files | Still served as prerendered files |
| Local preview | blume preview | blume 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 doctorA step here not working for you? Report a broken step.