← Back to site Loading…

notabene

Publish a public site

Guide

Generated on August 1, 2026

Publish a public site

Publish a public site

The review app is a dev-local tool — but the docs it renders often deserve a public home. build --public produces a pure-static, read-only artifact made for that:

notabene build --public --site https://you.github.io --base /your-repo --out ./_site

This very site is built that way — notabene’s docs, rendered and published by notabene.

What’s in, what’s out

  • Everything interactive is gone — structurally. No comment UI, no identity prompt, no /comments / /review / /journal, no API, and nothing from the .notabene store. The routes are not gated; they are not built.
  • What remains is the full reading experience: nav, search, Mermaid + image lightbox, dark mode, i18n (per-locale pages, switcher, hreflang), print/PDF routes, 404.
  • Born agent-readable. Every page ships a Markdown twin at <page>/index.md (advertised via <link rel="alternate" type="text/markdown">), the site ships /llms.txt (a machine index of every page, per locale) and /llms-full.txt (the whole doc as one Markdown document in reading order), plus robots.txt, a sitemap, canonical URLs, OpenGraph/Twitter meta and JSON-LD. Try it here: /llms.txt.

Full-text search (optional)

The public site inherits the built-in search as-is. Install Pagefind as a dev dependency and build --public upgrades it to static full-text search:

npm i -D pagefind

The build indexes the final artifact: per-language stemming (a /fr/ visitor searches a French index with French word forms), highlighted excerpts under each result, and a payload that stays small as the docs grow — the browser fetches only the index chunks a query needs. Zero configuration, same search box. Not installed → the public site keeps the built-in JSON search. And private content cannot leak into the index: indexing runs on the artifact, where scoped pages don’t exist.

The same dependency upgrades the notabene dev review app too: its index is built live from your Markdown sources and refreshed as they change — no build involved. Two dev-specific differences: the dev site (and so its index) includes your private pages, and per-section deep results (heading anchors) are public-only.

Where next

The dev loop is untouched: notabene dev and plain notabene build behave exactly as before — publishing is opt-in, per build.

Configuring publish

Configuring publish

Everything lives under one optional config key — the CLI flags (--site/--base) override it, and --out copies the artifact to a stable path (it refuses to overwrite anything it didn’t generate):

// notabene.config.mjs
export default {
  // …
  publish: {
    // Deployed ORIGIN. Bakes absolute URLs into canonical, og:url, JSON-LD,
    // hreflang, llms.txt, the sitemap and robots.txt's Sitemap line.
    // OPTIONAL — omit it to keep the domain out of the repo (see
    // "Domain managed server-side"). Origin only, no path: a sub-path goes in `base`.
    site: "https://you.github.io",

    // Sub-path when the site is served under a prefix (GitHub Pages project
    // site → "/<repo>"). Prefixes every link and asset URL — unlike the domain,
    // a sub-path always affects rendering, it can't be server-side.
    base: "/your-repo",

    // Sub-trees to keep out of public builds — globs matched against
    // `<space key>/<page id>` (locale-independent: one pattern hides every
    // translation of a page). `*` = one path segment, `**` = any depth.
    exclude: ["docs/internal/**", "docs/*/draft"],
  },
};

Three typical setups

publish: { site: "https://you.github.io", base: "/my-repo" }  // GitHub Pages, project site
publish: { site: "https://docs.example.com" }                 // custom domain at the root
publish: { exclude: ["docs/internal/**"] }                    // domain kept out of the repo

The third one is the origin-agnostic mode — same artifact behind any domain.

Per-page metadata

Two frontmatter keys feed the public surfaces (see the full frontmatter reference):

---
description: One-line summary — becomes the meta description / OpenGraph.
publish: false   # this page never ships in a public build
---

Scoping content out of the build has its own page: keep content private.

Keep content private

Keep content private

Three levels, from coarse to fine — each lives where the thing it scopes lives.

