A page is not finished when the MDX file exists. It also needs a navigation entry, and a move or deletion needs a redirect. CI enforces both.
Read AGENTS.md for the navigation map, the style guide, and the frontmatter
rules. This skill covers the procedure around them and does not repeat them.
Step 1. Choose the source directory
Use the "Source directory summary" table in AGENTS.md to go from subject to
directory. Two traps:
- Directory names do not match navigation names.
src/langsmith/fleet/appears as "No-code agents";src/langsmith/managed-deep-agents*.mdxappears under Build, not under a LangSmith menu. - Lifecycle menus mix products. Build draws from both
src/oss/andsrc/langsmith/; Test, Deploy, and Monitor all draw fromsrc/langsmith/.
Never write to build/. It is Mintlify output, regenerated by make build.
Step 2. Write the page
Required frontmatter:
---
title: Clear, concise page title
description: SEO summary with no markdown, no links, and no backticks
---
For OSS pages that differ by language, use :::python and :::js fences in one
file rather than writing two files. The build pipeline emits both versions.
Before writing a JSX tag you have not seen in src/, look it up on
Mintlify components or with the
docs-mintlify MCP server. A plausible name is not evidence: <Important> and
<ItemTable> both reached main and rendered as nothing.
Take every fact on the page from the product source, not from memory, a sibling
docs page, or the request that asked for the page. This covers behavior,
defaults, field names, version minimums, and examples. Use the
verify-against-source skill to find the repository that owns each claim. Never
fabricate a value to fill a gap. Leave the claim out, and list what you could
not confirm in the pull request body and in your reply to the requester.
Step 3. Add the navigation entry
Navigation lives in src/docs.json under navigation.products. There are two
products: products[0] is AGENT DEVELOPMENT LIFECYCLE (Home, Build, Test,
Deploy, Monitor) and products[1] is PRODUCTS AND SETUP (LangSmith setup, LLM
Gateway, No-code agents, Engine, Deep Agents Code).
Each menu item is addressed by its item key, then nests one of two ways:
menu[].tabs[].pages[]for most menu items.menu[].dropdowns[].tabs[].pages[]for Build, which has Python and TypeScript dropdowns.
A pages array holds page-path strings and {"group": ..., "pages": [...]}
objects, nested to any depth.
Three rules:
- A language-versioned OSS page needs two entries. Build page paths carry
the language segment (
oss/python/deepagents/overviewandoss/javascript/deepagents/overview), so one new page means one entry in the Python dropdown and one in the TypeScript dropdown. Omitting the TypeScript entry is the most common miss. - Page paths omit the
src/prefix and the file extension.src/langsmith/sandboxes.mdxis"langsmith/sandboxes". - A new group leads with an index page:
"pages": ["group/index", "group/page"].
Integration pages are the exception. Add them to the component's index.mdx
instead, and touch docs.json only when creating a brand-new component group.
Step 4. Add redirects for a move, rename, or deletion
For a move or rename of a file that was already on main, run the repo's mover first. It rewrites cross-references
across the corpus, which hand-editing misses: every link to the page in every
form (/oss/langchain/x, /oss/python/langchain/x, href="..."), plus the
src/docs.json nav entries and redirect destinations that name it:
uv run docs mv src/langsmith/old-name.mdx src/langsmith/new-name.mdx --dry-run
Read any Left ... links unchanged warning in the output. It marks links the
mover could not rewrite safely, such as an unversioned /oss/integrations/...
link that also reaches the other language's page. Fix those by hand.
Drop --dry-run once the preview looks right. The mover does not add a
redirect, so add one to the redirects array in src/docs.json:
{ "source": "/langsmith/old-name", "destination": "/langsmith/new-name" }
Redirect paths are site paths and start with /. A language-versioned page
needs a redirect per language, plus one for the unversioned path if the old URL
had one.
scripts/check_removed_pages_redirects.py runs in CI and fails the PR when a
page leaves the navigation and its source file is gone with no redirect. The
same script fails when docs.json names a page whose file does not exist, so a
typo in a page path is caught there rather than at build time.
Step 4b. Two traps that fail silently
Extract a snippet once a block appears on three pages
AGENTS.md covers how to add a snippet. The rule for when: the same block
repeated on three or more pages becomes one file under src/snippets/. A status
callout duplicated across a page family means the wording change that retires it
is an edit to every page in the family, and one will be missed.
Verify a new snippet reaches the build. The pipeline rewrites snippet imports to
language-specific paths, so /snippets/langsmith/x.mdx becomes
/snippets/python/langsmith/x.mdx in the output, and a missing target renders as
nothing at all rather than as an error:
ls build/snippets/python/langsmith/<name>.mdx build/snippets/javascript/langsmith/<name>.mdx
Editing a heading moves its anchor
A heading's slug is derived from its text, so rewording one silently breaks every
#anchor link pointing at it, including links from other pages and entries in
src/docs.json. Grep before editing:
grep -rn 'use-with-the-langsmith-gateway' src/ --include=*.mdx --include=*.json
Changing only capitalization is safe, because slugs are lowercased. Changing a
word is not, and needs either a reworded inbound link or a redirect.
<Step> and <Accordion> accept an explicit id, which is how to keep a
landing spot that is no longer a heading.
Step 5. Verify
Run all three, in this order:
make lint_prose FILES="src/path/to/page.mdx"
make check-mdx
make broken-links-with-anchors
When make build fails with Required uv version >=0.9.26 does not match the running version, the local uv has drifted from the one pyproject.toml
expects. Run the pipeline directly rather than working around the build:
PYTHONPATH="$(pwd)" .venv/bin/python -m pipeline build
A fresh git worktree has no .venv, and the same version check blocks
uv sync from creating one. Point at the main checkout's interpreter instead;
PYTHONPATH still loads the pipeline code from the worktree:
PYTHONPATH="$(pwd)" <main-checkout>/.venv/bin/python -m pipeline build
A fresh worktree's .mise.toml is also untrusted, so the first commit fails
at the Vale hook with Config files in <dir>/.mise.toml are not trusted. Run
mise trust once in the worktree. The docs-edit skill covers the permanent
fix.
make broken-links-with-anchors depends on build, so it fails the same way.
Its link-check half runs on its own once the build output exists:
cd build && mint broken-links --check-anchors | tee /tmp/bl.txt
cd .. && python3 scripts/filter_mint_broken_links.py --check-anchors --input /tmp/bl.txt
Read make broken-links-with-anchors output by skipping to the ⎿ lines. Those are the only
real failures. A bare filename with no indented lines beneath it is an
OpenAPI-generated page that exists at deploy time but not locally.
Fix every Vale finding. CI blocks on lint_prose, and its most common failure is
a spaced em dash (word — word must be word—word).
Step 6. Review the prose
Once the edit is complete and before committing, invoke the docs-review skill
on the files this pass changed. It runs in working-tree mode, so it needs no
checkout, and it covers the style-guide rules Vale cannot see: passive voice,
filler, product versus common noun capitalization, structure conventions, and
link text.
Run it on finished edits only. A review of a half-written section produces findings that go stale as soon as writing resumes.
Skip this step for a change too small to have prose in it, such as a pure
docs.json reorder or a redirect-only fix.
Step 7. Open the pull request
Commit, push, and open the PR however you usually do. If the change belongs on
an existing PR, use docs-edit instead. Two rules apply to the PR description:
- Linear: When the work has a Linear issue, link it with
Closes [DOC-N](<linear url>)orRefs [DOC-N](<linear url>)as appropriate.sync-linear-issue-from-pr.ymlonly syncs status for docs-release-watch PRs labeledrelease-watch-agent, so a human PR does not move the issue. - AI disclosure: End the description with one plain sentence, such as
Prepared with Claude Code.Do not add a🤖 Generated with [Claude Code](...)badge or any similar badge.check-pr-ai-attributionfails a PR that carries one, because the squash merge copies it into the commit onmain.
Checklist
- File in the directory the source-directory table names, not
build/. - Frontmatter present,
descriptionfree of markdown. - Every claim checked against product source; unconfirmed claims cut and listed in the PR body.
-
src/docs.jsonentry in the right product, menu item, tab, and group. - Both language entries added if the page is language-versioned.
- Index page first if a new group was created.
- Redirect added for every moved, renamed, or deleted path.
- Inbound
#anchorlinks checked before any heading was reworded. - A block now on three or more pages extracted to
src/snippets/, and its builtpython/andjavascript/targets confirmed to exist. -
make check-mdxclean. -
make lint_proseclean,make broken-links-with-anchorsshows no new⎿lines. -
docs-reviewrun on the changed files, findings addressed. - PR description references the Linear issue, if any, and has no AI badge.