How to: Work with the User Guide¶
This guide explains how to develop and maintain the SyRF User Guide, which provides end-user documentation for the SyRF platform.
Overview¶
The User Guide is a Jekyll-based documentation site using the just-the-docs theme. It is:
- Published at: help.syrf.org.uk
- Location:
/user-guide/directory in the monorepo - Theme: just-the-docs (remote theme)
- Build: Jekyll static site generator
Prerequisites¶
- Docker and docker-compose (for local development)
- Basic knowledge of Markdown
- Understanding of Jekyll front matter
Directory Structure¶
The user guide is organized into logical sections:
user-guide/
├── index.md # Home (Getting Started)
├── about.md # About CAMARADES & SyRF
├── getting-started/ # Introductory content
├── account/ # Account management
├── projects/ # Project management
│ └── membership/ # Nested membership section
├── studies/ # Study/reference management
├── stages/ # Screening and annotation
├── data-export/ # Data export guides
│ └── data-dictionary/ # Field definitions
├── whats-new/ # Release notes
└── figs/ # Images and screenshots
Navigation System¶
The just-the-docs theme uses YAML front matter to build the navigation sidebar.
Front Matter Fields¶
| Field | Required | Description |
|---|---|---|
title |
Yes | Page title in navigation |
nav_order |
Yes | Position in sidebar (lower = higher) |
parent |
For children | Title of parent page (exact match required) |
has_children |
For parents | Set to true if page has children |
layout |
Yes | Usually page, use home for landing pages |
permalink |
Optional | Custom URL path |
syrf |
Optional | The feature topics the page covers (syrf.topics). Jekyll ignores it; the content compiler, the in-app help links and the registry validator read it. See Guide topics |
Example: Parent Page¶
Example: Child Page¶
Example: Grandchild Page¶
Note: The parent value must exactly match the title of the parent page.
Adding Content¶
Creating a New Page¶
- Choose the right directory based on the content topic
- Create the markdown file with descriptive name
- Add front matter with correct parent/nav_order
- Write content using GitHub Flavored Markdown
- Add images to
figs/directory
Image References¶
Images are stored in figs/ and referenced with relative paths.
The pattern depends on the file's depth relative to the figs/ directory:
| File Location | Image Path |
|---|---|
Root level (index.md, about.md) |
figs/image.png |
First-level subdirectory (projects/join.md) |
../figs/image.png |
Nested subdirectory (projects/membership/inviting.md) |
../../figs/image.png |
Use standard Markdown image syntax: ![alt text] followed by (<relative-path>).
Internal Links¶
Use Jekyll's link tag for reliable cross-page linking.
The syntax is: [Link Text] followed by ({{ site.baseurl }}{% link path/to/page.md %}).
Or use relative HTML links: [Link Text] followed by (../path/to/page.html).
Moving, renaming or retiring a page¶
Guide URLs never break (spec 6H H1). user-guide/url-history.json lists every URL the guide has served since the
tree before the 2025-12-05 reorganisation: pages at their permalink or path, content files (figures, CSV files,
LICENSE-CC-BY) and the files the build generates. It is generated and append-only. A URL the guide stops serving
must be the from of an entry in user-guide/redirects.yaml, in the same change:
- from: /stage-settings.html # the old URL, as the history lists it
to: /stages/settings.html # the page that replaces it now (a served URL in the history)
reason: moved # moved | retired | alias
Then run pnpm generate (from the repository root). It appends any new URLs to the history and regenerates
user-guide/nginx/redirects.conf, the exact-match nginx 301s the image serves, with a relative Location. A page
also answers without .html, and a directory page as x/ and x, so one entry covers every form.
pnpm run validate:generated (pre-commit and CI) refuses, with the exact problem:
- a history URL that is no longer served and has no redirect (the message names it and the entry to add);
- a
fromthe guide still serves (a redirect never shadows a page); - a
tothat is absolute or external, has a query or fragment, is not in the history, is not served, or is itself redirected (no chains or loops; repoint the older entry at the final page); - an edited or shortened history (it is compared with the git base).
retired means there is no direct replacement and to is the nearest page; a redirect never claims the old page was
public. Build tooling that Jekyll happens to copy (README.md, GitVersion.yml, the shell scripts) is not part of
the URL contract.
Local Development¶
Using Docker (Recommended)¶
Visit http://localhost:4000 to see the site.
Using Ruby/Bundler¶
Current Navigation Structure¶
| nav_order | Section | Description |
|---|---|---|
| 1 | Getting Started | Home page and intro content |
| 2 | Account | Account creation and management |
| 3 | Projects | Project management (with Membership subsection) |
| 4 | Study Management | Uploading and managing references |
| 5 | Stages | Screening and annotation configuration |
| 6 | Data Export | Exporting project data |
| 7 | About Us | About CAMARADES and SyRF |
| 8 | What's New | Release notes and updates |
Best Practices¶
Content Guidelines¶
- Write for end users, not developers
- Use clear, simple language
- Include screenshots for complex UI interactions
- Link to related pages where helpful
- Keep pages focused on a single topic
Navigation Guidelines¶
- Maintain sequential
nav_orderwithin each section - Use descriptive but concise page titles
- Group related content under parent pages
- Don't nest more than 2 levels deep (parent → child → grandchild)
Image Guidelines¶
- Use PNG format for screenshots
- Include descriptive alt text for accessibility
- Crop images to show relevant UI elements
- Use consistent naming:
Fig_Section_Description.png
CI/CD Pipeline¶
The user guide is built and deployed automatically:
- Trigger: Push to
mainbranch with changes inuser-guide/ - Build: Jekyll builds the static site
- Check guide links (spec 6H H1): before the push, the image runs as two local containers and
.github/scripts/check-user-guide-links.shchecks that every history URL answers200or one relative301to a200, that every in-app help link (web templates,whats-new.json, help topics, runtime What's New and walkthrough bodies) answers200with its anchor, that the two containers give the same pages (self-parity), and thatuser-guide/test-seo-headers.shpasses. The reports are a workflow artefact and the job summary; broken links inside pages are listed as warnings. The same step then runs the accessibility checks (spec 6H H2, below) - Package: Docker image created and pushed to GHCR
- Deploy: ArgoCD syncs to Kubernetes cluster
- Access: Available at help.syrf.org.uk
The same gate also runs on pull requests that change the guide (#4596):
the Check user guide job in .github/workflows/pr-tests.yml stages the publication manifest, builds the image
locally (never pushed, no preview needed) and runs .github/scripts/check-user-guide-links.sh on it. It runs when a PR
changes user-guide/, e2e/user-guide/, e2e/playwright.user-guide.config.ts, e2e/package.json, pnpm-lock.yaml,
the guide gate scripts, the URL and parity tools, the help content compiler or its inputs (feature-definitions.yaml,
docs/content/protected/, docs/content/whats-new.yaml), the in-app link sources docs/content/walkthroughs.yaml,
whats-new.json and help-topics.generated.ts, or .dockerignore. A failure fails the required Test Summary, so a
page with a new accessibility finding, a broken link or a stale baseline entry cannot merge. The reports are the
user-guide-pr-gate-<run>-<attempt> artefact. Links added in web templates do not select the job; main's guide build
still checks them. The Starlight proof runs on main only.
To run the same gate on a local image (root and e2e tooling installed, image built as in the user-guide README):
pnpm install --frozen-lockfile --ignore-scripts --filter syrf --filter @syrf/e2e
bash .github/scripts/check-user-guide-links.sh syrf-user-guide:local "$(mktemp -d)"
Accessibility checks (spec 6H H2)¶
.github/scripts/check-user-guide-a11y.sh, the last stage of "Check guide links", runs
e2e/playwright.user-guide.config.ts in the pinned Playwright container (mcr.microsoft.com/playwright, the version
e2e locks, pinned by digest), joined to a guide container's network. For every page in user-guide/url-history.json
that the image serves (URLs that answer a redirect are skipped; H1 checks them), at 1280 and 390 px:
- axe-core (
@axe-core/playwright, exact version) with the WCAG 2.0, 2.1 and 2.2 A and AA tags; - keyboard: the first Tab stop is a skip link (
syrf-skip-link), every Tab stop shows a focus indicator and sits in the viewport (syrf-focus-visible, first 80 stops), focus never gets stuck (syrf-keyboard-trap), and on the home page search opens, shows results and closes by keyboard with focus back on the control (syrf-keyboard-search).
Light and dark both run when the guide offers a dark scheme (prefers-color-scheme: dark changes the home page). The
Jekyll theme (just-the-docs 0.3.3) loads only its light stylesheet, so the dark contexts record "skipped" today and
start checking automatically once a renderer offers dark (H3). Requests to other origins (the YouTube embeds) get a
local stub, so results never depend on the network.
Each finding is keyed by page, rule and target. user-guide/a11y-baseline.json records the findings on the Jekyll guide
when H2 landed (1,365: mostly theme colour contrast and unnamed navigation expanders, no skip link). Since #4594 the
template fixes those itself: user-guide/_includes/head_custom.html adds the skip link and names the navigation
expanders and the menu button, and user-guide/_sass/ sets the link and footer contrast and the search focus ring. The
baseline is down to 57 page-specific entries (code highlighting, target size, task-list labels, iframes and the 404
page), so a new page normally adds no finding. A finding that is
not listed fails the build with the page, rule and target; a listed entry that a complete run no longer finds also fails
until you delete it, so the baseline only shrinks. Fix a new finding rather than adding it. After fixing a listed
problem, delete its line (or regenerate the file, then review the diff: it must only remove lines):
GUIDE_A11Y_UPDATE_BASELINE=1 bash .github/scripts/check-user-guide-a11y.sh <running-guide-container> "$(mktemp -d)"
Some finding targets are positional (for example .nav-list-item:nth-child(3) > …), so adding, removing or
reordering a navigation item in an ordinary content change can move those selectors. The build then reports a new
finding (the selector at its new position) and a stale entry (the old one) for the same problem, and fails. This is
not a new accessibility regression: build the guide container, run the refresh command above, and review the diff.
Only selectors of problems that already existed may change position; any genuinely new finding must be fixed, not
baselined. Commit the regenerated user-guide/a11y-baseline.json with your content change.
The controls project proves the checks on every run: an unlabelled button and 3:1 text fail axe and pass once
fixed; a page without a skip link or focus indicator, a keyboard trap and a search that Escape does not close each
fail. The report (a11y-report.md and .json, per-page results and the Playwright HTML report) is in the
user-guide-parity-* artefact under a11y/ and in the job summary.
If Playwright stops without writing a11y-report.json, the script prints why from docker inspect: the exit code,
whether the kernel's OOM killer ended it (oomKilled=true, at the container's 3 GiB limit), or that the container was
removed while it ran. Exit 137 with no OOM kill means something else ran docker rm -f on it. Only the user-guide
build's residue step removes the check's containers and image. DOCKER_BUILD_JOB_KEY includes the service name, so
sibling builds do not share it; the guard stays as defence in depth because sibling builds on one host share the Docker
daemon and the guide image tags are per run (run 40243556961 failed when the docs build's clean-up ran mid-scan, before
the key carried the service).
Starlight build of the guide (spec 6H H3 proof; nothing ships)¶
user-guide/site/ is a Starlight (Astro) build of the same pages, a pnpm workspace member (syrf-user-guide). It reads
the Markdown in user-guide/ in place (no copy) and writes every page at its Jekyll URL (a/b.md → a/b.html,
a/index.md → a/index.html, permalinks exact), so the URL history, the redirects and every in-app link work
unchanged. The published guide is still Jekyll: the image builds Starlight only with
--build-arg GUIDE_RENDERER=starlight, and CI never pushes that build. Switching is owner decision D53 (plan §7.4).
What the build adds to plain Markdown, so both renderers show the same text, links, images and heading IDs:
src/guide/site.ts: Jekyll's publishing rules (which files are pages,permalink:,_config.ymlexclude:);src/guide/liquid.ts:{{ site.baseurl }}and{% link page.md %}only; any other Liquid fails the build;src/guide/remark-kramdown.tsandtypography.ts: kramdown's heading IDs (computed with the content compiler's own rules inguide-topics.ts, so help topics and the publication manifest keep their anchors;{#id}honoured), attribute lists ({: .note },{:target="_blank"},{: .fs-9 }),{:toc}, smart quotes and dashes, and1)text; figures stay at their own URLs (never/_astro/); an unknown construct fails the build;src/guide/nav.ts: the sidebar and each parent page's "Table of contents" fromparent,grand_parent,nav_orderandhas_children, by the Jekyll theme's rules (a page whoseparentnames no page is listed nowhere, as today);src/styles/syrf.css: the web app's Material 3 colour and type tokens, light and dark; search is Pagefind.
Authors change nothing: write pages exactly as for Jekyll. To preview the Starlight build locally (Node only):
pnpm install --frozen-lockfile --filter syrf-user-guide
pnpm --filter syrf-user-guide run build # user-guide/site/dist/
pnpm --filter syrf-user-guide run dev # live preview
pnpm run validate:generated runs the site's unit tests (user-guide/site/test/, one per construct, plus every
page's heading IDs against the compiler). On every guide image build, CI also builds the Starlight image from the same
commit and runs .github/scripts/check-user-guide-starlight.sh before the push, report-only until the owner's go
(D53) and the cut-over (H4): a failure shows as a warning and a job-summary line and never stops the Jekyll image. It
checks the publication manifest, the same
link gate with zero tolerance for accessibility (no baseline, light and dark), the compiler's heading anchors on both
images, and a cross-renderer comparison. Differences the comparison may accept are listed with a reason in
user-guide/site/parity-exceptions.yaml (Jekyll theme files the Starlight build does not serve, and three pages whose
malformed or escaped Markdown kramdown and CommonMark read differently); an entry that stops matching fails, so the
list only holds real differences. The reports are under starlight-proof/ in the user-guide-parity-* artefact. To
run it on local images:
docker build -f user-guide/Dockerfile --build-arg GUIDE_RENDERER=starlight -t syrf-user-guide:starlight .
bash .github/scripts/check-user-guide-starlight.sh syrf-user-guide:local syrf-user-guide:starlight "$(mktemp -d)"
Troubleshooting¶
Navigation Not Showing¶
- Verify
parentvalue exactly matches parent'stitle - Check
nav_orderis set - Ensure front matter delimiters (
---) are correct
Images Not Loading¶
- Verify correct relative path based on file location
- Check file exists in
figs/directory - Ensure filename case matches exactly (Linux is case-sensitive)
Build Failures¶
- Check Jekyll syntax in
_config.yml - Validate YAML front matter format
- Review GitHub Actions logs for specific errors
Related Documentation¶
- User Guide README - Quick reference for editors
- CI/CD Workflow - Build pipeline details
- just-the-docs Documentation - Theme documentation