Writing docs
This site is an Astro Starlight project in front/docs-site
(workspace @ulams/docs). Most pages are written by hand; ADRs, the roadmap, the contributor files
and the reference pages are generated from the repository on every run, so the site never holds a
second copy of a document.
Run it locally
Section titled “Run it locally”corepack yarn installcorepack yarn dev:docs # http://localhost:4322dev:docs runs the dev script of @ulams/docs: first sync (regenerates the generated pages),
then astro dev on port 4322. The site does not need the API stack. Other scripts:
| Command | What it does |
|---|---|
corepack yarn workspace @ulams/docs sync |
Regenerates the generated pages only |
corepack yarn workspace @ulams/docs coverage |
Runs the coverage check |
corepack yarn workspace @ulams/docs coverage --report |
Also lists “Needs review” and “Coming” pages |
corepack yarn workspace @ulams/docs typecheck |
sync, then astro check |
corepack yarn workspace @ulams/docs build |
sync, coverage check, astro build with link validation |
corepack yarn workspace @ulams/docs screenshots |
Captures UI screenshots (see below) |
DOCS_VALIDATE_LINKS=0 turns link validation off for a quick local build.
Where pages live
Section titled “Where pages live”Pages are .md or .mdx files in src/content/docs. Each top-level folder is a sidebar section,
listed in astro.config.mjs, and the sidebar inside a section is generated from the files.
Directoryfront/docs-site
- astro.config.mjs sidebar sections, link validation
Directoryscripts
- sync-content.mjs runs the generators
Directorygenerators/ one file per kind of generated page
- …
- coverage.mjs the coverage check
Directorysrc
Directorycomponents
- Screenshot.astro
Directoryassets
Directoryscreens/ captured screenshots (.webp)
- …
Directorycontent/docs
Directorygetting-started/
- …
Directorycreators/
- …
Directorylearners/
- …
Directoryadmin/
- …
Directorydevelopers/
- …
Directoryextending/
- …
Directoryoperators/
- …
Directorycontributing/
- …
Directoryreference/ generated
- …
Directorydecisions/ generated
- …
- roadmap.md generated
Add a page
Section titled “Add a page”-
Create a file in the section folder, for example
src/content/docs/admin/certificates.mdx. The URL follows the path:/admin/certificates/. Use.mdxwhen you need components,.mdotherwise. -
Add the frontmatter.
titleanddescriptionare required; the description is one sentence of 50 to 160 characters (it is the search-result and link-preview text).---title: Certificatesdescription: Design certificate templates, bind them to course completion and check what learners receive.sidebar:order: 30 # position in the section, lower firstmodules:- templates-pdf # what this page documents, see the coverage checkadminRoutes:- /configuration/templates/**--- -
Write the page. Import components at the top of an
.mdxfile:import { Aside, Badge, Steps, Tabs, TabItem, FileTree } from "@astrojs/starlight/components";In
.mdxprose,{,}and<start expressions and tags: escape them (\{,\},<) or put them in inline code. -
Run
corepack yarn workspace @ulams/docs coverageand look at the page indev:docs.
A new section (a new top-level folder) also needs an entry in the sidebar list in
astro.config.mjs. The section’s index.mdx with sidebar.order: 0 is its overview.
Frontmatter fields
Section titled “Frontmatter fields”Besides Starlight’s own fields (title, description, sidebar, tableOfContents, …), the
content schema in src/content.config.ts adds these:
| Field | Type | Meaning |
|---|---|---|
modules |
list of strings | Modules this page documents: folder names in api/packages (for example tenancy, lti) and the app keys api, admin, front, web, sdk, ui, api-h5p, api-pdf. |
adminRoutes |
list of strings | Admin routes this page documents, as written in admin/config/routes.ts (for example /users/groups). A trailing /** matches the route and everything below it. |
learnerRoutes |
list of strings | Learner routes of the reference frontend, derived from the file paths in front/web/src/pages (index dropped, so courses/[id].astro is /courses/[id]). /** works the same way. |
topicTypes |
list of strings | Topic types this page documents, by class name as registered in a package service provider (for example RichText). |
coming |
boolean | The feature is on the roadmap but not in the code yet. Shows a “Coming” badge under the title. |
needsReview |
boolean or string | Documented from the code but not verified end to end. Shows a “Needs review” badge; a string is shown under the title as what to check. |
generatedFrom |
string | Set only by the generators: the source file of a generated page. Never set it on a written page. |
Set coming and needsReview honestly: they tell readers how far to trust a page, and the
--report option lists them so they can be worked down.
The coverage check
Section titled “The coverage check”scripts/coverage.mjs builds an inventory from the code and fails when any item is not named by at
least one written page:
- every folder in
api/packagesplus the eight app keys (modules); - every admin route in
admin/config/routes.ts, except redirects,/,*and/user(adminRoutes); - every page file under
front/web/src/pages(learnerRoutes); - every topic type class registered with
registerContentClassorregisterContentClassesin a package service provider (topicTypes).
It also fails when a frontmatter entry matches nothing in the code (a renamed route or a removed package), when frontmatter is not valid YAML, and when a page has no description or one shorter than 20 characters. Generated pages do not count towards coverage, so a reference page cannot stand in for a missing guide.
corepack yarn workspace @ulams/docs coverage # exit 1 on gapscorepack yarn workspace @ulams/docs coverage --report # plus "Needs review" and "Coming" listsThe check runs in build and lint, so the docs build in CI fails on gaps. When you add a package,
an admin route, a learner page or a topic type, add or extend a page in the same branch (see
Decisions and documentation).
Generated pages
Section titled “Generated pages”scripts/sync-content.mjs deletes and rewrites these on every dev, build, typecheck and
lint. They are listed in front/docs-site/.gitignore and carry a “Generated from” badge.
Never edit a generated file: your change is overwritten on the next run. Edit the source:
| Pages | Source to edit |
|---|---|
| Decisions | docs/decisions/*.md and docs/decisions/README.md; api/docs/adr, admin/docs/adr, front/docs/adr |
| Roadmap | docs/ROADMAP-TODO.md |
| Packages and the other reference pages | Package READMEs (api/packages/*/README.md) and the code itself: permissions, settings, environment variables, events, artisan commands, schedules, routes, topic types, the UI catalogue |
| Contributing guide, Code of conduct, Security policy | CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md at the repository root |
The “Edit page” link on a generated page already points to its source file. Relative links in the sources are rewritten: links to another ADR or the roadmap become site links, everything else becomes a GitHub link, so write ordinary relative links in the source files.
Screenshots
Section titled “Screenshots”Screens are .webp files in src/assets/screens, captured with:
corepack yarn workspace @ulams/docs screenshotsThe capture (scripts/screenshots.mjs, Playwright and sharp) opens a demo tenant: the admin panel
at http://coffee.admin.localhost and the learner site at http://coffee.app.localhost, with the
API at http://coffee.localhost. Demo mode must be on for that tenant (Demo mode):
the admin panel then logs in as the tenant admin and the script logs in as the demo student, so no
password is involved. Override the hosts with DOCS_SHOTS_ADMIN, DOCS_SHOTS_LEARNER and
DOCS_SHOTS_API, and pass a name fragment to capture only some screens
(… screenshots admin-users). Admin screens are named after their route in
admin/config/routes.ts (/users/list is admin-users-list, course editor tabs are
admin-courses-list-course-<tab>), learner screens after their page (learner-learn-courseid),
and every topic type found in the demo course gets learner-topic-<type>. The script only opens
pages; start the stack first (see Local development).
Show a screen with the Screenshot component:
import Screenshot from "../../../components/Screenshot.astro";
<Screenshot name="admin-users-list" alt="The user list with filters" caption="Users, filtered by group." />name is the file name without .webp. alt is required: describe what the screen shows, not
“screenshot”. The component renders nothing while the file does not exist yet, so a page can
reference a screen before it is captured. Images are resized and served responsively by Astro.
- Link between pages with root-relative paths and a trailing slash:
/admin/tenants/, not../tenantsor/admin/tenants. When the site is published under a base path (GitHub Pages), the paths are prefixed automatically. - Link to code and files in the repository with full GitHub URLs
(
https://github.com/ulams-dev/ulams/blob/main/...). starlight-links-validatorchecks every internal link, including#anchors, duringbuild; a broken link fails the build./api/**is excluded.
- Write for the reader of the section: course authors and administrators get task-oriented pages without code; developers and operators get commands and file paths.
- Describe what the code does today. Planned behaviour goes on a page marked
coming: trueor in anAside, never mixed into the description of current behaviour. - English, short sentences, sentence-case headings. Code conventions are in Coding standards; how to test what you document is in Testing.
Build and publishing
Section titled “Build and publishing”.github/workflows/docs.yml checks and builds the site on pull requests that touch
front/docs-site/**, docs/** or the code the reference pages and the coverage check are made
from (packages, routes, admin routes, front/web pages, the UI registry; see
Continuous integration), and on push to main builds it again and deploys it
to GitHub Pages. The workflow sets DOCS_SITE and DOCS_BASE from
actions/configure-pages, which astro.config.mjs reads; locally the site runs at the root. A pull
request whose docs build fails (coverage gap, broken link, invalid frontmatter) is not merged.
corepack yarn workspace @ulams/docs coveragecorepack yarn workspace @ulams/docs typecheckcorepack yarn build:docscorepack yarn workspace @ulams/docs preview # http://localhost:4322