Hosting
Deploy Markdown docs to GitHub Pages
Turn the Markdown in your repository into a searchable docs site on GitHub Pages that rebuilds every time you push.
By Hayden Bleasel7 min read

By the end of this guide, the Markdown in your repository is a docs site at your GitHub Pages project URL, with sidebar navigation built from your folders, local search, and a sitemap. A GitHub Actions workflow rebuilds and publishes it every time you push, and your library's own scripts keep working as they did.
GitHub can also publish Markdown straight from a branch, by running Jekyll over your files. This guide takes the other route GitHub offers: Blume builds the site in a workflow, and GitHub serves the files it produces. Nothing is committed to a separate branch.
The examples use a fictional repository, acme/widget-sdk, whose site ends up at https://acme.github.io/widget-sdk/. Replace acme and widget-sdk with your own account and repository names throughout.
Add Blume to your repository
From the root of your repository, on a new branch, scaffold the Blume files:
npx blume initKeep the default, ., when it asks where to create the project, so Blume sits beside your code, and give the folder that holds your Markdown as the content directory (docs by default). init never overwrites a file that already exists, so in a repository with its own package.json it:
- Writes
blume.config.ts, with acontent.rootwhen your Markdown lives somewhere other thandocs. - Adds a home page at
docs/index.mdxwhen that file doesn't exist yet. If the folder already has anindex.md, delete the new file so the two don't compete for the same URL. - Adds
node_modules/,.blume/, anddist/to.gitignore, skipping any already there. - Leaves your
package.jsonalone and skips the install, then prints what's left to add.
Install Blume as a dev dependency, so it stays out of the dependencies your published package ships with:
npm install --save-dev blumeThen add scripts for the docs. If your dev and build scripts already run your library, give the docs their own names rather than replacing them:
{
"scripts": {
"build": "tsup",
"docs:dev": "blume dev",
"docs:build": "blume build"
}
}Run it locally
npm run docs:devOpen http://localhost:4321 and click through the site. Check three things before you deploy anything:
- Navigation. Folders become sidebar groups and files become pages. A page with no frontmatter still works: it takes its title from its first heading, or failing that, its file name. Add a
titleanddescriptionin frontmatter where you want better sidebar labels and search results, and remove the page's own# Headingline when you do, since Blume renders the title as the page heading. - Links and images. Links written for GitHub, like
[Setup](./setup.md), land on the page that file publishes. Relative images likeare optimized at build time, so keep images beside the pages that use them. - Search. Open search from the header and look for a word you know is on a page. The index is built locally, so there's no service or key to set up.
Your repository's own README.md sits outside the content folder, and Blume can't include a file from outside it. Move the long-form parts, like installation and usage, into docs/index.mdx, and keep the README short, with a link to the docs site.
Before you move on, run the link checker. It reads your content the way a build does and fails on any link that doesn't resolve:
npx blume validateSet the site URL and base
A project site is served from a subfolder named after the repository, not from the root of a domain. Blume needs to know both halves: the origin, for canonical URLs and the sitemap, and the subfolder, so every link and asset points inside it. Add them to your config:
import { defineConfig } from "blume";
export default defineConfig({
title: "Widget SDK",
deployment: {
site: "https://acme.github.io",
base: "/widget-sdk",
},
});Keep site to the origin alone and put the repository path in base. Blume adds the base to every internal link and asset for you, so keep writing links as if the site were at the root. Unlike Vercel or Netlify, GitHub Pages doesn't expose its URL to the build, so without site the build has no origin and leaves out the sitemap and absolute canonical URLs.
Blume also has a top-level basePath, which is a different thing. basePath moves your pages under a path like /docs inside a site whose root is yours. deployment.base is the folder the whole site is served from. For a project site you want base. If the repository is named acme.github.io, GitHub serves it at the root of that domain, so leave base out.
With the base set, the dev server serves the site under it too, at http://localhost:4321/widget-sdk. To see the production build exactly as a static host serves it, build and preview it:
npm run docs:build
npx blume previewTurn on GitHub Actions for Pages
In your repository on GitHub, open Settings, then Pages. Under Build and deployment, set Source to GitHub Actions. You don't need one of the suggested workflow templates: you'll add your own next.
Publishing from a workflow matters for Blume in one more way. When GitHub runs Jekyll on a branch, Jekyll skips folders whose names start with an underscore, and Blume's build keeps its styles and scripts in _astro/. A workflow deploys the files exactly as Blume wrote them.
Add the workflow
Create the workflow file. It builds the site on every push to main, then deploys the result:
name: Deploy docs
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npx blume validate
- run: npm run docs:build
- uses: actions/upload-pages-artifact@v5
with:
path: dist
include-hidden-files: true
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v5What each part does:
- permissions gives the run what deploying needs:
pages: writeto publish, andid-token: write, which GitHub requires for a Pages deployment. - concurrency runs one deploy at a time. A push that lands mid-deploy waits for the current one to finish instead of cancelling it.
- build installs your dependencies, runs the link checker so a broken link stops the deploy, builds the site, and uploads
dist/. That's the folder to upload: notdocs/, which holds your source, and not.blume/, which is Blume's working folder. - include-hidden-files keeps folders whose names start with a dot. The upload action leaves them out by default, and Blume's build writes its agent discovery files to
.well-known/. - deploy waits for the build (
needs: build), then publishes the artifact to thegithub-pagesenvironment and reports the site's URL.
Any Node.js version from 22.12 on works. With pnpm, Yarn, or Bun, set up that package manager and swap in its install and run commands. If you turn on lastModified: "git" for "Last updated" dates, add fetch-depth: 0 under the checkout step, since a shallow clone drops most of the history those dates come from.
GitHub Pages serves static files only, so leave out the Blume features that need a server: the built-in assistant, the MCP server, the API playground's built-in proxy, and Mixedbread search. Static or server-rendered documentation maps each one and the static alternatives.
Push and follow the deploy
Commit everything, merge it into main, and push. Open the Actions tab to watch the Deploy docs run. When the deploy job finishes, its summary shows the site's URL. From then on, every push to main updates the site, and you can rerun the workflow by hand from the Actions tab.
Check the live site
Open the site and check the four things that break first on a subfolder:
- A deep link. Paste the URL of a page a few levels down, like
https://acme.github.io/widget-sdk/guides/setup, straight into a new tab. It should load with its styles. - An asset. Images show, and the page's styles load from
/widget-sdk/_astro/. - Search. A search from the header returns results and opens pages under
/widget-sdk/. - The sitemap.
https://acme.github.io/widget-sdk/sitemap.xmllists every page with the full URL.
Use a custom domain
To serve the docs from a domain like docs.acme.example, add it under Custom domain in the repository's Pages settings, then create a CNAME DNS record that points it at acme.github.io, and turn on Enforce HTTPS. A site published from a workflow doesn't need a CNAME file in the repository: GitHub ignores it.
The site then lives at the root of its own domain, so update the config: set site to the new domain and remove base.
deployment: {
site: "https://docs.acme.example",
},Push the change, and the next deploy rebuilds every link for the new address.
Plan for your next major version
When a breaking release is coming, you'll want the old instructions to stay online beside the new ones. Version your docs for a breaking release walks through snapshotting them, and it deploys through this same workflow.
Troubleshooting
The site loads without styles, or images 404
The files are being requested from the root of the domain instead of the repository's folder, so base is missing or doesn't match the repository name exactly, including its case. Check the URL of a broken file in your browser's network panel. A path written in raw HTML, like <img src="/logo.png">, doesn't get the base added, so use Markdown image syntax or a relative path instead.
The deploy job fails on permissions
Check that the workflow has the permissions block above, with pages: write and id-token: write, and that the Pages source is set to GitHub Actions rather than a branch.
The deploy is rejected for your branch
The github-pages environment can limit which branches may deploy. Deploy from main, or review the environment's branch rules under Settings, then Environments.
My library's build output disappeared
blume build empties dist/ before writing the site, so it can't share that folder with another build. Give the docs a folder of their own instead: run npx blume init website, move your Markdown into website/docs/, set the workflow's install and build steps to run in website (with working-directory: website), and upload website/dist.
A link to the README or a source file breaks
Only files in the content folder become pages, so blume validate reports a link like ../README.md or ../src/index.ts. Link to the file's page on GitHub with its full URL instead.
Next step
Put your docs on GitHub Pages
Run it in your repository to add Blume beside your code, then add the workflow above.
npx blume initA step here not working for you? Report a broken step.