# Writing docs (/dev/writing-docs)



Every Remy app keeps its docs the same way, so the shared tasks and the Fumadocs CLI work in each one.
There are two docs sites, served by the docs Worker (the `docs/` app, apart from the app Worker): the guide
for the app's users at `/docs`, and these developer docs at `/dev`.
Each has its own languages, search, `llms.txt` and Ask AI folder.

## Who each part is for [#who-each-part-is-for]

The docs have three parts, and each is written from one reader's side. Google's AI Mode, Gemini and every
other assistant tell them apart only by what the pages say, so keep them apart:

| Part                                      | Reader                                    | Writes about                           | Never                                          |
| ----------------------------------------- | ----------------------------------------- | -------------------------------------- | ---------------------------------------------- |
| Product guide, `content/users/` (`/docs`) | someone using the app                     | what they see and do in the app        | code, tools, the repository, deploys           |
| Developer docs, `content/dev/` (`/dev`)   | a developer building Remy or an app on it | code, tasks, tools, rules              | how to use the app, beyond a link to the guide |
| API reference (`/reference`)              | a developer calling the API               | endpoints, generated from the contract | prose; it is generated                         |

* A page that needs the other side links to it in one line ("Building it? The developer docs.").
* Each part's home page opens by saying who it is for; its titles end with the part's name (`docs.config.ts`,
  `titles`), and so do its `llms.txt` and MCP server ([Docs in your AI tools](./ai-tools.md)).
* The parts, their readers, llms files and MCP servers are one list, `src/lib/sections.ts`.

## Layout [#layout]

<Files>
  <Folder name="docs/content">
    <Folder name="users">
      <File name="i18n.json" />

      <File name="meta.json" />

      <File name="index.mdx" />

      <File name="formats.mdx" />
    </Folder>

    <Folder name="dev">
      <File name="i18n.json" />

      <File name="meta.json" />

      <File name="index.md" />

      <File name="index.es.md" />

      <File name="writing-docs.mdx" />
    </Folder>
  </Folder>
</Files>

* `i18n.json` is the site's languages: `defaultLanguage` and `languages`. The site's URLs, language menu,
  search and translation tasks all read it. The two sites' lists differ: developer docs are translated
  only into the languages we support developers in.
* `meta.json` is the site's navigation: `pages` sets the order, `"---Heading---"` starts a section,
  `"[Title](https://…)"` is a link out.
* A translation sits beside its page: `index.es.md` beside `index.md`, at `/dev/es/`.
* The guide's pictures and videos are in `docs/public/media`, referenced by URL (`/media/formats.png`); the developer docs have Mermaid diagrams and type tables;
  the API reference is its own section at `/reference`, made from the oRPC contract.

## How a page reaches the reader [#how-a-page-reaches-the-reader]

