From 9669d3bf88c1a25228db3f54afb92613896a9003 Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Mon, 22 Jun 2026 10:12:34 -0400 Subject: [PATCH 1/3] chore(docs): drop removed skills repository field and document skills path The skills config rework in fern-api/fern#16510 removes the `repository` display field from page-actions.options.skills (the schema becomes strict) and adds a `path` field that points the CLI at a directory of skills in the docs repo, which it validates, bundles, and publishes. - fern/docs.yml: drop the repository line; keep title/description/learn-more-url, the manual skills array, and install-command (npx skills add fern-api/skills) - host-skills.mdx: rework to recommend declaring page-actions.options.skills.path; demote the hand-built .well-known/skills/ convention to a legacy fallback - install-skills-properties.mdx: add the path ParamField, remove repository - site-level-settings.mdx: path-based Install skills example, drop repository Co-Authored-By: Claude Opus 4.8 (1M context) --- fern/docs.yml | 1 - fern/products/docs/pages/ai/host-skills.mdx | 101 ++++++++++-------- .../pages/navigation/site-level-settings.mdx | 8 +- .../snippets/install-skills-properties.mdx | 12 +-- 4 files changed, 62 insertions(+), 60 deletions(-) diff --git a/fern/docs.yml b/fern/docs.yml index 0d78ac0aa..122270b61 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -172,7 +172,6 @@ page-actions: title: Install agent skills description: Skills for authoring Fern documentation, maintained in our open-source skills repo. learn-more-url: https://buildwithfern.com/learn/docs/ai/agent-skills - repository: https://github.com/fern-api/skills install-command: npx skills add fern-api/skills skills: - name: fern-docs diff --git a/fern/products/docs/pages/ai/host-skills.mdx b/fern/products/docs/pages/ai/host-skills.mdx index 2d2e07774..29b3f2197 100644 --- a/fern/products/docs/pages/ai/host-skills.mdx +++ b/fern/products/docs/pages/ai/host-skills.mdx @@ -4,19 +4,19 @@ description: Serve author-supplied agent skills from your Fern docs site and con availability: beta --- -Fern Docs sites can serve your own [Agent Skills](https://www.skills.sh) so your users can discover and install them with the [skills CLI](https://www.skills.sh). You can also add an "Install skills" button to your docs UI that opens a modal with the install command and your skill list. +Fern Docs sites can serve your own [Agent Skills](https://www.skills.sh) so your users can discover and install them with the [skills CLI](https://www.skills.sh). Point `page-actions.options.skills.path` at a directory of skills in your docs repo, and Fern validates the bundle, generates the discovery manifest, and publishes it — so anyone can run `npx skills add https://`. You can also add an "Install skills" button to your docs UI that opens a modal with the install command and your skill list. - + -Place your skills in your `fern/` folder under `.well-known/agent-skills/` (the current v0.2.0 spec) or `.well-known/skills/` (the legacy v0.1.0 layout). Both are supported and can coexist. Each skill is a subdirectory holding at least a `SKILL.md`, with optional supporting files alongside it. +Put your skills in a single directory, with each skill in its own subdirectory holding at least a `SKILL.md`. Supporting files (references, scripts) live alongside it. + +The directory can live anywhere in your repo. A common choice is a repo-root `.agents/skills/` folder, which doubles as the location local coding agents (Claude Code, Cursor, and others) read from: - - - - - + + + @@ -28,15 +28,18 @@ Place your skills in your `fern/` folder under `.well-known/agent-skills/` (the + + + -Each `SKILL.md` needs YAML frontmatter with `name` and `description`, following the [Agent Skills spec](https://agentskills.io): +Each `SKILL.md` needs YAML frontmatter with `name` and `description`, following the [Agent Skills spec](https://agentskills.io). The `name` must be kebab-case, at most 64 characters, and match its parent directory name: -```markdown title="fern/.well-known/agent-skills/plant-care/SKILL.md" +```markdown title=".agents/skills/plant-care/SKILL.md" --- name: plant-care description: Guide for diagnosing and treating common plant diseases. @@ -48,43 +51,36 @@ When diagnosing a plant issue, check for... ``` - - -The `index.json` at the root of the skills directory is required. It enumerates every skill so clients can discover them in a single request, referencing each by `url` and `digest`: - -```json title="fern/.well-known/agent-skills/index.json" -{ - "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json", - "skills": [ - { - "name": "plant-care", - "type": "skill-md", - "description": "Guide for diagnosing and treating common plant diseases.", - "url": "/.well-known/agent-skills/plant-care/SKILL.md", - "digest": "sha256:c4d5e6f7..." - } - ] -} -``` + -The legacy v0.1.0 manifest lists a `files` array per skill instead of `url` and `digest`. +Point `page-actions.options.skills.path` at your skills directory. The path resolves relative to the folder containing `docs.yml`, and `../` is allowed — so a `docs.yml` in `fern/` can reach a repo-root `.agents/skills/` directory: + +```yaml title="fern/docs.yml" +page-actions: + options: + skills: + path: ../.agents/skills +``` -Fern uploads the bundle during `fern generate --docs` and serves the files as-is — no `docs.yml` configuration is required. `fern check` validates the bundle at upload time, flagging: +Fern discovers and publishes your skills during `fern generate --docs`. For every subdirectory containing a `SKILL.md`, it generates the `/.well-known/skills/index.json` discovery manifest and uploads the skill's files — nothing is written back to your repo. + +`fern check` validates the bundle at build time and blocks publishing on: -- A missing or unparseable `index.json` +- A declared path that doesn't exist or contains no skills - A `SKILL.md` whose `name` isn't kebab-case, exceeds 64 characters, or doesn't match its parent directory name - A `SKILL.md` with a missing or empty `description` (max 1,024 characters) +- Duplicate skill names across the directory +- A markdown reference that escapes the skill's directory (`../`), which would dangle once the skill is installed -Once published, skills are served at: +Once published, your skills are served at: -- `https://your-docs-domain.com/.well-known/agent-skills/index.json` (discovery manifest) -- `https://your-docs-domain.com/.well-known/agent-skills//SKILL.md` (individual skill) -- `https://your-docs-domain.com/.well-known/skills/index.json` (legacy v0.1.0 manifest) +- `https://your-docs-domain.com/.well-known/skills/index.json` (discovery manifest) +- `https://your-docs-domain.com/.well-known/skills//SKILL.md` (individual skill) -For sites with a basepath like `/docs`, the endpoints live under that basepath (e.g., `https://example.com/docs/.well-known/agent-skills/index.json`). +For sites with a basepath like `/docs`, the endpoints live under that basepath (e.g., `https://example.com/docs/.well-known/skills/index.json`). @@ -98,25 +94,32 @@ npx skills add https://your-docs-domain.com The `https://` scheme is required: a bare domain is treated as a GitHub repository shorthand. - + -Surface your skills in the docs UI by adding an "Install skills" button to the [page action bar](/learn/docs/configuration/site-level-settings#page-actions-configuration). Set `page-actions.options.skills` in `docs.yml` and the button opens a modal with a copyable install command, the list of available skills, and an optional link to the source repository. +Setting `page-actions.options.skills` already adds an "Install skills" button to the [page action bar](/learn/docs/configuration/site-level-settings#page-actions-configuration). The button opens a modal with a copyable install command and your skill list (read from the generated manifest). Customize the modal with `title`, `description`, and `learn-more-url`: -```yaml docs.yml +```yaml title="fern/docs.yml" page-actions: options: skills: + path: ../.agents/skills title: Plant API Skills description: Skills for working with the Plant Store API. - repository: https://github.com/your-org/plant-api-skills - install-command: npx skills add https://your-docs-domain.com - skills: - - name: plant-care - description: Guide for diagnosing and treating common plant diseases. - url: https://github.com/your-org/plant-api-skills/tree/main/plant-care - - name: garden-planner - description: Plan garden layouts and planting schedules. + learn-more-url: https://your-docs-domain.com/skills ``` + +By default the modal shows `npx skills add https://`. Override it with `install-command` when that command won't work as-is — for example auth-walled docs, a private registry, or a multi-step install: + +```yaml title="fern/docs.yml" +page-actions: + options: + skills: + path: ../.agents/skills + install-command: + - npm login --registry https://npm.your-org.com + - npx skills add https://your-docs-domain.com +``` + @@ -125,3 +128,7 @@ page-actions: Configure the Install skills button under `page-actions.options.skills` in `docs.yml`. + +## Legacy: hand-built `.well-known/skills/` + +Before `path` existed, you could serve skills by hand-populating a `.well-known/skills/` (or `.well-known/agent-skills/`) directory inside your `fern/` folder, complete with your own `index.json` manifest. Fern still uploads such a directory as-is when no `path` is declared, so existing setups keep working. Prefer `path`: it generates the manifest for you and validates the bundle. If you declare a `path` and also have a hand-built `fern/.well-known/skills/` folder, the declared path takes precedence and Fern warns about the conflict. diff --git a/fern/products/docs/pages/navigation/site-level-settings.mdx b/fern/products/docs/pages/navigation/site-level-settings.mdx index a317d9d76..6267615ab 100644 --- a/fern/products/docs/pages/navigation/site-level-settings.mdx +++ b/fern/products/docs/pages/navigation/site-level-settings.mdx @@ -871,19 +871,15 @@ Toggle the built-in actions on or off with a boolean. **Copy Page** (`copy-page` ### Install skills action -Add an "Install skills" button to the page action bar that opens a modal showing how to install your site's [custom skills](/learn/docs/ai-features/host-skills). The modal displays a copyable install command, the list of available skills, and links to the skill source. +Add an "Install skills" button to the page action bar that opens a modal showing how to install your site's [custom skills](/learn/docs/ai-features/host-skills). The modal displays a copyable install command and the list of available skills. Point `path` at a directory of skills in your repo and Fern publishes them and reads the modal's skill list from the generated manifest: ```yaml docs.yml page-actions: options: skills: + path: ../.agents/skills title: Plant API Skills description: Skills for working with the Plant Store API. - repository: https://github.com/your-org/plant-api-skills - skills: - - name: plant-care - description: Guide for diagnosing and treating common plant diseases. - url: https://github.com/your-org/plant-api-skills/tree/main/plant-care ``` diff --git a/fern/products/docs/snippets/install-skills-properties.mdx b/fern/products/docs/snippets/install-skills-properties.mdx index 0d71c5682..8377fd167 100644 --- a/fern/products/docs/snippets/install-skills-properties.mdx +++ b/fern/products/docs/snippets/install-skills-properties.mdx @@ -2,6 +2,10 @@ Enables the "Install skills" page action and configures the modal it opens. Omit to hide it. + + Path to a directory of agent skills in this repo. Resolved relative to the folder containing `docs.yml`; `../` is allowed (e.g. a repo-root `.agents/skills/` directory that also serves local coding agents). Every subdirectory containing a `SKILL.md` is published as one skill: the CLI validates the bundle, generates the `/.well-known/skills/index.json` discovery manifest, and uploads every file so that `npx skills add https://` works. Nothing is written back to your repo. + + Overrides the modal title. @@ -14,14 +18,10 @@ URL for a "Learn more" link shown alongside the description. - - Source repository URL displayed as a "View source" button in the modal. - - - Command(s) shown as a copyable block in the modal. A single string is shown as-is; an array is joined with newlines for multi-step installs. Defaults to `npx skills add https://` when omitted. + Command(s) shown as a copyable block in the modal. A single string is shown as-is; an array is joined with newlines for multi-step installs. Defaults to `npx skills add https://` when omitted. Set this when the generated command won't work as-is — for example auth-walled docs, a private registry, or a multi-step install. - List of skills displayed in the modal. Each entry has a `name` (required), `description` (optional), and `url` (optional link to the skill source). When the site serves a `/.well-known/agent-skills/index.json` or `/.well-known/skills/index.json` manifest, the manifest replaces this list. + List of skills displayed in the modal. Each entry has a `name` (required), `description` (optional), and `url` (optional link to the skill source). When `path` is set, or the site serves a `/.well-known/skills/index.json` (or `/.well-known/agent-skills/index.json`) manifest, the generated manifest replaces this hand-listed array. From f7c5ed043447a8c31315f297e13ef3764d2aa6ac Mon Sep 17 00:00:00 2001 From: Devin Logan Date: Mon, 22 Jun 2026 11:50:54 -0400 Subject: [PATCH 2/3] remove legacy info --- fern/products/docs/pages/ai/host-skills.mdx | 4 ---- fern/products/docs/pages/changelog/2026-06-22.mdx | 9 +++++++++ 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 fern/products/docs/pages/changelog/2026-06-22.mdx diff --git a/fern/products/docs/pages/ai/host-skills.mdx b/fern/products/docs/pages/ai/host-skills.mdx index 29b3f2197..ae1bf7b44 100644 --- a/fern/products/docs/pages/ai/host-skills.mdx +++ b/fern/products/docs/pages/ai/host-skills.mdx @@ -128,7 +128,3 @@ page-actions: Configure the Install skills button under `page-actions.options.skills` in `docs.yml`. - -## Legacy: hand-built `.well-known/skills/` - -Before `path` existed, you could serve skills by hand-populating a `.well-known/skills/` (or `.well-known/agent-skills/`) directory inside your `fern/` folder, complete with your own `index.json` manifest. Fern still uploads such a directory as-is when no `path` is declared, so existing setups keep working. Prefer `path`: it generates the manifest for you and validates the bundle. If you declare a `path` and also have a hand-built `fern/.well-known/skills/` folder, the declared path takes precedence and Fern warns about the conflict. diff --git a/fern/products/docs/pages/changelog/2026-06-22.mdx b/fern/products/docs/pages/changelog/2026-06-22.mdx new file mode 100644 index 000000000..0a24694ec --- /dev/null +++ b/fern/products/docs/pages/changelog/2026-06-22.mdx @@ -0,0 +1,9 @@ +--- +tags: ["ai"] +--- + +## Host agent skills from a directory in your repo + +The "Install skills" page action now accepts `page-actions.options.skills.path`, pointing at a skills directory in the same repo as your `docs.yml`. The CLI validates the bundle, generates the `/.well-known/skills/index.json` manifest, and publishes it so users can install with `npx skills add https://`. + + From 9658df10250a2a3b37a82b13b6aa1648da2101e6 Mon Sep 17 00:00:00 2001 From: Mathieu Legault Date: Mon, 13 Jul 2026 13:49:00 -0400 Subject: [PATCH 3/3] docs(skills): add fern-docs skill and configure skills path Co-Authored-By: Claude --- .agents/skills/fern-docs/SKILL.md | 154 +++++++++++ .../fern-docs/references/authentication.md | 180 +++++++++++++ .../skills/fern-docs/references/changelog.md | 141 ++++++++++ .../skills/fern-docs/references/products.md | 121 +++++++++ .../skills/fern-docs/references/redirects.md | 98 +++++++ .../skills/fern-docs/references/snippets.md | 90 +++++++ .../fern-docs/references/translations.md | 246 ++++++++++++++++++ fern/docs.yml | 8 +- 8 files changed, 1031 insertions(+), 7 deletions(-) create mode 100644 .agents/skills/fern-docs/SKILL.md create mode 100644 .agents/skills/fern-docs/references/authentication.md create mode 100644 .agents/skills/fern-docs/references/changelog.md create mode 100644 .agents/skills/fern-docs/references/products.md create mode 100644 .agents/skills/fern-docs/references/redirects.md create mode 100644 .agents/skills/fern-docs/references/snippets.md create mode 100644 .agents/skills/fern-docs/references/translations.md diff --git a/.agents/skills/fern-docs/SKILL.md b/.agents/skills/fern-docs/SKILL.md new file mode 100644 index 000000000..3d5e974df --- /dev/null +++ b/.agents/skills/fern-docs/SKILL.md @@ -0,0 +1,154 @@ +--- +name: fern-docs +description: >- + Author and edit Fern documentation: MDX pages, navigation, docs.yml config, + custom components, landing pages, and changelog entries. Use when working in a + Fern docs repo (a fern/ directory with docs.yml). Routes to a detailed + reference per task. +--- + +# Fern docs authoring + +Write and edit documentation on a Fern site. `SKILL.md` is the index: follow the +principles below, then read the `references/` file that matches the +task before writing. Don't work from this page alone when a reference exists. + +**Scope: this skill governs Fern docs content only** — MDX pages, navigation, +`docs.yml` and related config, snippets, changelog entries, custom components, +and landing pages. It does **not** govern repo governance or tooling files that +happen to live in the same repo, such as `CLAUDE.md`, `AGENTS.md`, +`STYLE-GUIDE.md`, and contributor guides. Never apply the principles below — +"one canonical home," "prefer editing over creating," consolidation — to those +files. Leave them alone unless the user explicitly asks you to edit them. + +## Fern resources + +Fern's configuration evolves. Look things up rather than guessing. + +- **Docs:** https://buildwithfern.com/learn/docs/getting-started/overview.md + (append `.md` to any docs URL for clean markdown to fetch) +- **Page index:** https://buildwithfern.com/learn/llms.txt +- **MCP server** — connect your agent to Fern's live docs and query it for any + syntax or behavior you're unsure of. The endpoint works with any MCP client; + for Claude Code: + + ```bash + claude mcp add --transport http fern https://buildwithfern.com/learn/_mcp/server + ``` + +## Routing + +| Task | Read | +|------|------| +| Writing or editing a changelog entry | `references/changelog.md` | +| Setting up a product switcher, adding a product, or refactoring nav into products | `references/products.md` | +| Moving, renaming, or deleting a page, or any edit that changes a published URL | `references/redirects.md` | +| Restricting pages or content by role (RBAC), or wiring docs into an auth method (password, SSO, JWT, OAuth) | `references/authentication.md` | +| Editing the same content across multiple pages, or adding generic/boilerplate content (e.g. "contact support") | `references/snippets.md` | +| Translating pages into another language, adding a language, or updating existing translations (multi-language Fern repo) | `references/translations.md` | + +## Core principles + +- **Defer to the repo's own conventions.** When a docs repo has an established + pattern, voice, or structure — a `CLAUDE.md`, a style guide, or just the + consistent practice of surrounding pages — follow it over the guidance here. + This skill is the default, not an override. +- **Write what the reader needs to succeed — no more.** Every sentence earns its place. +- **Prefer editing over creating.** Search the repo for a page that already + covers the topic and update it instead of adding a duplicate. +- **Make minimal, precise edits.** Don't rewrite a page when a paragraph fix will do. +- **Push back when something seems wrong.** Explain why rather than complying silently. +- **Ask when unclear.** Don't fill gaps with assumptions. +- **Never fabricate.** If you don't know a config key or behavior, look it up + (MCP server or docs) or say so. Don't invent frontmatter or YAML fields. +- **Cross-reference.** When you mention a concept documented elsewhere, link to it + so readers can find their way. +- **Preserve URLs when they change.** Any edit that moves, renames, or deletes a + page — or changes a slug, section, or product — can change a published URL. + When it does, set up [redirects](https://buildwithfern.com/learn/docs/seo/redirects.md) + from the old path (see `references/redirects.md`) so inbound links and search + rankings survive. + +## Links + +Internal links are URL paths derived from **this repo's own `docs.yml` and page +frontmatter** — not file paths on disk or relative paths. Nearly every edit +touches a link, so get this right every time. + +A page URL is `//`: + +- **Base path** — the prefix in your docs URL (e.g. `/learn`, `/docs`; may be empty). +- **Product slug** — the product's `slug` in `fern/docs.yml`. Omitted for a + product with no `slug:` (such as a default single Home product). +- **``** — found by step 1 or step 2 below. + +To find ``: + +1. **Open the target page and check its frontmatter for `slug:`.** A frontmatter + slug is **absolute within the product**: it *is* `` on its own, and it + replaces the entire section/folder hierarchy. The section's slug does not + appear. Do not walk the navigation. +2. **Otherwise, walk `docs.yml`** within the product, from section through any + folders to the page. Each level contributes one slug: + - Its explicit `slug:` if set. + - Otherwise auto-derived from the display name: lowercased, spaces to + hyphens, special characters stripped (so `v3 (Latest)` becomes + `v-3-latest`). Auto-derivation isn't always obvious — verify, don't assume. + - In folder-based navigation, a page's slug comes from its **filename**, not + a display name (`quick-start.mdx` to `quick-start`). + - **Omit any level with `skip-slug: true`** — it contributes no segment. + +Example — page `./pages/ai/overview.mdx` under section "AI features", in product +`docs`, base path `/learn`: + +- with frontmatter `slug: ai-ai` → `/learn/docs/ai-ai` (the `ai-features` + section slug is dropped) +- with no frontmatter slug → `/learn/docs/ai-features/overview` (section + auto-slug `ai-features` + page auto-slug `overview`) + +Section slugs frequently differ from folder names, so never guess the URL from +the directory layout. If you can't resolve a path from `docs.yml` and the page's +frontmatter, say so rather than guessing. + +**Anchors** append `#heading`, and only resolve for real `##`/`###` Markdown +headings — not for JSX title props like ``, ``, +``, or ``. + +**Not internal links** (leave as-is): external `https://` URLs, image paths +(`./images/...`), snippet includes (``), and +same-page anchors (`#section`). + +## Cross-referencing + +When you document new functionality — a new page, or new behavior on an existing +one — related pages usually need a pointer to it, or the new content is hard to +find from where readers actually start. This applies to small edits too, not +just new pages. + +- **One canonical home.** The full explanation lives on one page; every other + page that touches the topic links to it instead of restating it. If you're + copying more than a sentence or two onto another page, it belongs on the + canonical page. +- **Find the targets.** Grep the docs for the feature name and one or two related + keywords, then read each hit and decide whether a pointer is warranted: + + ```bash + grep -rln "\|" fern --include="*.mdx" + ``` + + Common targets: pages for behavior the feature affects, adjacent feature pages, + and overview or landing pages. +- **Use the lightest form that works:** an inline link inside an existing + sentence, then a `` for a secondary aside, then a new `##` section, then + a new page. Inline links and Notes add no maintenance surface; new pages and + sections do. +- **Phrase inline links naturally — never "See [page]."** Put the link on a noun + phrase already in the sentence, not a tacked-on pointer sentence. Don't wrap a + lone link in a `` whose only job is to host it. + - Good: "Each request counts against your [rate limit](...), which resets hourly." + - Bad: "Requests are rate limited. See the [rate limits reference](...) for details." + - Bad: a standalone `` containing only "For more, see [rate limits](...)." +- **Frame by function, not plumbing.** Contrast what each feature does ("X + decides A; Y decides B"), not that they share a config key or sit in the same + directory. If the only thing linking two features is the same YAML key, it's + not worth a cross-reference. diff --git a/.agents/skills/fern-docs/references/authentication.md b/.agents/skills/fern-docs/references/authentication.md new file mode 100644 index 000000000..c86ad86ca --- /dev/null +++ b/.agents/skills/fern-docs/references/authentication.md @@ -0,0 +1,180 @@ +# Authentication & RBAC + +Gate a docs site so readers only see content meant for them. Read this when a +task involves restricting pages or content by role (RBAC), or wiring docs into an +auth method (password, SSO, JWT, OAuth). + +This page is decision context: what each piece is, which surface it lives on, and +**which canonical doc to fetch before you edit**. Don't work the syntax from +memory — fetch the matching page (append `.md` for clean markdown) or query the +Fern MCP server, since the config evolves. + +## First, locate the surface + +Most of an auth setup is *not* in the docs repo. Before touching anything, +work out which surface the task actually lives on — it changes whether you edit a +file at all: + +- **In the repo (`docs.yml` + MDX)** — declaring roles and gating content. This is + RBAC, and it's the bulk of what you'll edit here. +- **In the Fern Dashboard** — password protection (passwords mapped to roles). +- **With Fern support** — SSO, OAuth, and JWT signing secrets. You send Fern your + identity-provider details; Fern provisions the connection. **The repo never + holds client secrets or signing keys** — if a task asks you to put one in + `docs.yml`, push back. + +When a request is really "turn on SSO" or "set the docs password," the answer is a +pointer to the Dashboard or support, not a `docs.yml` edit. Say so rather than +inventing config. + +## RBAC (the in-repo part) + +RBAC has three moving parts, all in the repo. You'll usually touch more than one +in a single task, so know what each is for: + +- **Declare roles** — a top-level `roles:` list in `docs.yml`. Every role + referenced anywhere must be declared here or `fern check` fails. `everyone` is + auto-assigned to all visitors, including unauthenticated ones — but it is *not* + exempt from the declaration rule: if you reference it in `viewers:` or + ``, you must still list `everyone` under `roles:` or `fern check` + fails with "Role 'everyone' is used but not declared." +- **Gate navigation** — a `viewers:` key on a navigation item. Only listed roles + see it. Fern allows `viewers:` on **products, versions, tabs, sections, pages, + api references, and changelogs** — every level of the tree. +- **Gate inline content** — the `` component, to hide part of a + page rather than the whole page. + +Three behaviors that are easy to get backwards — get these right or the gating is +silently wrong: + +- **Omitting `viewers:` is authenticated-only, NOT public.** A navigation item + with no `viewers:` key is visible to *any signed-in user* but hidden from + logged-out visitors. To make something truly public you must set it explicitly: + `viewers: [everyone]`. "No key" and `[everyone]` are different states — don't + treat a bare item as public. +- **Viewership is inherited.** If a section is restricted to `admins`, all its + pages and nested sections are admins-only too — you don't repeat `viewers:` on + each child. So to test or reason about one level in isolation, keep its parents + permissive; otherwise an inherited restriction, not the key you're looking at, + is what's gating. +- **Gated items are hidden, not locked.** By default an unauthorized reader sees + no trace of the item in nav (not a 404 or lock screen), and unauthenticated + users are redirected to login. A "visible but locked" presentation exists but + must be requested from Fern, so don't assume it's on. + +**`viewers:` placement depends on the item.** On most items it sits directly on +the navigation entry, but **on a tab it goes in the `tabs:` map, not on the +`- tab:` entry** in `navigation:`. Putting it on the `- tab:` entry fails +`fern check`. + +**Fetch before editing:** +https://buildwithfern.com/learn/docs/authentication/features/rbac.md — for exact +`roles:`/`viewers:`/`` syntax. + +### Gating a level means that level must exist in the nav + +To gate, say, the tab or version level, the nav must actually contain a tab or a +version — and Fern's nav structure rules vary by file type in ways that bite when +you build a fixture or restructure to add a gate: + +- A **versioned product**'s version files use flat navigation (sections/pages); + **tabs are not supported there** — put tab-level structure in a non-versioned + product, whose product file does support `tabs:`. +- A **versioned product entry** in `docs.yml` needs both a top-level `path:` (the + default version) **and** a `versions:` list. +- An `- api:` reference node validates in a product's flat navigation but is + rejected inside a version file's tab layout. + +These are navigation-structure rules, not RBAC rules, but they surface the moment +you add a gate to a level that wasn't there before. Confirm structure against the +[products](https://buildwithfern.com/learn/docs/configuration/products.md) and +[versions](https://buildwithfern.com/learn/docs/configuration/versions.md) docs +(or `references/products.md`) rather than guessing — getting the wrong shape costs +far more time than looking it up. + +### You can't verify RBAC in local preview + +`fern docs dev` renders the site but does **not** enforce `viewers:` — every +reader is effectively unauthenticated, so restricted content just won't appear and +you can't confirm a role sees what it should. RBAC only takes effect on the +**published** site once an auth method is configured. Plan to verify on the +deployed site (or a preview instance) with real credentials, and tell the user +that local preview can't prove the gating works. + +## Auth methods — which one, and where it's configured + +Four ways to establish *who* a reader is; RBAC then decides what they see. JWT, +OAuth, and SSO all work through a `fern_token` browser cookie. + +| Method | Configured | RBAC | API key injection | Fetch when working on it | +|--------|-----------|------|-------------------|--------------------------| +| Password | Dashboard | ✅ up to 3 roles | ❌ | [`setup/password-protection.md`](https://buildwithfern.com/learn/docs/authentication/setup/password-protection.md) | +| SSO | Fern support | ❌ | ❌ | [`setup/sso.md`](https://buildwithfern.com/learn/docs/authentication/setup/sso.md) | +| OAuth | Fern support | ✅ | ✅ | [`setup/oauth.md`](https://buildwithfern.com/learn/docs/authentication/setup/oauth.md) | +| JWT | You (mint the token) | ✅ | ✅ | [`setup/jwt.md`](https://buildwithfern.com/learn/docs/authentication/setup/jwt.md) | + +Pick by the constraints, not preference: + +- **Password** — quick gate for a small set of audiences. **Capped at three + password-mapped roles**, no API key injection. Passwords (and their role + mapping) are set in the Fern Dashboard → Settings → Password card, not in the + repo. Each Dashboard role name must match a string in `docs.yml`'s `roles:` list + **exactly**, or the password won't unlock the content it's meant to. You don't + map a password to `everyone` — that's the built-in unauthenticated role. +- **SSO** — internal docs behind corporate login (SAML 2.0 / OIDC). No RBAC, no + API key injection — it's all-or-nothing access. +- **OAuth** — Fern runs the flow against your provider; supports RBAC and API key + injection. Choose over JWT when you'd rather not mint tokens yourself. +- **JWT** — you sign the `fern_token` yourself, so you control the payload. The + only method with meaningful repo/code involvement: roles travel in the token's + `fern` claim, and you can inject an API key into the API Explorer through it. + Fetch `setup/jwt.md` for the claim shape and a token-minting example, and + [`features/api-key-injection.md`](https://buildwithfern.com/learn/docs/authentication/features/api-key-injection.md) + for the injection payload schema. + +## Footguns + +- **Declare before you use.** Any role in `viewers:` or `` must be in the + top-level `roles:` list, or `fern check` fails. +- **Bare ≠ public.** An item with no `viewers:` is authenticated-only, not public. + Set `viewers: [everyone]` for anything that should reach logged-out visitors. +- **Inherited restrictions hide more than you think.** Because viewership cascades + down, a permissive-looking child can still be gated by an ancestor. Check the + whole path from product down, not just the item itself. +- **Gating without auth exposes everything.** `viewers:` and `` are + inert until an auth method is configured. On a published site with *no* auth + method, all gated content is publicly visible — *not* hidden, and not locked. + `fern check` passing does not mean content is protected. Never treat `viewers:` + as protection until auth is wired up and verified live. +- **Secrets never live in the repo.** Signing keys, client secrets, and passwords + belong in the Dashboard or with Fern support. +- **Gating ≠ moving.** Hiding a page with `viewers:` doesn't change its URL, so + it's not a redirect situation. Renaming or moving it still is — see + `references/redirects.md`. + +## Verify + +Run `fern check` after editing roles or `viewers:` — it catches undeclared roles +and malformed config. But neither `fern check` nor `fern docs dev` proves the +gating works: visibility is only enforced on the **published** site with an auth +method configured, so a passing check is necessary, not sufficient. + +Validating RBAC end-to-end happens mostly outside the repo. Walk the user through +the full loop and present it as concrete next steps: + +1. **Publish** — `fern generate --docs` (needs `fern login` or a `FERN_TOKEN`; add + `--no-prompt` for non-interactive runs). +2. **Configure an auth method** — declaring and gating in the repo enforces + nothing on its own. For a quick RBAC test, password protection is the fastest + path: in the Dashboard, add one password per role, with role names matching + `docs.yml` exactly. Fetch + https://buildwithfern.com/learn/dashboard/configuration/password-protection.md + for the current Dashboard steps and walk the user through them — don't recite + menus from memory, since the UI changes. +3. **Verify per role** — load the published site and enter each role's credential + in turn, confirming each role sees exactly its slice and *not* another's. Give + the user a role → expected-visible-content table so they know what correct + looks like. + +Always tell the user that local checks can't validate RBAC and that auth must be +live first. diff --git a/.agents/skills/fern-docs/references/changelog.md b/.agents/skills/fern-docs/references/changelog.md new file mode 100644 index 000000000..19e91aa79 --- /dev/null +++ b/.agents/skills/fern-docs/references/changelog.md @@ -0,0 +1,141 @@ +# Writing changelog entries + +A changelog entry announces a shipped change to users. Each entry is a dated +`.mdx` file; the filename date is the title. This reference covers how to *write* +an entry. For setup (folder location, `docs.yml` wiring, RSS, navigation), see +the official docs: https://buildwithfern.com/learn/docs/configuration/changelogs.md + +## File and naming + +- Entries go directly in the `changelog/` folder inside `fern/`. The folder must + be named `changelog` exactly. **No subdirectories** — nested files are silently + ignored. +- One entry per date, named by date. Supported filename formats: `YYYY-MM-DD` + (preferred, sorts cleanly), `MM-DD-YYYY`, `MM-DD-YY`. Always use `.mdx` (Fern + also accepts `.md`, but `.mdx` is the standard and unlocks the component + library). +- Sort order comes from the **filename date**, not frontmatter. A wrong filename + date puts the entry in the wrong position. +- The date is the title. **Do not add an h1.** +- If a file already exists for that date, add your feature as a new `##` section + in it rather than creating a second file. + +## Frontmatter tags + +Every entry needs a `tags` array. Tags drive the changelog's filtering, so use +specific, descriptive tags users would search for — by product area, feature, +release stage, platform, or impact: + +```mdx +--- +tags: ["voice-api", "breaking-change"] +--- +``` + +**Reuse existing tags** for consistency before adding a new one: + +```bash +grep -rh "^tags:" changelog/ | sort | uniq -c | sort -rn +``` + +A near-duplicate tag (`auth` when `security` already exists) fragments the +filter. + +## Entry format + +```mdx +--- +tags: ["search"] +--- + +## Filter search results by section + +You can now scope a search to a single section of your docs. Visitors pick a +section from the search bar, and results are limited to pages under it. + +- Available on all plans. +- Configured per docs instance. + + +``` + +- **One `##` per feature.** No h1. +- **Lead with the capability** — "You can now…" — not the implementation. +- **Keep it short:** 2–6 sentences. Bullets for detail lists, not the whole entry. +- **End with a Button** linking to the relevant docs page (recommended). +- For dashboard/UI changes, give the click-path (**Settings** > **Page access**) + before the Button. + +## The docs-link Button + +The `href` is a docs **URL path** built from `docs.yml`, not a file path: +`//`. + +```mdx + href="/changelog/../pages/search/configuration" + href="/docs/search-config/configuration" + href="/docs/customization/search" +``` + +See the **Links** section of `SKILL.md` for how to construct the path (base +path, product slug, frontmatter slugs, `skip-slug`) from this repo's `docs.yml`. + +## Voice + +Dry and direct. Cut hedges ("just", "simply", "make sure you") and filler ("in +order to", "so that"). At most one em dash per short paragraph. + +## Options + +Beyond a plain entry, the changelog supports: + +- **Overview page** — an `overview.mdx` in the `changelog/` folder renders above + all entries. `overview.mdx` is a reserved filename, so it is never treated as a + dated entry. +- **Components are available but avoid them.** `.mdx` entries *can* use Fern's + component library (``, ``, ``, etc.), but stacked across + a list of short entries they create visual noise. House rule: prose + the + `