# Authoring conventions for `public-docs`

Rules for anyone — human or AI agent — adding to or editing this documentation
site. They exist because the site is published in three forms at once (HTML,
`llms*.txt`, and a raw Markdown mirror), and all three only stay correct if the
source obeys a few constraints.

`npm test` enforces everything in this file that can be enforced mechanically.
Run it before you commit.

## Non-negotiables

1. **Plain Markdown only.** Files are `.md`, never `.mdx`. No `import`
   statements, no JSX components. The site is configured with
   `markdown.format: 'md'`, so JSX would not render anyway — but the real
   reason is that `/raw/<path>.md` and `llms-full.txt` serve the source
   directly, and content locked inside a component is content an agent cannot
   read.
2. **Every page needs frontmatter.** `id`, `title`, and `description` are
   required. `description` must be a single line of 20–200 characters — it is
   what appears next to the page in `llms.txt`, so write it as a real summary,
   not a restatement of the title.
3. **Never edit `static/llms.txt`, `static/llms-full.txt`, `static/robots.txt`,
   `static/agents.md`, or `static/raw/`.** They are generated by
   `scripts/build-ai-index.mjs` on every build and are gitignored.
   (`static/agents.md` is a copy of this file, so the authoring contract is
   published by the docs site itself rather than off on a repo host.)
4. **Never link to github.com — or any repo host — from a page.** This is a
   public product manual; a studio admin cannot act on a repo link. There is
   deliberately no `editUrl` in `docusaurus.config.ts` either, so no page
   renders an "Edit this page" link.
5. **Never mention `llms.txt`, `llms-full.txt`, `/raw/`, or `agents.md` in page
   prose.** Those endpoints exist and are fully discoverable — via their
   conventional root paths, `robots.txt`, and the `<link rel="alternate">` tags
   in `docusaurus.config.ts`'s `headTags` — but they are plumbing, and plumbing
   on the page is noise for the end user.

Rules 4 and 5 are enforced by the `no page links to a repository host` and
`no page mentions the machine-readable endpoints` tests.

## Frontmatter template

```yaml
---
id: studio-settings
title: Studio settings
description: Configure operating hours, the schedule request window, timezone, and integrations.
sidebar_position: 3
keywords: [studio hours, request window, timezone]
---
```

| Key | Required | Notes |
| --- | --- | --- |
| `id` | yes | Unique. Matches the filename for leaf pages; `<folder>/index` for section indexes. |
| `title` | yes | The page's `H1`. Don't repeat it as a Markdown heading — Docusaurus renders it. |
| `description` | yes | One line, 20–200 chars. Used in `llms.txt`, meta tags, and search results. |
| `sidebar_position` | recommended | Orders the page within its section. |
| `keywords` | recommended | Inline array. Helps search and retrieval. |
| `slug` | rarely | Overrides the URL. Only `docs/index.md` uses it (`slug: /`). |

## Adding a page

1. Create `docs/<section>/<name>.md` with the frontmatter above.
2. Give it a `sidebar_position` that slots it where you want.
3. Add a **See also** section at the foot with relative links to neighbouring
   pages.
4. Run `npm test && npm run build`.

The sidebar is autogenerated from the folder tree — `sidebars.ts` needs no
change.

## Holding a page back until a feature ships

A page documenting something that is not released yet stays in `docs/`, marked
`draft: true` in its frontmatter with a comment saying why:

```yaml
# Unreleased: the feature is not shipped yet. `draft` keeps this page out of
# the production build, the sidebar, and the generated indexes; it still
# renders on the local dev server. Delete this block to publish the page.
draft: true
```

Docusaurus drops a draft from `npm run build` and keeps it in `npm start`, and
`scripts/build-ai-index.mjs` drops it from `llms.txt`, `llms-full.txt`, and the
raw mirror — that last one matters most, since it would otherwise serve the
whole page verbatim to anyone who asked.

Two things do not happen automatically:

- **No published page may link to it.** A link to a draft is a broken link in
  production, and it advertises a feature the reader cannot use. Remove the
  links *and* the surrounding prose, glossary entries, and FAQ answers that
  describe the feature. The `no published page links to a draft page` test
  fails the build if a link survives.
- **The draft is still checked.** `npm test` validates draft pages like any
  other, and `npm run images` still lists their outstanding artwork, so a page
  held back for a release does not rot while it waits.

Publishing it later is: delete the `draft` block, restore the links (`git log`
on this page is the record of what was removed), `npm test && npm run build`.

One trace remains by design: the page's `id` appears in the site's JavaScript
bundle, which is how Docusaurus warns you about drafts in dev. If a page must
leave no trace at all, prefix the filename with `_` — Docusaurus ignores it
entirely, at the cost of not being able to preview it.

## Adding a section

Create `docs/<section>/` containing a `_category_.json`:

```json
{
  "label": "For Studio Admins",
  "position": 3,
  "collapsed": false,
  "link": { "type": "doc", "id": "admin/index" }
}
```

`label` becomes the section heading in `llms.txt`; `position` orders sections
in both the sidebar and the generated indexes.

## Links

- **Between docs pages**, use relative Markdown links *with the `.md`
  extension*: `[Studio settings](../admin/studio-settings.md)`. Docusaurus
  rewrites these to final URLs and, crucially, fails the build if the target
  doesn't exist.
- `onBrokenLinks` and `onBrokenMarkdownLinks` are both `'throw'`, so
  `npm run build` is the link checker. A broken link is a red build, not a
  warning.
- **Anchor links** must match the generated slug of the heading —
  `../admin/studio-settings.md#request-window`.

### Linking into the app

