Hosting docs at a subpath
Mount your Tangly docs under /docs (or any path) on an existing site. Build flag, reverse proxies, and edge rewrites.
~ 5 min read
Hosting docs at a subpath
You probably already have a marketing site, an app, or both. Putting docs at docs.example.com works fine, but a lot of teams prefer example.com/docs. One domain, shared analytics, no extra DNS, and links from the homepage stay first-party. This page covers what Tangly does to make that work and the realistic ways to wire it up.
The short version: build with --base /docs, then either drop the output into your site or point a rewrite at it.
What --base does
bun x tangly build --base /docsTwo things happen. Every internal link in the rendered HTML gets prefixed with /docs/: sidebars, breadcrumbs, prev/next, search results, hashed asset URLs, the redirect on the index page. The build outputs that travel alongside (sitemap.xml, robots.txt, llms.txt, llms-full.txt) and the Pagefind search index also use the prefix, so result clicks land at the right URL.
If you set siteUrl in docs.json, Tangly composes absolute URLs as ${siteUrl}${base}/${slug} for the sitemap and robots. Set siteUrl to the canonical origin (https://example.com) without the prefix. The prefix comes from --base. Setting both will double up.
The flag is borrowed from Astro’s base config option, which Tangly threads through. Astro’s docs cover the same idea for stand-alone Astro projects, which is useful background if you want to understand what the rendered HTML looks like under the hood.
Strategy 1: build into your site’s public directory
Easiest path if your existing site is built with anything that serves a public/ (or static/, or dist/) directory verbatim. Next.js, Vite, Remix, TanStack Start, Astro, SvelteKit, plain HTML, all of them work. Run a Tangly build into that directory before the site build:
// package.json on the parent site
{
"scripts": {
"build:docs": "cd ../docs && bun x tangly build --base /docs --out ../site/public/docs",
"build": "bun run build:docs && next build"
}
}That writes a self-contained tree at public/docs/ containing index.html, every page, the Pagefind index, sitemap, llms.txt, and the hashed assets. Your hosting provider serves it as-is. No rewrite rules. No proxy hop.
Trade-offs: one repo, one deploy, one thing that can break. The docs and the site ship together, so if your docs change daily and your site changes weekly, you’ll deploy the site weekly anyway. The file count under public/ also grows, which can slow CI uploads on some platforms.
Strategy 2: separate deploy + edge rewrite
Deploy Tangly as its own project (its own Vercel/Cloudflare/Netlify site, its own bucket, anywhere) at dg-docs.vercel.app/docs. Then add one rewrite rule on your main site that proxies /docs/* to the docs origin. The browser only ever sees example.com/docs/*. The docs project URL stays invisible.
Vercel calls this an external rewrite and explicitly supports it for incremental migrations and reverse-proxy use cases:
// vercel.json on the parent site
{
"rewrites": [
{ "source": "/docs", "destination": "https://dg-docs.vercel.app/docs" },
{ "source": "/docs/:path*", "destination": "https://dg-docs.vercel.app/docs/:path*" }
]
}Netlify supports the same pattern via their _redirects file or netlify.toml. A 200 status code is the signal for a proxy rewrite (vs a 301/302 redirect):
# _redirects on the parent site
/docs https://dg-docs.netlify.app/docs 200
/docs/* https://dg-docs.netlify.app/docs/:splat 200
For Next.js sites, use next.config.js rewrites. The shape is similar:
module.exports = {
async rewrites() {
return [
{ source: "/docs", destination: "https://dg-docs.vercel.app/docs" },
{ source: "/docs/:path*", destination: "https://dg-docs.vercel.app/docs/:path*" },
];
},
};Trade-offs: two projects, two CI pipelines, two URLs. The upside is that docs and app iterate independently, rollbacks are scoped, preview URLs work for each side, and the docs project is reusable if you ever spin up a second product.
One thing to watch: when the docs project responds, its absolute URLs (canonical, OG image, sitemap entries) need to point at example.com/docs/..., not dg-docs.vercel.app/docs/.... Set siteUrl: "https://example.com" in your docs.json and Tangly composes URLs against that, not against the deploy URL. Verify by viewing source on a deployed page and checking the <link rel="canonical"> tag.
Strategy 3: Cloudflare
Cloudflare’s behaviour depends on how you want to do it.
For simple path rewriting on the same origin, URL Rewrite Rules cover it. They rewrite the URI path or query string before the request reaches your origin. They don’t proxy to a different origin though, so they won’t pull /docs/* from a separate Cloudflare Pages project on their own.
For cross-origin path mounting, the standard pattern is a Cloudflare Worker as a reverse proxy. The Worker runs on example.com, intercepts requests under /docs/*, and fetches the corresponding path from the Pages project hosting your docs. Worth a few lines of code, and the upside is full control over caching headers and request transforms.
// worker.js
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname.startsWith("/docs")) {
const target = new URL(url.pathname + url.search, "https://my-docs.pages.dev");
return fetch(new Request(target, request));
}
return fetch(request);
},
};Strategy 4: nginx (self-hosted)
If you’re behind nginx, two location blocks do the job. The trick is making sure the upstream serves the same prefix Tangly was built with, otherwise links break. Build with --base /docs and proxy /docs/ to the upstream root, and the prefixes line up.
# example.com nginx
location /docs/ {
proxy_pass http://docs-upstream:8080/docs/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}The official nginx reverse-proxy guide covers the basics. If you ever need to rewrite response bodies (you usually don’t, since Tangly already builds with the prefix baked in), ngx_http_sub_module can substitute strings in proxied responses, but it’s not built into stock nginx in many distributions.
If your upstream is just static files on disk, skip the proxy entirely:
location /docs/ {
alias /var/www/docs/;
index index.html;
try_files $uri $uri/index.html =404;
}alias maps the URL prefix to a filesystem path. Same idea as Strategy 1, but for hosts that aren’t framework-shaped.
Strategy 5: GitHub Pages under a project repo
GitHub Pages serves project repos at https://USER.github.io/REPO/, which is a subpath whether you wanted one or not. Build with --base /REPO:
- run: bun x tangly build --base /REPO-NAME
- uses: actions/upload-pages-artifact@v3
with:
path: distSame flag, different reason for using it. Astro’s GitHub Pages deploy guide walks through the surrounding workflow. Tangly inherits the relevant pieces.
Sub-subpaths and what to set in docs.json
The --base flag accepts any path: /docs, /docs/v2, /product/docs/v2. Tangly normalizes leading and trailing slashes, so /docs, /docs/, docs, and docs/ all behave identically.
Two docs.json fields interact with subpath hosting:
| Field | What it does | Recommendation |
|---|---|---|
siteUrl | Origin for absolute URLs in sitemap, robots, OG metadata, canonical | Set to the public origin only (https://example.com), no path. |
--base (CLI) | Path prefix every internal link gets | Match the path you’ll be served at (/docs). |
Don’t put the path inside siteUrl. Tangly composes them itself, and setting both will double-prefix.
Verifying
After deploying, the smoke test is the same as for any deploy with extra eyes on the prefix:
# Built tree should never reference root paths in internal links.
bun x tangly build --base /docs
grep -rEn 'href="/[^d]' dist/ | grep -v 'href="/docs' | head
# (anchors and external URLs are fine; only flag root-relative internal hrefs)
# llms.txt should list every page with the prefix.
grep '](/' dist/llms.txt | head
# Pagefind result URLs should resolve under /docs.
# Click a result on the deployed site; the URL bar should land at /docs/<slug>.The browser test matters more than the grep test. Click the sidebar, click a search result, click an embedded link in an MDX page, click the logo. Every one should stay under /docs.
When not to do this
If your docs need authenticated access, dynamic per-user content, or shell elements (top nav, sidebar, account dropdown) shared with the parent app, a static subpath mount won’t help. You need the docs to live inside the app’s render tree, which is a bigger project than this guide covers.
For everything else, pick the strategy that matches how the parent site is already deployed. Strategy 1 if you want one deploy and one repo. Strategy 2 if you want them independent.