GitHub Pages Documentation Layout
Context
- Repo root: !
git rev-parse --show-toplevel 2>/dev/null || pwd
Working directory
All README.md, docs/, docs/index.md, docs/pages/, docs/_config.yml, docs/screenshots/ paths in this skill are relative to Repo root from Context. The cwd may be a subdirectory — prefix every Read/Edit/Write/Grep/Glob with the Repo root value. Bare paths are cwd-relative and will silently miss the actual root.
Use this skill when organizing project-level documentation into a README plus a GitHub Pages Jekyll site running the just-the-docs theme. The goal is a consistent shape across repos: the README sells the project in under a screen, every user-facing capability gets its own sidebar entry, and developers get a single Developer guide that may itself have technical child pages.
Reference implementations:
- jsonl-logs-intellij-plugin — modern just-the-docs reference. Single product with deep splitting (Usage parent + 6 children, Development parent + 2 children).
- chrome-assistant — monorepo variant (one product per subfolder under
docs/pages/). - bga-assistant — just-the-docs flat variant; game pages grouped under a
Gamesparent, a game-agnostic feature page (Play-time tracking) at top level, and a Development page with the data-flow reference nested under it via front matter.
Read the target repo's README.md, docs/index.md, docs/_config.yml, and one or two child pages before writing new docs — copy their tone and shape.
Core principles
- Single source of truth lives on GitHub Pages. The README is an entry point, not a manual. If something takes more than a paragraph, it belongs on a page and the README links to it.
- User perspective by default. Every page outside the Developer guide is written for someone using the thing, not building it. No build commands, no internal module names, no architecture diagrams — those live under
development. - One Developer guide. The Developer guide may have child pages (Architecture, Extension points, etc.) — split when the parent page exceeds ~200 lines or covers distinct reference topics.
- Theme-driven navigation. The sidebar is generated by
just-the-docsreading front matter (nav_order,parent,has_children). Do not write manual nav strips on pages — the theme renders them. - Screenshots live in
docs/screenshots/and are referenced from every page that needs them. Keeping that set honest afterwards — dangling references, orphaned files, and shots of UI the code has since changed — belongs to~/.claude/skills/documentation/SKILL.md, which re-checks it on every change set.
File layout
Flat variant (single product, several user-facing capabilities):
README.md
docs/
_config.yml
_includes/
nav_footer_custom.html # an HTML comment — drops the theme's footer attribution
index.md # nav_order: 1, the homepage
pages/
usage.md # nav_order: 2, has_children: true (parent stub)
usage/
<feature-a>.md # parent: Usage, nav_order: 1
<feature-b>.md # parent: Usage, nav_order: 2
...
development.md # nav_order: 3, has_children: true
development/
architecture.md # parent: Development, nav_order: 1
extending.md # parent: Development, nav_order: 2
privacy.md # if the product handles user data
screenshots/
*.png
Monorepo variant (several products sharing one docs site):
README.md
docs/
_config.yml
index.md
pages/
<product-a>.md # nav_order: 2, has_children: true
<product-a>/
<feature>.md # parent: <Product A>, nav_order: 1
<product-b>.md # nav_order: 3, has_children: true
<product-b>/
<feature>.md
development.md # nav_order: 99 (last), has_children: true
development/
<product-a>-architecture.md
<product-b>-architecture.md
screenshots/
*.png
Splitting heuristic: a single page becomes a parent + children once it grows past ~200 lines OR has 5+ distinct H2 sections that read as separate references rather than a continuous narrative.
docs/_config.yml
Minimal Jekyll config using just-the-docs as a remote theme:
title: <Project Name>
description: <same tagline used in README / index.md>
remote_theme: just-the-docs/just-the-docs@<latest-release-tag>
baseurl: /<repo-name>
plugins:
- jekyll-remote-theme
color_scheme: light
search_enabled: true
heading_anchors: true
exclude:
- plans/ # internal planning docs, if any
Pin the remote theme
remote_theme without a @ref resolves to the theme repo's default branch HEAD on every rebuild — the build log says so plainly (Downloading .../just-the-docs/zip/HEAD). An upstream commit then changes the site with nothing changing in the repo, and nothing triggers it until the next unrelated docs edit, which makes the eventual breakage look like it came from whatever you touched that day. owner/name@ref is documented jekyll-remote-theme syntax and takes a tag.
Resolve the current tag rather than hardcoding one — gh api repos/just-the-docs/just-the-docs/releases/latest --jq .tag_name — and treat it like any other dependency: bump deliberately, not on every run. Note the theme's releases lag its default branch by months, so pinning moves the site backwards from HEAD; check that anything the site relies on from recent theme source (an include's contents, a variable's default) exists at the tag before pinning.
Content width
Leave $content-width at the theme default unless you have read ~/.claude/learnings/just-the-docs-customization.md and accepted what it costs. It looks like a width knob and is not one: the default is 50rem (800px), and md and lg in the theme's $media-queries are derived from it — md being the window width at which .side-bar stops rendering as a mobile header. The frequently-copied $content-width: 87.5rem therefore pushes the sidebar to 1400px, so a 1366px laptop gets the mobile layout.
Nor can the two be separated. At lg, .side-bar and .side-bar + .main's margin-left are both computed from $nav-width + $content-width, so forcing a wider .main in _sass/custom/custom.scss overflows the right edge on mid-size windows. If you do override it, override the variable in _sass/custom/setup.scss, in rem — GitHub Pages uses sass 3.x, which rejects mixed px/rem arithmetic.
A wide table is usually better served by letting the theme's .table-wrapper scroll. And do NOT re-derive the width from .main-content { max-width: ... } — width belongs to $content-width. (The class itself does exist, contrary to what this line used to say; see ~/.claude/learnings/just-the-docs-customization.md.)
Footer attribution
just-the-docs renders "This site uses Just the Docs, a documentation theme for Jekyll." Remove it by default by adding docs/_includes/nav_footer_custom.html containing an HTML comment and nothing else — Jekyll resolves local _includes/ before the remote theme, same precedence as the _sass/ overrides.
Three things about that sentence are counter-intuitive enough to get wrong; ~/.claude/learnings/just-the-docs-customization.md has the verified detail:
- The include is
nav_footer_custom.html. The similarly-namedfooter_custom.htmlis an unrelatedsite.footer_contenthook — shadowing it suppresses a feature and leaves the attribution in place. - It must be non-empty. The theme guards on
!= ""and ships a 0-byte copy, so an empty override prints the attribution rather than removing it. - It must contain no
{%or{{, even inside the comment. An include is parsed as Liquid first, so quoting the theme's own guard there fails the Pages build with an unclosed-iferror.
Verify with comments stripped, and only after confirming the pages build and deployment run succeeded — a failed build keeps serving the previous deploy, which reads exactly like a change that did nothing.
Front matter cheat sheet
| Key | When to use |
|---|---|
layout: default |
Required on every page |
title: <Name> |
Sidebar label; required |
nav_order: N |
Sidebar order — local to the level (top-level or within a parent's children) |
parent: <Title> |
Marks a page as a child; must match the parent's title: exactly |
has_children: true |
On a parent page; expands the disclosure arrow in the sidebar |
nav_exclude: true |
Hide a page from the sidebar entirely (rare — prefer parent/child structure) |
docs/index.md shape
The homepage (nav_order: 1). Mirrors the README but lives inside the site.
---
layout: default
title: Home
nav_order: 1
---
*<One-line tagline — same as README, italicized.>*
<Context paragraph: what kind of thing this is (form factor, supported platforms) plus the one or two core capabilities a visitor sees first — not a features rundown; those stay in the list below.>
<a href="screenshots/<cover>.png"><img src="screenshots/<cover>.png" alt="<descriptive alt>" width="1000"></a>
## Example (optional — if a concrete before/after illustrates the value)
<Input → output illustration.>
## Features
- **<Feature 1>** — one-line user-visible description
- **<Feature 2>** — ...
## Install (omit when a dedicated Installation page leads the Next steps list below)
<One-line pointer to the Developer guide or distribution channel.>
## Next steps
- **[Usage](pages/usage)** — <one-line description of what the user finds there>
- **[Developer guide](pages/development)** — <one-line description>
index.md does NOT contain a usage walkthrough or screenshots beyond the cover — those live in the user-facing pages. Keep it focused: what is this, what does it do, where do I learn more.
docs/pages/.md — parent stub (when split into children)
When a feature is large enough to need children, the parent page is a brief landing:
---
layout: default
title: <Feature Name>
nav_order: 2
has_children: true
---
<One-paragraph intro for the whole topic.>
<a href="../screenshots/<hero>.png"><img src="../screenshots/<hero>.png" alt="..." width="1000"></a>
## <Onboarding section, e.g. Installation>
<Brief content — anything that's "before you start using the feature".>
The parent doesn't repeat content from its children. just-the-docs auto-renders a child list at the bottom of the parent, so users can navigate into details from the parent body or from the sidebar.
docs/pages//.md — child page
---
layout: default
title: <Sub-feature Name>
parent: <Feature Name>
nav_order: 1
---
<Direct content — no nav strip, no top-bar, the theme handles navigation.>

Image path conventions:
| Page location | Path to docs/screenshots/foo.png |
|---|---|
docs/index.md |
screenshots/foo.png |
docs/pages/<feature>.md |
../screenshots/foo.png |
docs/pages/<feature>/<sub>.md |
../../screenshots/foo.png |
Every screenshot needs a visible edge, and it belongs in the image, not the stylesheet. A capture's own background is often the same colour as the page it sits on — a light app on a light theme, a dark app on a dark theme — and then nothing shows the reader where the picture stops. The edge is produced in two steps, in this order, by whatever takes the shot (see ~/.claude/skills/documentation/SKILL.md): background out to transparent, then a hairline contrasting with the foreground's own background.
Do not reach for a blanket .main-content img { border: ... } rule. It looks tidier and is wrong three ways: a single colour cannot suit both colour schemes, since color_scheme in _config.yml is one line and a light hairline vanishes under a dark theme and vice versa; it draws a square box around any image with transparent rounded corners, leaving page background inside the frame; and it doubles up on any image that already carries its own edge. A per-image border has none of those problems, and is theme-independent by construction — whichever of border-vs-page or image-vs-page lacks contrast, the other one has it. It also reaches README.md, which a stylesheet never can: GitHub's markdown sanitizer strips style and github.com never loads the site's CSS.
A before/after pair also needs an arrangement, and the choice is aspect ratio, not preference:
Narrow and tall (a panel column, a sidebar) → a two-column table, so the height difference is the comparison and reads at a glance. Keep no
|inside a cell's HTML — it ends the cell.| Before — <what> | After — <what> | |:---|:---| | <a href="../../screenshots/<a>.png"><img src="../../screenshots/<a>.png" alt="..." width="320"></a> | <a href="../../screenshots/<b>.png"><img src="../../screenshots/<b>.png" alt="..." width="320"></a> |Wide and short (a header bar, a board) → stacked, with a bold
**Before**/**After**label above each. Side by side would halve them into a ~370px column and make the text illegible.
Lead in with the measurement rather than the position: "the same header either way — 205 pixels of it, and then 54" survives both arrangements, where "above and below" becomes wrong the moment a pair goes side by side.
Internal link conventions (no .md extension, no leading ./):
| Source → Target | Link form |
|---|---|
| Sibling at same level | [Other](other) |
| Parent's sibling (one level up) | [Other](../other) |
| Child of a sibling | [Other](../other-feature/sub) |
| Anchor within a sibling | [Section](other#anchor) |
After restructuring, watch for stale anchors — #section-name references break silently when the target H2 is renamed or moved.
docs/pages/development.md
The Developer guide. Same parent-stub pattern when it grows children.
---
layout: default
title: Development
nav_order: 3
has_children: true
---
## Setup
### Prerequisites
- <runtime versions>
### Install / Build from source
<commands in a code fence>
## Commands
<Table or bulleted list of common dev commands.>
## Architecture
<Short rationale — 2-4 paragraphs of design principles. The full deep-dive lives in the Architecture child page.>
See the [Architecture reference](development/architecture) for the full domain model, data flow, and component breakdown.
## Project structure
<Annotated source tree.>
## Testing
<How to run tests.>
## Extension points
For step-by-step recipes, see the [Extension points reference](development/extending).
Children of development.md:
development/architecture.md— long technical deep-dive (domain model, data flow, components, persistence, live-update path, etc.)development/extending.md— recipes for adding filters, colours, settings toggles, etc.- More as needed.
Rules for the developer page:
- The parent's
## Architecturesection is a brief rationale + link to the child reference. Do not duplicate content between parent and child. - Likewise for Extension points: parent has a one-line pointer; the child has the recipes.
- Each child is reachable both from the sidebar (via
parent: Development) and from a link insidedevelopment.md.
docs/pages/privacy.md (if applicable)
Required when the product reads, stores, or transmits user data. Follow the bga-assistant shape: what is / isn't collected, how the extension works, a permissions table, contact. Set nav_order to slot it into the sidebar at the appropriate position (typically last among top-level pages).
README.md shape
Keep it short enough to read on one screen. Structure, in order:
- H1 project title.
- One-line italicized tagline — must match
docs/index.md's tagline byte-for-byte (excluding the*…*italics). - Context paragraph — copy from
docs/index.md(form factor, platforms, the one or two headline capabilities). Must match word for word. - Cover screenshot:
— alt text matchesdocs/index.mdfor consistency. - Features bulleted list — copy from
docs/index.mdFeatures section. Bullets must match exactly (same set, same wording, same order). Capabilities already named in the context paragraph don't get a bullet — each feature lives in exactly one of the two. - Install — one-line pointer to wherever install instructions live (Developer guide or a dedicated Installation page). Omit entirely when the docs site has a dedicated Installation page sitting at the top of the footer list below — a pointer one line above its own footer link is duplication.
- License — keep whatever licence line the README already carried (
[MIT](LICENSE)), immediately before the footer. It is the one piece of prose that belongs on the repo page rather than the docs site, and trimming a README to this shape is exactly when it gets dropped by accident. - Footer block listing every page in the docs site:
---
See full project documentation at **[<org>.github.io/<repo>](https://<org>.github.io/<repo>/)**:
- [<Top-level page A>](https://<org>.github.io/<repo>/pages/<a>)
- [<Child A.1>](https://<org>.github.io/<repo>/pages/<a>/<sub>)
- ...
- [<Top-level page B>](https://<org>.github.io/<repo>/pages/<b>)
- [<Child B.1>](https://<org>.github.io/<repo>/pages/<b>/<sub>)
Indentation must match the actual parent/child hierarchy. Every existing page must appear; no listed page may be missing on disk.
The README never contains setup, build, architecture, or API content. If you catch yourself writing any of that in the README, move it to development.md and leave a link.
In-app About dialog (if present)
Some projects (desktop apps, browser extensions, IDE plugins) ship an in-app About dialog. When one exists, treat it as a third surface that shares the tagline with README.md and docs/index.md, word for word. Beyond the tagline the surfaces diverge: README and index carry the Features bullet list (see below), while the About dialog carries its own short what-it-does prose — a couple of sentences, not required to mirror README/index paragraphs and never a bullet list.
The About dialog additionally carries two things the README/home page handle elsewhere, and which belong only in the dialog:
- Version and release date — the running build's version (and its release date if available).
- A link to the project documentation — pointing at the GitHub Pages home page.
So the rule is: shared tagline, dialog-own prose, dialog-only version/date + docs link. Do not copy screenshots, Features bullets, Install pointers, or the docs footer into the About dialog — those stay on the README/site.
Auditing a site that already exists
Every rule in this skill reaches a repo only on the day that repo is scaffolded. A site set up
earlier keeps whatever shape it had then, and nothing in the greenfield process below ever revisits
it, so each rule added here quietly applies to fewer repos than it appears to. Audited 2026-09-08
across six local sites: three still served the theme's attribution footer and four had an unpinned
remote_theme, all of them scaffolded before those two rules existed. The user found it by reading
the footer on a published page.
So when a task touches an existing docs site for any reason, audit it first rather than assuming it was built to the current rules:
for c in $(find "$PROJECTS_ROOT" -maxdepth 4 -path '*/docs/_config.yml' -not -path '*/node_modules/*'); do
d=$(dirname "$c"); f="$d/_includes/nav_footer_custom.html"
repo=$(cd "$d/.." && basename "$PWD")
pinned=$(grep -c '^remote_theme:.*@' "$c"; true)
if [ -f "$f" ]; then size=$(wc -c < "$f"); liquid=$(grep -c '{%\|{{' "$f"; true); else size=ABSENT; liquid=-; fi
if [ -f "$d/_includes/footer_custom.html" ]; then decoy=yes; else decoy=no; fi
printf '%-28s pinned=%s nav_footer=%s liquid=%s decoy=%s\n' "$repo" "$pinned" "$size" "$liquid" "$decoy"
done
Note the ; true after each grep -c: grep exits 1 when it counts zero matches, so in an
A && B || C chain the || branch fires on a legitimate result of 0 and prints a second value.
Plain assignments avoid it.
Read it as: pinned must be 1, nav_footer must be a byte count and not ABSENT, liquid must be 0,
and decoy should be no. A footer_custom.html is the specific trap worth naming, because it looks
like the fix and is not: it shadows the unrelated site.footer_content hook while the attribution
carries on rendering, so its presence is evidence the footer was attempted and missed.
Fix each in its own repo's own commit. Do not fold another project's docs fix into the commit you came to make.
Verify against the published page, never the source. An unpinned remote_theme rebuilds from the
theme's default branch, which runs ahead of the newest release, so the version serving the site is
not knowable from the repo and correct-looking sources can sit above a deploy that still shows the
old markup. Do not try to identify the served version by matching its markup against release tags
either — on 2026-09-08 that produced a confident wrong answer, because the search covered a range of
tags that stopped short of the ones introducing the class names in question:
curl -sL https://<org>.github.io/<repo>/ | grep -c "documentation theme for Jekyll"
Writing process
Follow these steps whenever creating or restructuring docs.
Inventory. List the user-facing capabilities. Decide which top-level pages exist (Home, Usage, Development, optional Privacy) and what children each has. A long page splits into one child per major section once it exceeds ~200 lines / 5+ H2 headings.
Set up
_config.yml. Use the just-the-docs template above.baseurlmust match the GitHub repo name. Also createdocs/_includes/nav_footer_custom.htmlholding an HTML comment to drop the theme's footer attribution (see Footer attribution) — non-empty and Liquid-free, for the reasons given there.Write
docs/index.md. Tagline + hero screenshot + (optional Example) + Features bullets + Install pointer + Next steps. Do not include a usage walkthrough — push that to the Usage page tree.Write child pages. One sub-capability per file under
docs/pages/<feature>/. Each getsparent: <Feature>+ a localnav_order. No nav strips.Write the parent stub for each feature. Brief intro +
has_children: true. The parent doesn't repeat children's content.Write
development.mdthe same way: parent for the dev tree, with children for Architecture, Extension points, etc.Write
README.mdlast. Lift the tagline and Features bullets fromindex.md. Footer lists every existing page hierarchically. Verify each footer URL resolves.Verify cross-consistency.
- Tagline matches across
README.md,docs/index.md,_config.yml description, and the in-app About dialog (if present). - Context paragraph matches between
README.mdanddocs/index.md, word for word, and no capability appears in both the paragraph and a Features bullet. - Features bullets match between
README.mdanddocs/index.md(set, wording, order). When a Features page exists, the list covers the same topics (it may skip lesser, plumbing-level sections like configuration mechanics or naming details) but need not share its order: the summary list is a highlight reel and may rank bullets by impact, while the reference page keeps a logical build-up order — that divergence is deliberate, not drift. - Hero-screenshot alt text matches across
README.mdanddocs/index.md. - Footer in
README.mdlists everydocs/pages/**.md(excluding nav-excluded pages); no listed page is missing on disk. - Internal links: bare names for siblings,
..for cross-level — and search for stale#anchor-namereferences after any section rename.
- Tagline matches across
Verify git is actually tracking the new files. A repo whose
.gitignoreignoresdocs/*and re-includes specific paths (a common way to keep local scratch docs out) swallows the entire site silently: an ignored path never appears ingit status, so nothing in the normal flow mentions it, and the site simply is not in the commit. Ask git rather than reading the ignore file:git check-ignore -v docs/_config.yml docs/index.md docs/_includes/ docs/pages/Exit 1 with no output means everything is tracked. Any
<file>:<line>:<pattern>citation names the rule to add a!line beside — repeat until it exits 1. Do this for every new top-level entry underdocs/; re-including a directory covers what is inside it.Publish, and point the repo at it. The README's footer now links to a site that 404s until GitHub Pages is switched on, so this is part of the job, not a follow-up. Confirm with the user first — it publishes the docs — and do it after the branch carrying
docs/is pushed, or the first build runs against a tree with no site in it.gh api repos/<owner>/<repo>/pages -f source[branch]=main -f source[path]=/docs gh repo edit --description "<the tagline>" --homepage "https://<owner>.github.io/<repo>/"The repo's own description and homepage are a fourth surface for the tagline: leaving the old description in place is the same drift the cross-consistency check exists to catch.
gh api ... /pagesreturning 404 on a GET is how you tell Pages is not enabled yet.
Out of scope
- Do NOT write manual nav strips (
[Home] | [Usage] | …) on pages —just-the-docsrenders the sidebar from front matter. - Do NOT add more than one Developer-guide entry at the top level — technical deep-dives become children of
development.md, not separate top-level pages. - Do NOT mix this layout with another docs framework. If a project already uses Docusaurus, MkDocs, VitePress, Astro Starlight, etc., do not migrate — defer.
- Do NOT create
CONTRIBUTING.md,CHANGELOG.md, or other root-level docs unless asked — they are not part of this layout. - Do NOT inline screenshots, blurbs, or feature walkthroughs into the README beyond what's prescribed (tagline, hero, Features, Install, footer). Anything more belongs on the docs site.