Skip to content

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

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

---
title: Projects
has_children: true
nav_order: 3
layout: page
---

Example: Child Page

---
title: Create a Project
parent: Projects
has_children: false
nav_order: 2
layout: page
---

Example: Grandchild Page

---
title: Members & Groups
parent: Membership
has_children: false
nav_order: 1
layout: page
---

Note: The parent value must exactly match the title of the parent page.

Adding Content

Creating a New Page

  1. Choose the right directory based on the content topic
  2. Create the markdown file with descriptive name
  3. Add front matter with correct parent/nav_order
  4. Write content using GitHub Flavored Markdown
  5. 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>).

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 from the guide still serves (a redirect never shadows a page);
  • a to that 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

cd user-guide
docker-compose up

Visit http://localhost:4000 to see the site.

Using Ruby/Bundler

cd user-guide
bundle install
bundle exec jekyll serve

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
  • Maintain sequential nav_order within 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:

  1. Trigger: Push to main branch with changes in user-guide/
  2. Build: Jekyll builds the static site
  3. Check guide links (spec 6H H1): before the push, the image runs as two local containers and .github/scripts/check-user-guide-links.sh checks that every history URL answers 200 or one relative 301 to a 200, that every in-app help link (web templates, whats-new.json, help topics, runtime What's New and walkthrough bodies) answers 200 with its anchor, that the two containers give the same pages (self-parity), and that user-guide/test-seo-headers.sh passes. 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)
  4. Package: Docker image created and pushed to GHCR
  5. Deploy: ArgoCD syncs to Kubernetes cluster
  6. 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.yml exclude:);
  • src/guide/liquid.ts: {{ site.baseurl }} and {% link page.md %} only; any other Liquid fails the build;
  • src/guide/remark-kramdown.ts and typography.ts: kramdown's heading IDs (computed with the content compiler's own rules in guide-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, and 1) 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" from parent, grand_parent, nav_order and has_children, by the Jekyll theme's rules (a page whose parent names 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

  • Verify parent value exactly matches parent's title
  • Check nav_order is 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