WritedocsWritedocs

Serving under a path

Your docs can live at a path of your product’s domain - example.com/docs - instead of a domain of their own. Your site keeps serving everything else, and a proxy in front of it passes /docs and everything under it to the docs site.

Tell Writedocs the path with basePath, and every address the docs make starts with it.

basePath

writedocs.json
{
  "domain": "example.com",
  "basePath": "/docs"
}
basePath string

The path the site is served under, like /docs or /help/docs. It starts with / and has no / at the end; each part has lowercase letters, digits, - and _ only. Leave it out (or set "" or "/") for a site at the root of its domain - the default. Any other value is an error in writedocs validate and in the editor.

Set domain to the domain your readers see (example.com), not the address of the host behind the proxy: full URLs - canonical links, sitemap.xml, llms.txt, link previews - are domain + basePath, like https://example.com/docs/guides/setup/.

What changes

With "basePath": "/docs", every address the site makes starts with /docs:

  • the links in the sidebar, tabs, version and language switchers, dropdowns, breadcrumbs, the footer, the topbar and the page menu;
  • the site’s own files - styles, scripts, fonts, icons, the logo and favicon, link preview images;
  • search;
  • sitemap.xml, robots.txt, llms.txt, llms-full.txt and each page’s Markdown copy;
  • the MCP server, at /docs/mcp;
  • redirects, and the redirects from /docs and from folder addresses to the first page.

Your pages keep site addresses

In your pages and in writedocs.json, keep writing addresses from the site’s root, without /docs. The base is added for you:

You writeReaders get
[Setup](/guides/setup)/docs/guides/setup
![Diagram](/images/flow.png)/docs/images/flow.png
<a href="/api/">, <img src="/logo.svg">/docs/api/, /docs/logo.svg
<Card href="/guides/setup">/docs/guides/setup
"favicon": "/images/favicon.svg"/docs/images/favicon.svg
{ "source": "/old", "destination": "/new/" } in redirects/docs/old → /docs/new/
../setup, https://example.com/pricing, #installunchanged

A link written with the base already in it - /docs/guides/setup - gets it twice and leads nowhere. writedocs broken-links reports it.

The same pages build with or without basePath, so you can move the docs between docs.example.com and example.com/docs without editing them.

The built site

writedocs build writes the site into a folder named after the path, so the files sit where their addresses say:

dist/
├── docs/              ← the whole site: pages, search, sitemap.xml, llms.txt, ...
│   ├── index.html
│   ├── guides/setup/index.html
│   ├── robots.txt
│   └── sitemap.xml
├── 404.html           ← the "not found" page, with links under /docs
├── _redirects         ← redirects, every path under /docs, plus / → /docs/
└── _headers           ← only if your project has one

At the root of dist/ there are only the files a host reads there: 404.html, _redirects, _headers, and the MCP server’s _worker.js, _routes.json and .assetsignore for Cloudflare. Serve dist/ at the root of any static host and /docs/... works with no extra rules.

writedocs dev serves the preview under the path too: it opens at http://localhost:4321/docs/.

Behind your proxy

Deploy the built site to a host - call it docs-host.example.net - and have your main site pass /docs and everything under it on to that host, with the path unchanged: a request for example.com/docs/guides/setup/ asks the host for /docs/guides/setup/. Everything outside /docs stays yours.

worker.js
const DOCS_HOST = 'docs-host.example.net';

export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === '/docs' || url.pathname.startsWith('/docs/')) {
      url.hostname = DOCS_HOST;
      return fetch(new Request(url, request));
    }
    return fetch(request); // the rest of your site
  },
};

Route the Worker to example.com/docs*, or to the whole domain if it also serves the rest of your site.

vercel.json
{
  "rewrites": [
    { "source": "/docs", "destination": "https://docs-host.example.net/docs" },
    { "source": "/docs/:path*", "destination": "https://docs-host.example.net/docs/:path*" }
  ]
}
next.config.js
module.exports = {
  async rewrites() {
    return [
      { source: '/docs', destination: 'https://docs-host.example.net/docs' },
      { source: '/docs/:path*', destination: 'https://docs-host.example.net/docs/:path*' },
    ];
  },
};
nginx.conf
location /docs {
    proxy_pass https://docs-host.example.net;   # no path after the host: the path goes as it came
    proxy_set_header Host docs-host.example.net;
    proxy_ssl_server_name on;
}
  • Keep the path. A proxy that strips /docs before passing the request on breaks every page: the site’s files are under /docs/ on the host too.
  • Send the host’s name. The request that reaches the docs host should carry its own name in the Host header (docs-host.example.net), not yours - the Cloudflare, Vercel and Next.js examples do this by themselves; in Nginx it’s the proxy_set_header Host line.
  • Redirects pass through. The site’s redirects answer with addresses under /docs, so the reader stays on your domain.
  • “Not found” pages come from the host, with the status 404 and the site’s own look. An address outside /docs is never sent to the docs.

Limits

  • robots.txt and the sitemap. Search engines only read robots.txt at the root of your domain - example.com/robots.txt, which is yours, not the docs’. The docs’ own copy at /docs/robots.txt has the rules and the Sitemap: line to copy into yours:

    example.com/robots.txt
    User-agent: *
    Allow: /docs/
    
    Sitemap: https://example.com/docs/sitemap.xml

    Or submit https://example.com/docs/sitemap.xml in your search engine’s webmaster tools (Google Search Console, Bing Webmaster Tools).

  • Your own CSS and JavaScript files are used as written. An address inside a .css or .js file of your project, or in the content of a scripts entry, doesn’t get the base: write it in full (url(/docs/images/bg.png)), or, in a file in public/, relative to the file. The same goes for a _redirects or _headers file of your own: its rules match full paths, so they start with /docs.

  • Addresses computed in code aren’t changed. An attribute written as an expression - <a href={link}> - and anything a React snippet renders are left as they are; include the base yourself there.

  • Only one place at a time. A build serves the docs under one path. To publish them at the root of a domain as well, build again without basePath.