A whole space — flag the roots[] entry:

roots: [
  { key: "docs",  label: "Docs",  path: "docs" },
  { key: "notes", label: "Notes", path: "notes", publish: false },  // whole space stays private
],

A sub-treepublish.exclude globs on <space key>/<page id> (that’s the page’s URL path without any locale prefix, so one pattern hides every translation):

publish: { exclude: ["docs/internal/**", "docs/*/draft"] },

A single page — its own frontmatter:

---
publish: false   # this page never ships in a public build
---

A navigation link — not content, but the same idea: a nav entry marked publish: false stays in dev and never reaches the artifact (a dashboard, an internal wiki):

nav: { header: [{ label: "Ops dashboard", href: "https://ops.internal", publish: false }] },

The guarantee

Private content isn’t hidden, it’s not built — no route (the URL 404s), no sidebar entry, no search hit, no llms.txt line, no .md twin, no sitemap entry, no print/PDF inclusion, and the space’s name and path never reach the public HTML.

notabene dev and normal builds always show everything — you review your private docs exactly like the rest. The publish notion exists only for public builds.

If a public page links to a private one, that link 404s in the public artifact — the build doesn’t rewrite or warn about it. notabene lint catches exactly this: run it after build --public and every link from a public page into scoped-out content is reported (the public route truth simply doesn’t contain those pages — see the CLI reference).

Domain managed server-side

Domain managed server-side? Omit site

When the public domain is the server’s business — a vhost or reverse proxy in front, several mirrors, or a domain that isn’t chosen yet — just don’t set site (and pass no --site). The artifact becomes origin-agnostic: not one absolute URL is baked in, so the same output works behind any domain, and changing domains never requires a rebuild.

What changes

SurfaceWith siteWithout
llms.txt / llms-full.txt / .md twinsabsolute URLsroot-relative paths (agents resolve them against the origin they fetched from)
hreflang alternatesabsolutepath-based
canonical, og:url, JSON-LDemittednot emitted — they only mean something with an origin
sitemap + robots.txt Sitemap: lineemittednot emitted — the specs require absolute URLs

What that costs, concretely

Search-engine visibility — nothing else. Without a sitemap, canonical URLs or valid hreflang, crawlers only discover pages by following links, nothing consolidates duplicates if the docs answer on several domains, and multilingual pages send no language signals to search engines. Social link previews keep their title/description but lose the URL card. Human readers and AI agents lose nothing — every page, twin and llms file works identically.

Rule of thumb: internal hosting, a mirror, or a domain that isn’t settled → omit site; a public site whose search ranking matters → set site.

base stays independent

Set base whenever the site lives under a sub-path, with or without a domain — a sub-path always affects the rendered links, so it can’t be left to the server.

Deploy via GitHub Pages

Deploy via GitHub Pages

Deploy anywhere static. For GitHub Pages: Settings → Pages → Source = GitHub Actions, then:

# .github/workflows/docs.yml
name: docs
on:
  push:
    branches: [main]
permissions:
  contents: read
  pages: write
  id-token: write
concurrency:
  group: pages
  cancel-in-progress: false
jobs:
  publish:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - run: npx notabene build --public
          --site https://${{ github.repository_owner }}.github.io
          --base /${{ github.event.repository.name }}
          --out ./_site
      - uses: actions/upload-pages-artifact@v3
        with: { path: ./_site }
      - id: deployment
        uses: actions/deploy-pages@v4

Notes:

  • With publish: { site, base } set in your config, the --site/--base flags can be dropped entirely.
  • Hosting at a custom domain or a user/org root site? Drop --base and set --site to your domain.
  • The artifact ships a .nojekyll, so _astro/ assets survive even classic gh-pages-branch hosting.
  • npm i -D pagefind in your repo and this exact workflow ships full-text search too — npm ci installs it, the build picks it up. Nothing to change here.
  • This documentation site is deployed by exactly this workflow — see it in the repo.