<Mermaid
  chart="flowchart LR
  A[&#x22;docs/content/*.md, *.mdx&#x22;] --> B[&#x22;fumadocs-mdx macro&#x22;]
  B --> C[&#x22;loader: src/lib/source.ts&#x22;]
  C --> D[&#x22;Docs pages: /docs, /dev&#x22;]
  C --> E[&#x22;Search: /api/search/dev&#x22;]
  C --> F[&#x22;/dev/llms.txt and .md pages&#x22;]
  C --> G[&#x22;MCP server: /api/mcp/dev&#x22;]
  C --> H[&#x22;Social images: /og/dev/...&#x22;]
  I[&#x22;oRPC contract&#x22;] --> J[&#x22;fumadocs-openapi&#x22;] --> K[&#x22;API reference: /reference&#x22;]"
/>

## Frontmatter [#frontmatter]

Every page starts with a title and a description. The description is the page's summary in search
results, `llms.txt` and link previews.

```md title="docs/content/users/formats.mdx"
---
title: "Formats"
description: "How dates, numbers, currency and direction change with the language you choose."
---
```

## Pictures and videos [#pictures-and-videos]

<CalloutContainer type="info">
  <CalloutDescription>
    Take them with the project's own tool, so they show the real app:
    `npm run shots -- /formats --out docs/public/media` for a picture, and add `--video`
    for a short recording of the page.
  </CalloutDescription>
</CalloutContainer>

<Tabs items="['Picture', 'Video']">
  <Tab value="Picture">
    ```md
    ![The Formats page in English](/media/formats.png)
    ```

    Fumadocs sizes it, and a click enlarges it.
  </Tab>

  <Tab value="Video">
    ```mdx
    <video src="/media/formats-tour.webm" controls muted playsInline preload="metadata" />
    ```

    A video needs an `.mdx` page (it is a JSX element).
  </Tab>
</Tabs>

## Components [#components]

In `.mdx` pages: `<Callout>` (or `:::note`, `:::warning` in any page), `<Cards>` and `<Card>`, `<Tabs>`
and `<Tab>`, `<Steps>` and `<Step>`, `<Accordions>` and `<Accordion>`, `<Files>`, `<Folder>` and
`<File>`, a ` ```mermaid ` diagram, and a type table read from our TypeScript:

<TypeTable
  id="type-table-source.server.ts-DocsPageData"
  type="{
  &#x22;id&#x22;: &#x22;source.server.ts-DocsPageData&#x22;,
  &#x22;name&#x22;: &#x22;DocsPageData&#x22;,
  &#x22;description&#x22;: &#x22;Everything a docs page needs, all serializable.&#x22;,
  &#x22;entries&#x22;: [
    {
      &#x22;name&#x22;: &#x22;site&#x22;,
      &#x22;description&#x22;: &#x22;&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;\&#x22;dev\&#x22; | \&#x22;docs\&#x22;&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;union&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;slug&#x22;,
      &#x22;description&#x22;: &#x22;The page's slugs joined ('' for the site's index).&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;path&#x22;,
      &#x22;description&#x22;: &#x22;The page's file in its collection (the translation, else English), which the browser loads.&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;lang&#x22;,
      &#x22;description&#x22;: &#x22;The language of the page (the URL's).&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;languages&#x22;,
      &#x22;description&#x22;: &#x22;The site's languages.&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string[]&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;array&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;langs&#x22;,
      &#x22;description&#x22;: &#x22;The languages this page has its own text in, the default first: its hreflang alternates.&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string[]&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;array&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;title&#x22;,
      &#x22;description&#x22;: &#x22;&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;description&#x22;,
      &#x22;description&#x22;: &#x22;&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;lastModified&#x22;,
      &#x22;description&#x22;: &#x22;The page file's last commit date (fumadocs-mdx `lastModified`), ISO.&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string | undefined&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: false,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;headings&#x22;,
      &#x22;description&#x22;: &#x22;&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;DocsHeading[]&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;array&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;pageTree&#x22;,
      &#x22;description&#x22;: &#x22;The page tree in the language, serialized for DocsLayout (useFumadocsLoader).&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;SerializedPageTree&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;object&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;links&#x22;,
      &#x22;description&#x22;: &#x22;English file path in the collection → this language's URL, for relative links.&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;Record<string, string>&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;Record<string, string>&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;origin&#x22;,
      &#x22;description&#x22;: &#x22;The request's origin (https://…), for the head's absolute URLs: canonical, hreflang, social image.&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;url&#x22;,
      &#x22;description&#x22;: &#x22;&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;markdownUrl&#x22;,
      &#x22;description&#x22;: &#x22;&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    },
    {
      &#x22;name&#x22;: &#x22;githubUrl&#x22;,
      &#x22;description&#x22;: &#x22;&#x22;,
      &#x22;tags&#x22;: [],
      &#x22;type&#x22;: &#x22;string&#x22;,
      &#x22;simplifiedType&#x22;: &#x22;string&#x22;,
      &#x22;required&#x22;: true,
      &#x22;deprecated&#x22;: false
    }
  ]
}"
/>

## The Fumadocs CLI [#the-fumadocs-cli]

`npm run docs:cli -- feature <name>` (remy-auth only) runs the pinned Fumadocs CLI in the shared docs
Worker, `packages/ui/src/docs`; its `cli.json` maps the folders there, and what it writes is a change to
the package, reviewed like any other. Apps take the Worker from the package and never run it.

| Feature  | What it gave this site                                                                                                 |
| -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `llms`   | `/<site>/llms.txt`, `/<site>/<lang>/llms-full.txt`, `/<site>/<lang>/<page>.md`                                         |
| `mcp`    | an MCP server per part, `/api/mcp/docs`, `/api/mcp/dev`, `/api/mcp/reference` ([Docs in your AI tools](./ai-tools.md)) |
| `og`     | a social image per page                                                                                                |
| `webmcp` | the docs as tools for agents in the browser                                                                            |

## Translations [#translations]

Write English only. `dev:land` translates on main after the landing, one writer at a time
([translations: one writer](./how-we-work.md#translations-one-writer)); `npx remy i18n check docs` lists what each
language still needs.
