# Writing pages

> Frontmatter, URLs, menu order, links and images.

A page is a `.md` or `.mdx` file in the version folder. Both go through the
same chain: the extension does not change what is possible, it only states the
intent.

## The frontmatter

```yaml
---
title: Installation
description: What you need, and how to set up a project.
date: 2026-09-09
tags: [guide, installation]
---
```

`title` becomes the `<title>` tag, the menu entry and the breadcrumb.
`description` feeds the metadata and the JSON-LD. `tags` show at the bottom of
the page, below the text they describe — one tag may be written without
brackets. Everything is optional: without `title`, the project name stands in.

The fields are detailed in the [reference](/versions/latest/reference/frontmatter/).

## Who wrote the page

A page that names its authors, or carries a date, opens with a byline:

```yaml
---
title: Installation
authors: [ada, grace]
date: 2026-09-09
modified: 2026-09-16
---
```

The names show as written. To give them a biography, an avatar and a link,
describe them in a JSON file of the version folder:

```json
{
  "ada": {
    "name": "Ada Lovelace",
    "bio": "Wrote the first algorithm meant for a machine.",
    "avatar": "authors/ada.png",
    "url": "https://example.com/ada"
  }
}
```

Then name that file in the configuration: `authors: 'authors.json'`. It is
read **in each version folder**, as the menu is — a biography corrected in
the version being written leaves the published one alone. The file itself is
not published; the avatars are, and are measured at the build so that the
text does not jump when they arrive. A version without the file shows the
names alone: an older version needs no copy of it.

A key nobody describes is shown as written, which is what lets a project name
its authors before describing them. The description also feeds the page data:
a biography becomes the `description` of its `Person`, a link its `url`.

A date written the day the page was is not repeated as an update, and a home
page carries no byline at all: it is an entrance hall, not a document.

## What the URL depends on

The file path gives the URL path, stripped of its extension and of its sorting
prefix:

| File                        | URL                    |
| --------------------------- | ---------------------- |
| `index.md`                  | `/`                    |
| `guide/01-installation.md`  | `/guide/installation/` |
| `guide/index.md`            | `/guide/`              |
| `components/02-columns.mdx` | `/components/columns/` |

The `01-` prefix **orders the menu without appearing in the URL**. It is the
only way to sort pages other than alphabetically, and it saves keeping a
separate list.

## The menu order

The menu is derived from the file tree. Folders become sections, numeric
prefixes give the order, and an `index.md` in a folder provides the title of
the section.

```
docs/v1.0/
├── index.mdx           →  /
├── guide/
│   ├── index.md        →  /guide/        (title of the section)
│   ├── 01-installation.md
│   └── 02-first-site.md
└── components/
    ├── index.md
    └── 01-card.mdx
```

## Series of pages

A folder is a **series**: its `index` page introduces it, and the pages beside
that index are its instalments. Written in that index, one line builds the grid
of its pages, as clickable cards:

```mdx
<Cards />
```

Each card takes the `title` and `description` of its page, its `preview` as an
image, and its `modified` date. On the index of a folder that holds other
folders, the cards of those series also count their pages. Nothing is listed
by hand, so nothing goes stale when a page is added or renamed.

The [Cards](/versions/latest/components/cards/) page details every option.

## Writing the menu by hand

When the file tree does not give the menu you want, describe it in a JSON file
of the version's folder, and name it in the configuration:

```js
// docpensieve.config.mjs
sidebar: 'sidebar.json',
```

```json
[
  "/",
  { "label": "Start here", "items": ["guide/installation", "guide/first-site"] },
  { "auto": "components" },
  { "label": "Repository", "href": "https://github.com/me/my-project" }
]
```

A page is named by its path, as in its URL. `{ "auto": "components" }` keeps
the automatic menu of a folder — the DocPensieve section stays whole that way.
A page the file leaves out is still published, only off the menu, and a
version without the file keeps the menu of its folders — an older version
needs no copy of it. Every kind of entry is in the
[`sidebar` reference](/versions/latest/reference/configuration/).

## Internal links

Two spellings, two meanings:

- **relative** — `./sibling/`, `../guide/` — resolves against the folder of the
  page's file, as between any two files;
- **absolute** — `/guide/installation/` — starts from the **version root**, not
  from the domain root.

The second rule deserves a pause. A documentation does not know it may be
served under `/my-project/versions/v1.0/`: if `/guide/installation/` were taken
literally, every internal link would break as soon as a prefix comes into play.
They are therefore rewritten at build time.

To target a real domain URL, the full address remains.

## Images

An image sits next to the page and is written relatively:

```md
![Pipeline diagram](./diagram.png)
```

Files that are not pages are copied as is into the output, at the same
relative place. The path is rewritten like a link.

The build reads the width and height of each image from its file — PNG, JPEG,
GIF, WebP or SVG — and writes them on the page: the browser keeps the room
before the image arrives, instead of shifting the text when it does. Every
image but the first loads lazily, when the reader nears it; the first, often in
view, keeps its normal loading.

## Components

In an `.mdx` page, the shipped components are used **without an import**:

```mdx
<Columns>
  <Column span={8}>The bulk of the point</Column>
  <Column span={4}>A side remark</Column>
</Columns>
```

They are rendered at build time: the delivered HTML only holds their result.
The list is in [Components](/versions/latest/components/).

`globalComponents: false` removes them from every page. It is meant for a
project that would rather bring its own: a page still using one then stops the
build, naming it.

## A trap to know

The content of a JSX tag **left alone on its own line** becomes a paragraph:

```mdx
<p className="flex gap-3">Some text</p>
```

produces `<p class="flex gap-3"><p>Some text</p></p>` — two nested paragraphs,
which is invalid. The browser closes the first one by itself: the wrapper
disappears, and the intended layout with it.

The formatter makes the trap sneaky. A long string of classes ends up broken
over several lines, which leaves the text alone on its line **after the fact**,
without anyone having written it that way.

Three ways to guard against it:

- prefer `<div>` to `<p>` as a wrapper — a paragraph is valid inside it;
- write short content on the same line as its tags;
- for repeated cases, put a class in the `theme/` folder rather than a long
  string of utilities, so that the line stays short.

`docpensieve check` reports these nestings on the produced site.

## What is not published

A page whose frontmatter carries `draft: true` is loaded but kept out of the
output. That is what lets a work-in-progress page stay in the repository
without being published.
