Deploying
Ship your docs to Vercel, Cloudflare, Netlify, Docker, or any static host.
~ 3 min read
tangly build produces a static site at dist/ that drops onto any CDN. The adapter is auto-detected from your project; override with --adapter.
Adapter auto-detection
| File present in project root | Adapter selected |
|---|---|
vercel.json | vercel |
wrangler.toml or wrangler.jsonc | cloudflare |
Dockerfile | node |
| (none) | static |
Vercel
The simplest path. Add vercel.json (even empty {} is fine; its presence is the auto-detect signal) and:
bun x tangly build
vercel deploy ./distFor a git-connected project, configure the build through vercel.json:
{
"buildCommand": "bun x tangly build",
"outputDirectory": "dist",
"framework": null
}Or set the same in the Vercel dashboard:
| Setting | Value |
|---|---|
| Framework Preset | Other |
| Build Command | bun x tangly build |
| Output Directory | dist |
| Install Command | bun install |
Cloudflare Workers (static assets)
Workers with static assets is Cloudflare’s current way to host a static site (Pages is in maintenance mode). An assets-only Worker needs no script — just a wrangler.jsonc:
{
"name": "my-docs",
"compatibility_date": "2026-06-10",
"assets": {
"directory": "./dist",
"html_handling": "drop-trailing-slash",
"not_found_handling": "404-page"
},
"routes": [{ "pattern": "docs.example.com", "custom_domain": true }]
}bun x tangly build
bunx wrangler deployTwo settings matter:
html_handling: "drop-trailing-slash"— Tangly emits directory-style output (cli/audit/index.html) but canonical URLs without a trailing slash (/cli/audit, Mintlify parity). Cloudflare’s default (auto-trailing-slash) 307-redirects every page to the slash variant — each URL then contradicts its own canonical.drop-trailing-slashserves/cli/auditdirectly and redirects/cli/audit/down to it, matching the canonicals.not_found_handling: "404-page"— serves Tangly’sdist/404.html(your404.mdxif you have one) for unknown routes.
The presence of wrangler.toml/wrangler.jsonc triggers the cloudflare adapter on auto-detect, and the build’s copy step excludes wrangler.* from dist/ automatically.
Cloudflare Pages (maintenance mode)
Existing Pages projects keep working:
bun x tangly build
bunx wrangler pages deploy ./dist --project-name my-docsFor a git-connected Pages project, in the Cloudflare dashboard:
| Setting | Value |
|---|---|
| Framework preset | None |
| Build command | bun x tangly build |
| Build output directory | dist |
| Root directory | / (or your subdir) |
Netlify
No special adapter; Netlify serves any static directory. Add netlify.toml:
[build]
command = "bun x tangly build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Or via the dashboard with the same build command and publish directory.
Plain static (S3, R2, GitHub Pages, nginx)
bun x tangly build
# Upload dist/ to your host.
# S3:
aws s3 sync dist/ s3://my-bucket --delete
# rsync to a VPS:
rsync -avz --delete dist/ user@host:/var/www/docs/
# GitHub Pages: commit dist/ to the gh-pages branch.For nginx, point the server root at dist/:
server {
listen 80;
server_name docs.example.com;
root /var/www/docs;
index index.html;
location / {
try_files $uri $uri/index.html =404;
}
}
Docker
Drop a Dockerfile to trigger the node adapter (still static today):
FROM oven/bun:1.2-alpine AS build
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile
RUN bun x tangly build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
nginx.conf:
server {
listen 80;
root /usr/share/nginx/html;
location / {
try_files $uri $uri/index.html =404;
}
}
Build and run:
docker build -t my-docs .
docker run -p 8080:80 my-docsGitHub Pages
name: Deploy docs
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun x tangly build --base /REPO-NAME
- uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4The --base flag is required when serving under https://USER.github.io/REPO/.
Subpath hosting
Deploy under /docs instead of the host root:
bun x tangly build --base /docsEvery internal link, asset URL, sitemap entry, llms.txt URL, and Pagefind result is prefixed with the base. See Hosting docs at a subpath for the five common strategies: building into a parent site’s public/, edge rewrites on Vercel/Netlify/Cloudflare, nginx reverse proxy, and GitHub Pages project repos.
What gets built
dist/
- index.html redirects to first nav page
introduction/
- index.html
- introduction.md raw source for AI agents
guides/
<slug>/- index.html
<slug>.mdraw markdown twin
- images/ copied verbatim from project root
- logo/
- public/
- pagefind/ search index
- sitemap.xml
- robots.txt
- llms.txt for AI crawlers
- llms-full.txt
Drafts (draft: true in frontmatter) are excluded. noindex: true pages are emitted but excluded from sitemap.xml, llms.txt, llms-full.txt, the per-page .md files, and the Pagefind index.
Markdown for agents
Tangly emits a raw-Markdown twin of every page (<slug>.md alongside <slug>/index.html) so coding assistants can fetch source instead of rendered HTML; ~10× token reduction. Works on any static host with no extra config: append .md to any URL and you get the source.
For same-URL content negotiation (Accept: text/markdown returns Markdown, browsers still get HTML), add a one-line CDN rewrite. See Markdown for agents for Vercel, Cloudflare, and Netlify recipes.
Pre-deploy checklist
bun x tangly check --strict # exit 0 means clean
bun x tangly build
bun x tangly preview # smoke test in browserCI: drop the same three commands into your pipeline. tangly check --strict --json plays well with annotation scripts.