ScribekitDocs

/scribekit-docs-github-pages

Reference for the /scribekit-docs-github-pages skill: its two modes, its eight steps, the deploy workflow it writes, and the static-export trade-offs it will not paper over.

11 min readUpdated 11 August 2026
/scribekit-docs-github-pages

/scribekit-docs-github-pages publishes a Scribekit docs corpus to GitHub Pages as a static export - whether the project already has a Next.js app rendering it, or is just a bare docs/<slug>/*.mdx folder with nothing to render it. It learns the project first, scaffolds the host app if there is none, then patches the Next.js config, writes the deploy workflow, sets siteUrl to the Pages origin, and hands you back a push away from live.

Not using Claude Code?

There is an escape hatch that needs no agent at all: the deploy workflow is a plain file, copyable straight from node_modules/@daanvandenbergh/scribekit/skills/scribekit-docs-github-pages/assets/deploy.yml to .github/workflows/deploy.yml. It is not quite standalone, though - its own header names the prerequisite: next.config.* must read NEXT_PUBLIC_BASE_PATH, or a project site never receives its base path. Publish your docs to GitHub Pages walks the whole thing by hand.

Invoke the skill

/scribekit-docs-github-pages [--verify]

--verify additionally runs the local export build and the browser check in Step 7; without it, Step 7 is a config sanity check only.

Modes

There is no mode argument. Step 0 picks one of two paths from what it finds:

ModeChosen whenWhat it adds
DeployA new Docs(...) instance and route files already render the corpusNothing extra. Straight to the eight steps.
CreateA docs/<slug>/*.mdx corpus exists but no Docs instance and no routesScaffolds the host app from assets/app-template/ first, then runs the eight steps on it.

If there is neither a Docs instance nor a corpus, the skill stops and points you at /scribekit-docs. It never writes docs content itself.

What Create mode scaffolds

Copied from assets/app-template/ and filled in from the corpus it just read: _docs.ts (with contentDir, brandName, and the tabs / groups arrays derived from every distinct front-matter tab and group, in order), _docs-links.tsx, _docs-image.tsx, _docs-chrome.tsx, layout.tsx, page.tsx, and slug-page.tsx as app/docs/[slug]/page.tsx. It then adds the two files the template does not carry: a root app/layout.tsx importing both @daanvandenbergh/scribekit/styles.css and the template's globals.css, and a package.json whose devDependencies include typescript, @types/node, @types/react, and @types/react-dom - without them CI's npx --no-install next build aborts outright. A multi-locale corpus gets the [lang] tree with prefixDefaultLocale: true instead, since a static host has no middleware for clean default-locale URLs.

What it is not

This is not a content skill. Writing, rewriting, or reorganizing docs pages is /scribekit-docs. It never edits MDX page bodies, front-matter, slugs, or the nav in either mode. In Deploy mode it edits deploy config exclusively: next.config.*, the _docs.ts siteUrl and routing flags, .github/, and public/. Create mode additionally creates the host app - route files, layout, chrome, _docs.ts, next.config.*, package.json - but still stops at the content boundary.

For the guided walkthrough, see Publish to GitHub Pages.

The eight steps

StepWhat it does
Step 0Learn the project: find the Docs wiring (contentDir, siteUrl, basePath, locales, defaultLocale, prefixDefaultLocale, redirects), find the Next app root, derive the Pages origin and base path from git remote get-url origin, and detect the static-export blockers present. Picks Deploy or Create here; stops only if there is no corpus at all.
Step 1Patch next.config.*: merge in output: "export", trailingSlash: true, images: { unoptimized: true }, and a basePath / assetPrefix pair derived from process.env.NEXT_PUBLIC_BASE_PATH.
Step 2Create an empty public/.nojekyll so the exported _next/ folder is never treated as Jekyll source.
Step 3Copy assets/deploy.yml to .github/workflows/deploy.yml at the git repo root, not the app subdir. Workflows only run from the repo root's .github/.
Step 4Set siteUrl in _docs.ts to the derived Pages origin, so canonical, sitemap, hreflang, and OG URLs point at the real site. The domain is confirmed with the user first.
Step 5Fix the blockers found in Step 0, and only those present.
Step 6Enable Pages, then push. The workflow self-enables; report the manual fallback only if an org policy blocks it.
Step 7Verify. Config sanity always; the full build and browser check with --verify.

Step 0 derives the origin three ways: a repo named exactly <owner>.github.io is a user/org site served at the root with an empty base path; a public/CNAME file means a custom domain, also root-served with an empty base path; anything else is a project site under https://<owner>.github.io/<repo>/ with a base path of /<repo>.

Why it is mostly config, not code

A Scribekit docs site is already static-export-ready:

  • The Docs class reads the filesystem at build time.
  • Pages render as React Server Components with next-mdx-remote/rsc.
  • Search is client-side Fuse.js.
  • Every dynamic route already ships generateStaticParams and export const dynamicParams = false.

So next build with output: "export" prerenders the whole site to static HTML. The only things affected are two routing-layer conveniences a project may layer on top, handled in Step 5.

Note that output: "export" exports the whole Next app. A Blog section on the same app is exported too and is subject to the same rules.

What the workflow contains

assets/deploy.yml triggers on push to branches: [main] plus workflow_dispatch, takes least-privilege permissions (contents: read, pages: write, id-token: write), and runs one deploy at a time with concurrency: { group: pages, cancel-in-progress: false }.

The build job on ubuntu-latest:

StepAction or command
Checkoutactions/checkout@v7
Set up Nodeactions/setup-node@v7 with node-version: 22 and cache: npm
Configure Pagesactions/configure-pages@v6 with enablement: true - and deliberately no static_site_generator
Install dependenciesnpm ci
Build static exportnpx --no-install next build, with env: NEXT_PUBLIC_BASE_PATH: ${{ steps.pages.outputs.base_path }}
Upload artifactactions/upload-pages-artifact@v5 with path: ./out

The deploy job needs build, targets the github-pages environment, and runs actions/deploy-pages@v5.

static_site_generator: next is left out on purpose. That mode lets the action inject basePath itself, which would leave the app unable to read the same value - and _docs-image.tsx's hero <img> needs it. Instead configure-pages@v6 only emits base_path, the build step passes it in as NEXT_PUBLIC_BASE_PATH, and the app's own next.config.* and hero component both read it. enablement: true turns Pages on for the repository on the first run.

The template carries a commented-out defaults.run.working-directory block for an app in a subdirectory; uncommenting it also means changing the upload path: to <subdir>/out and adding cache-dependency-path: <subdir>/package-lock.json. Both npm ci and setup-node's cache: npm need a committed package-lock.json. A TypeScript app must carry typescript, @types/node, @types/react, and @types/react-dom in its own package.json - a repo-root one does not count for an app in a subdirectory, and npx --no-install cannot fetch them. If the repo's default branch is not main, the trigger changes. If .github/workflows/deploy.yml already exists, the skill shows the diff and asks before overwriting.

The static-export trade-offs

Being honest about these is one of the skill's non-negotiables.

Middleware clean-URLs are a hard blocker

A proxy.ts or middleware.ts that rewrites /docs/... onto a /<defaultLocale>/docs/... route tree cannot run on a static host. Static export drops middleware entirely, so only the prefixed paths (/en/docs/..., /fr/docs/...) get generated and the bare /docs/... clean URLs 404.

The clean fix is prefixDefaultLocale: true on the Docs (and Blog) instance, so every locale including the default is served under its own prefix with no middleware. That means deleting the proxy.ts or middleware.ts, removing any /en-stripping the client chrome did on usePathname() (it assumed the rewrite), and optionally adding a static entry redirect from /docs to /<defaultLocale>/docs. URLs become /<lang>/docs/... for every language: the honest trade for static hosting.

A single-locale site usually has no middleware and nothing to do here. The flat /docs/... export just works.

Runtime slug redirects degrade, but do not hard-break

A redirects config served via permanentRedirect() in the [slug] route still builds under output: "export". Next turns the 308 into a client-side redirect: the old slug exports as a near-empty 200 HTML page carrying a NEXT_REDIRECT marker in its payload, and once its JS hydrates the router navigates to the new URL and applies the deployment base path. Renamed slugs do redirect for real visitors.

The loss is SEO and no-JS. To a crawler or a JS-less client, the old URL is a blank 200 with no 308, no <meta refresh>, and no canonical: a weak signal. There is no server on Pages, so a true 308 is not available either way. Two options:

OptionTrade
Leave itZero work. JS visitors get redirected. Correct when the old URLs are not SEO-critical.
Static stubsDrop ...getRedirectRefs() from the route's generateStaticParams and the permanentRedirect branch, then write a public/<oldpath>.html stub with <meta http-equiv="refresh" content="0; url=<newUrl>"> and <link rel="canonical" href="<newUrl>">.

You cannot keep both: a public/ file and a prerendered route at the same path collide. getRedirect(slug, lang) returns <newUrl> rooted at the docs section base path (/docs/...); on a project site the stub must also carry the deployment base path (/<repo>) or it lands on a 404.

The project-site base path takes three pieces of plumbing

On a project site at https://<owner>.github.io/<repo>/, everything under /<repo> has to be prefixed, and three different things need three different mechanisms. The skill wires all three in Step 5, and a project site then renders correctly with no custom domain required:

WhatHow it gets the base path
Routes, next/link navigation, _next/ assetsNext's own basePath / assetPrefix, set in next.config.* from NEXT_PUBLIC_BASE_PATH (Step 1)
In-body prose links in MDXcomponents={{ a: BodyLink }} - raw <a href="/..."> would otherwise bypass next/link entirely
The hero image from image: front-matterimgComponent={BaseImg}, which prepends NEXT_PUBLIC_BASE_PATH itself - next/image will not, once images.unoptimized is set

In Deploy mode the skill adds imgComponent={BaseImg} and components={{ a: BodyLink }} to the existing [slug] route, alongside the Step 1 config change. In Create mode the template already ships both.

Absolute SEO URLs need nothing extra: siteUrl carries the /<repo> segment and Scribekit prepends it, so canonical, sitemap, hreflang, and OG all come out correct under a subpath.

Root hosting is still one less moving part if you have the choice - a custom domain via public/CNAME, or a <owner>.github.io user/org repo - and there the base path is simply empty.

Enabling Pages

The workflow's configure-pages step sets enablement: true, so on its first run it turns Pages on itself (build type "GitHub Actions"). In the common case there is no manual step: commit, push to the default branch (or run the workflow from the Actions tab), and the site publishes at the Pages URL derived in Step 0.

The exception is an org policy that blocks Actions from enabling Pages. That first run fails with "Get Pages site failed / Not Found", and Step 6 tells you the one-time fix:

GitHub -> repo Settings -> Pages -> Build and deployment -> Source: "GitHub Actions".

Re-run the workflow after that and it deploys. With Pages already enabled, enablement: true is a no-op.

Anti-rules

  • Do not hardcode a literal base path in next.config.*. Derive basePath / assetPrefix from process.env.NEXT_PUBLIC_BASE_PATH, which the workflow feeds from configure-pages's base_path output. A literal basePath: "/my-repo" breaks local builds and every other host.
  • Do not swap in a workflow that relies on static_site_generator: next. The app has to own the base path, or the hero <img> cannot read it.
  • Do not set trailingSlash on one side only. Step 1 writes trailingSlash: true, matching Scribekit's own default: the export then emits docs/<slug>/index.html, so Pages serves /docs/<slug>/ and redirects /docs/<slug> to it. A project that already runs trailingSlash: false keeps it and sets trailingSlash: false on its Docs/Blog instance instead. Flipping one without the other makes every canonical, hreflang, sitemap entry, and nav link disagree with the URL the host serves - silently, since nothing throws.

Constraints

  • Preserve the user's config. Merge into next.config.* and _docs.ts; never rewrite the file from scratch or drop existing keys. Every edit is idempotent, so re-running the skill does not duplicate keys, workflows, or stub pages.
  • Do not touch docs content. No editing MDX bodies, front-matter, slugs, or the nav - in either mode. Create mode writes the app around the corpus, never the corpus.
  • Be honest about static-export limits. Middleware clean-URLs and true 308 redirects are the real trade-offs; they are losses a setting cannot buy back.
  • Confirm before destructive or outward-facing edits. Overwriting an existing workflow, deleting a proxy.ts or middleware.ts, and changing the public URL scheme via prefixDefaultLocale all change how the live site behaves.
  • Never start a dev server. Never create git branches.
Was this page helpful?