# Mdoc Conventions

> Reference for mdoc code-block modifiers and Docusaurus admonitions used across ZIO documentation. Load when writing any documentation that contains Scala code blocks.

- Skill: `zio/mdoc-conventions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add zio/mdoc-conventions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/zio/mdoc-conventions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: zio (https://skillmd.com/u/zio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/zio/mdoc-conventions

---


# mdoc Conventions

Choose an mdoc modifier for every executable Scala block based on whether you need scope sharing and whether output should render.

**Mandatory:** any block a reader could paste and run — it has an `import`, a bound `val`/`def` with a body, a call, or an evaluated expression — MUST carry an mdoc modifier (`mdoc:compile-only` if unsure). Plain ` ```scala ` hides code from the compiler, so use it ONLY for genuinely non-runnable content: abstract signature blocks (declarations with no bodies), pseudocode, ASCII diagrams, sbt/config. Never downgrade a real example to plain ` ```scala ` to dodge a compile error — fix the example.

**Public API only.** An mdoc block compiles in the default package, exactly like user code — reference only PUBLIC symbols. A `private`/`private[pkg]`/`protected` type or member won't compile in a snippet, so never demonstrate a closed extension point: ✅ implement the public `LogRecordProcessor` ❌ implement `LogFormatter` whose required parameter type is `private[telemetry]`. Verify visibility against the real source before drafting; if the only path needs a package-private symbol, pick a public alternative or drop the example.

## Modifiers

- **`mdoc:compile-only`** — Renders source only, isolated scope. **Default** for self-contained examples. Later blocks cannot reference its definitions.
- **`mdoc:silent`** — Renders nothing, scope shared with later blocks. Use to define types/values/imports later blocks reference. Cannot redefine a name later — use `silent:nest` for that.
- **`mdoc:silent:nest`** — Renders nothing, scope shared, code wrapped in an anonymous `object`. Lets you shadow/redefine names from earlier blocks.
- **`mdoc:silent:reset`** — Renders nothing, clears all prior scope. Use when switching to a completely different context mid-document. Because it clears scope, **re-declare every import (and any setup) the block's code uses** — nothing from earlier blocks carries over, so a missing `import` here is a "not found" compile error.
- **`mdoc`** (no qualifier) — Renders source + evaluated output, scope shared. Shows the code and its REPL-style result.
- **`mdoc:invisible`** — Invisible block, scope shared. Rare; prefer `silent` or `compile-only`.
- **`mdoc:embed:<path>`** — Custom modifier: replaces the block (leave its body empty) with the file at `<path>` (repo-root relative) as a titled code fence. Append `:showLineNumbers` for line numbers. Requires the docs subproject to depend on `"dev.zio" %% "zio-sbt-source"` — add it if missing.
- **Plain `` ```scala ``** (no mdoc) — Source only, not compiled. ONLY for non-runnable content: abstract signature blocks (no method bodies), pseudocode, ASCII diagrams, sbt config. Never for a block with imports/calls/bound bodies.

**Never hardcode expression output in comments** (`val x = 42 // 42`). Let `mdoc` render it.

## Choosing the Right Modifier

```
Is this block runnable (imports / bound bodies / calls / expressions)?
├─ NO → plain ```scala (pseudocode, ASCII art, abstract signatures — no bodies)
└─ YES → Do later blocks need these definitions?
   ├─ NO → Show the output?  NO → mdoc:compile-only   YES → mdoc
   └─ YES → Is this a later block showing a result?
      ├─ YES → mdoc
      └─ NO → Redefining an earlier name?  YES → mdoc:silent:nest   NO → mdoc:silent
```

After a `mdoc:silent` block, if you need a completely different context, use `mdoc:silent:reset`.

**Before finishing**, scan every ` ```scala ` fence: if its body is runnable and it has no mdoc modifier, add one.

## Common Patterns

- **Silent setup + output**: `mdoc:silent` block defines helpers/imports; a following `mdoc` block calls them and shows the result.
- **Self-contained**: a single `mdoc:compile-only` block that stands alone.
- **Multi-example suite**: when a page has many independent examples reusing names like `user`/`file`/`config`, start **every** example's first block with `mdoc:silent:reset` to avoid "Conflicting definitions" errors — once per independent example. Each such reset block must re-declare the imports its code uses (scope was cleared).

## For Tutorials (Linear Learning Path)

A tutorial builds one concept on the previous, so favor a shared, accumulating scope:

1. First setup block → `mdoc:silent` (imports, base types).
2. Each concept block that has meaningful output → `mdoc` (shows the result the learner should see).
3. Redefining a type to add a field mid-tutorial → `mdoc:silent:nest`.
4. "Putting It Together" final block → `mdoc:embed:<path>` pointing at the companion `CompleteExample.scala` (single source of truth; the file is compiled by the examples build).

Only `:reset` when the tutorial deliberately restarts in a new domain — rare in a linear tutorial.

## Tabbed Scala 2 / Scala 3 Examples

Tab ONLY when the code a **reader writes** differs (`given` vs `implicit`, `enum` vs `sealed trait`), never for internal decl/impl differences (macro vs `inline def`) with an identical call site — state those in one sentence. When it does differ, add these imports after the frontmatter:

```mdx
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
```

Use `groupId="scala-version"` (syncs all tab groups on the page) and `defaultValue="scala2"`. Keep blank lines inside `<TabItem>` so mdoc processes the fenced blocks. Use `mdoc:compile-only` inside tabs.

## Docusaurus Admonitions

Use admonitions **sparingly** (at most 3-4 per page) for genuinely important callouts:

```
:::note[Optional title]
Context or clarification the learner should remember.
:::

:::tip
A practical shortcut or pattern.
:::

:::caution
A common mistake to watch out for.
:::
```

## Docs classpath

mdoc compiles against the docs project's `.dependsOn(...)`. If the documented module is missing there, add it to build.sbt (match sibling style, e.g. `<module>.jvm`) and reload — never downgrade real code to plain ```` ```scala ```` over a missing dependency.

**A build's gap is fixed in the build, never in the page.** `key not found: VERSION` means the docs project defines no `mdocVariables`, so add `mdocVariables += "VERSION" -> version.value` there and re-run:
✅ `mdocVariables += "VERSION" -> version.value` in build.sbt ❌ writing `% "0.1.0"` into the page (it reads as fixed, and it breaks writing-style rule 25)

## Verifying

Always compile scoped to the files you touched, never the whole docs set
(unscoped `sbt docs/mdoc` recompiles every doc, minutes of sbt) — the sole
exception is the integrator's site-build-gate fallback (see docs-integrator):

```
sbt "docs/mdoc --in <file> --out website/<file>"
```

`mdoc` is an sbt task, not a shell binary: quote the whole `docs/mdoc …` as one argument (never bare `mdoc`, never unquoted).

One `--in`/`--out` pair per file; `out` is the same path prefixed with
`website/`, e.g. `docs/reference/x.md` → `website/docs/reference/x.md`. If the
failure output only shows a stack trace with "stack trace is suppressed; run
'last <scope>'", run that `sbt "last <scope>"` command to get the real error.

If mdoc produces more than ~3 errors, the blocks are likely not isolated — check for a missing `:reset`/`:nest` or a name collision. Strip modifiers from the reported lines, confirm the errors clear, then re-apply one at a time, re-running after each change.