Every admin screen sits at a fixed address — no studio id and no account id in
the path — so one link works for every reader. Link the screen rather than
printing its path: a reader following it lands on that screen in their own
studio, signing in on the way if they need to.

- **Write app URLs in full**: `https://app.myroutine.io/company/users`. A
  relative link resolves against the docs site, not the app.
- **Hang the link on the bold UI label** the reader sees on screen —
  `**[Your Studio](https://app.myroutine.io/company/users)**` — so the sentence
  still reads as an instruction rather than an address.

| Screen | URL |
| --- | --- |
| Home / dashboard | `https://app.myroutine.io/company` |
| Your Studio | `https://app.myroutine.io/company/users` |
| Studio Scheduling | `https://app.myroutine.io/company/schedule` |
| Studio Settings | `https://app.myroutine.io/company/scheduler` |
| Studio Settings, one tab | `…/company/scheduler?tab=` + `hours`, `request-window`, `timezone`, `integrations`, `language` |
| Send Schedule Links | `https://app.myroutine.io/company/schedule-link-emails` |

- **Never link an address containing an id.** A student's schedule page and a
  family's schedule link both carry an identifier that differs per reader;
  describe how to reach them in the UI instead.
- `Capture:` lines inside screenshot and recording comments still name the bare
  path. They are directions to whoever takes the shot, not reader-facing links.

The `links into the app are absolute and point at a shared screen` and
`app screens are linked, not printed as bare paths` tests enforce this.

## Images and screenshots

Images live in `static/img/<section>/` and are referenced with an absolute
path: `/img/admin/studio-settings-hours.png`.

Screenshots that haven't been captured yet are recorded as a commented block so
the page never renders a broken image:

```markdown
<!-- SCREENSHOT · static/img/admin/studio-settings-hours.png
     Capture: /company/scheduler?tab=hours — full page, light mode, 1440px wide
     Then delete these comment markers:
![Studio Settings — Studio Hours tab](/img/admin/studio-settings-hours.png)
-->
```

To land a screenshot: save the PNG at the path named on the first line, then
delete the `<!--` and `-->` lines. Nothing else changes.

### Recordings

Where an action is easier to watch than to read — dragging a lesson, drawing a
time range, an upload running to a preview — use an animated GIF instead of a
still. Pending recordings are recorded the same way, with a `RECORDING` marker:

```markdown
<!-- RECORDING · static/img/students/verify-calendar.gif
     Record: drag an entry to a new time, resize it, then delete it
     Then delete these comment markers:
![Adjusting entries on the verify calendar](/img/students/verify-calendar.gif)
-->
```

The target must be a `.gif` under `static/img/` — a still belongs in a
`SCREENSHOT` slot, and `npm test` enforces the split. Landing one works exactly
like a screenshot: save the file at the path on the first line, delete the
comment markers.

Keep each recording **silent, 5–10 seconds, looping, and about a single
action**. One that covers three things is one nobody can follow. Prose still has
to carry the instruction — a GIF is unreadable to a screen reader and to an
agent, so the alt text and the surrounding paragraph must stand on their own.

### Illustrations

Illustrations — brand-palette artwork of students, families, and studio owners
— live in `static/img/illustrations/` and are referenced live, never commented
out. Every one currently on the site is a **placeholder**: a filler SVG
carrying a `<!-- routine:placeholder -->` marker comment.

To land real artwork, overwrite the SVG at the same path. The marker goes with
it, the page needs no edit, and the slot drops off the outstanding list.

Three rules:

- **Deliver SVG at the existing path.** A different filename or extension means
  editing every page that references it.
- **Survive dark mode.** The site respects `prefers-color-scheme`, and an
  illustration is one asset serving both themes. Either give it a self-contained
  background (a rounded panel, as the placeholders do) or use strokes and fills
  that hold up on `#16202b` as well as white. A transparent illustration drawn
  in dark ink disappears in dark mode.
- **Alt text carries the content.** An illustration is decoration for a sighted
  reader and *nothing at all* for a screen reader or an agent unless the alt
  text describes the scene. The test suite requires at least 15 characters, but
  the real bar is: would this sentence make sense read aloud in place of the
  image? Never let an image be the only carrier of a fact — that is why the home
  page keeps a terse table under its flow illustration.

### Finding outstanding artwork

```bash
npm run images        # placeholder illustrations + uncaptured screenshots and recordings
```

Prints a Markdown table to stdout. It is not committed anywhere — a checked-in
list would rot the moment a slot moved. Redirect it if you want a file to hand
to a designer: `npm run images > artwork.md`.

## Style

- Write for the reader's task, not the system's structure. "Chasing
  stragglers", not "The completion-tracking subsystem".
- Use the exact UI labels in **bold** — **Generate Schedule**, **Regenerate
  Link**. Readers match words on screen.
- Prefer a table to a bulleted list when there are two or more facts per item.
- State consequences, especially destructive ones. Use `:::warning` and
  `:::danger` admonitions for anything that loses data or is hard to reverse.
- One `H1` per page, supplied by the frontmatter `title`. Start body content at
  `##`.

## Commands

```bash
npm start        # dev server on :3000, regenerates the AI index first
npm run build    # production build; fails on broken links
npm run serve    # serve the production build locally
npm test         # frontmatter, llms.txt, raw-mirror, and no-JSX assertions
npm run typecheck
npm run images   # artwork still needed: illustrations, screenshots, recordings
npm run ai-index # regenerate llms.txt / llms-full.txt / agents.md / raw mirror only
```

## CI

`public-docs/**` is wired into CircleCI's path filtering as the `docs-changed`
flag. A commit touching only this folder runs `build-docs` and halts every
backend and frontend job. See `.circleci/config.yml` and
`.circleci/continue-config.yml`.
