Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions documentation/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,21 @@ release-gated, so an edit here reaches the public site at the next release rathe
maintainer can publish out of band by dispatching the sync workflow from `main`. Run
`pnpm fern:check` after editing either directory.

- A document is published only if it is listed in its group README frontmatter `children:`. The
exporter reads that list and never scans the filesystem, so a file on disk but absent from
`children:` is simply not published rather than an error.
- Every published document needs a `fern:` frontmatter block: `slug`, `section` (`Guides`,
`Concepts`, or `Migration guides`), and `description`. Add `navTitle` only when the sidebar needs a
shorter label than the page title.
- The published page title is the document's `# ` heading. There is no `fern.title`, so the title
cannot drift from the heading; `pnpm fern:check` rejects one if it is reintroduced, and rejects an
authored top-level `title` that no longer matches the heading.
- `fern.slug` is data and is never derived from a heading, so rewording an `#` heading cannot move a
live URL. Changing a slug requires `pnpm docs:fern -- --update-lock`, which records a permanent
redirect in `documentation/fern-slugs.lock.json`. `pnpm fern:check` fails on an unrecorded change.
live URL. Both a new slug and a changed one are recorded with `pnpm docs:fern -- --update-lock`,
which writes `documentation/fern-slugs.lock.json` — appending a permanent redirect when a slug
moved. Commit that lock diff with the document; `pnpm fern:check` fails on any slug the lock does
not record. The sync workflow runs `pnpm docs:fern` without `--update-lock`, so an entry that is
never committed never appears, and the page's first slug reword would move a live URL silently.
- Cross-document links must resolve to a published document, and a `#fragment` must match a real
heading on the target page. Never link a published document to `authoring/` or `internal/`.
- Do not hand-edit the pages in `contentful-docs`; they are generated and the next sync overwrites
Expand Down
1 change: 1 addition & 0 deletions documentation/fern-slugs.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"documentation/guides/migrating-experience-js-plugins-and-preview.md": "migrate-experiencejs-plugins-and-preview",
"documentation/guides/migrating-experience-js-react-to-react-web.md": "migrate-experiencejs-to-react-web",
"documentation/guides/migrating-experience-js-to-the-web-sdk.md": "migrate-experiencejs-to-web-sdk",
"documentation/guides/migrating-optimization-sdk-packages-from-v1-to-v2.md": "migrate-optimization-sdk-v1-to-v2",
"documentation/guides/rendering-personalized-nextjs-routes-with-static-isr-and-edge-handoffs.md": "render-personalized-nextjs-routes",
"documentation/guides/using-contentful-graphql-data-with-the-optimization-sdks.md": "use-contentful-graphql-data-with-the-optimization-sdks"
},
Expand Down
17 changes: 11 additions & 6 deletions scripts/documentation-integrity-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,30 @@ cwd="$(printf '%s' "$input" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*

# Only spend time when something the validators inspect changed in this working tree.
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
changed="$(git status --porcelain -- documentation/internal/sdk-knowledge documentation/internal/migration-knowledge documentation/authoring documentation/guides skills scripts/validate-guide-authoring.ts scripts/validate-sdk-knowledge.ts 'packages/**/src/**' 2>/dev/null)"
changed="$(git status --porcelain -- documentation/internal/sdk-knowledge documentation/internal/migration-knowledge documentation/authoring documentation/guides documentation/concepts documentation/fern-slugs.lock.json skills scripts/validate-guide-authoring.ts scripts/validate-sdk-knowledge.ts scripts/validate-fern-export.ts scripts/fern 'packages/**/src/**' 2>/dev/null)"
[ -z "$changed" ] && exit 0
fi

# Run both validators even when the first fails so the agent receives the complete status.
# Run every validator even when an earlier one fails so the agent receives the complete status.
# `fern:check` is here because the publishing contract it enforces — a document listed in its group
# README `children:`, a recorded slug, resolvable links, safe MDX — is what decides whether an edit
# reaches the public site at all, and the ways it breaks are silent rather than loud.
knowledge_status=0
guide_status=0
fern_status=0
knowledge_output="$(pnpm --silent knowledge:check 2>&1)" || knowledge_status=$?
guide_output="$(pnpm --silent guides:check 2>&1)" || guide_status=$?
if [ "$knowledge_status" -eq 0 ] && [ "$guide_status" -eq 0 ]; then
fern_output="$(pnpm --silent fern:check 2>&1)" || fern_status=$?
if [ "$knowledge_status" -eq 0 ] && [ "$guide_status" -eq 0 ] && [ "$fern_status" -eq 0 ]; then
exit 0
fi

# Cap the report so a large failure cannot flood the context window.
max_lines=40
capped="$(printf '%s\n%s\n' "$knowledge_output" "$guide_output" | head -n "$max_lines")"
[ "$(printf '%s\n%s\n' "$knowledge_output" "$guide_output" | wc -l)" -gt "$max_lines" ] &&
capped="$(printf '%s\n%s\n%s\n' "$knowledge_output" "$guide_output" "$fern_output" | head -n "$max_lines")"
[ "$(printf '%s\n%s\n%s\n' "$knowledge_output" "$guide_output" "$fern_output" | wc -l)" -gt "$max_lines" ] &&
capped="${capped}
… (truncated; run \`pnpm knowledge:check\` and \`pnpm guides:check\` to see all problems)"
… (truncated; run \`pnpm knowledge:check\`, \`pnpm guides:check\`, and \`pnpm fern:check\` to see all problems)"

reason="Documentation integrity checks are failing. Fix these before finishing:

Expand Down
16 changes: 14 additions & 2 deletions scripts/fern/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,27 @@ export function buildBundle(options: BuildOptions): BuildResult {
const { slugs } = lock
for (const doc of docs) {
const { [doc.relPath]: previous } = slugs
if (previous !== undefined && previous !== doc.fern.slug) {
// An unrecorded document is what a brand-new page looks like, and it publishes fine without an
// entry because `reconcileLock` supplies one in memory. Requiring the entry anyway is what buys
// the page slug protection: the check below can only compare against a slug the lock names, and
// the sync workflow runs `pnpm docs:fern` without `--update-lock`, so an entry the author never
// commits never appears. Without it, the page's first slug reword moves a live URL silently.
if (previous === undefined) {
problems.push({
file: doc.relPath,
line: 1,
message: `fern.slug "${doc.fern.slug}" is not recorded in documentation/fern-slugs.lock.json; rerun \`pnpm docs:fern -- --update-lock\` to record it`,
})
continue
}
if (previous !== doc.fern.slug) {
problems.push({
file: doc.relPath,
line: 1,
message: `fern.slug changed "${previous}" -> "${doc.fern.slug}"; rerun \`pnpm docs:fern -- --update-lock\` to record the redirect`,
})
}
}
// A document missing from the lock has never been published; that is expected for a new page.
}

const reconciled = reconcileLock(lock, docs)
Expand Down
3 changes: 2 additions & 1 deletion scripts/validate-fern-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* This is the gate that makes the link rot the manual process accumulated impossible to reintroduce:
* every cross-document link must resolve to a published page, and every `#fragment` must match a real
* heading on that page. It also enforces the MDX safety rule that catches leaked markup, and refuses
* an unrecorded slug change so a live URL can never move silently.
* any slug the lock does not record — a new page's as well as a changed one's — so a live URL can
* never move silently.
*
* Usage: pnpm fern:check [-- --ref <git-ref>]
*/
Expand Down
64 changes: 64 additions & 0 deletions skills/docs-site-publishing/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
name: docs-site-publishing
description: >-
Wire a document under documentation/guides/ or documentation/concepts/ into the published Contentful
documentation site. Use when adding, renaming, moving, reordering, or removing a published document,
when editing a `fern:` frontmatter block, a group README `children:` list, or
documentation/fern-slugs.lock.json, and when `pnpm fern:check` fails.
argument-hint: '[document, group README, or the failing fern:check output]'
paths: documentation/guides/**, documentation/concepts/**, documentation/fern-slugs.lock.json
---

# Publishing authored docs to the documentation site

Writing a document does not publish it. Publication is data, and two of its four requirements fail
silently — this skill exists for those two.

**Sources of truth** — read rather than restate:

- [`documentation/AGENTS.md`](../../documentation/AGENTS.md) "Publishing to the documentation site" —
the rules: the `fern:` block, title-from-heading, slugs, links, MDX safety.
- [`docs/ADRs/0002`](../../docs/ADRs/0002-generate-the-public-documentation-site-content-from-this-repository.md)
— why the exporter is shaped this way, and the three-layer sync.

## The two silent failures

**A document absent from its group README `children:` is not published.** The exporter reads that
list and never scans the filesystem, so the page is invisible rather than an error. Nothing fails; the
page simply never exists on the site.

**`children:` order is the sidebar order, and it is global across the group, not per section.** A
page's position inside its `fern.section` comes from its index in the one `children:` list — so
inserting a `Migration guides` page second in `documentation/guides/README.md` puts it _first_ in the
Migration guides sidebar section. Place it where it should land in its own section.

## Recording the slug

`pnpm docs:fern -- --update-lock`, then commit the lock diff **with the page**. This covers both a new
page and a changed slug; only the latter appends a redirect. Never hand-edit the lock.

A new page publishes without a lock entry, so this is easy to skip — but the slug then has no
protection, because the change check can only compare against a slug the lock already names. `pnpm
fern:check` rejects an unrecorded slug for that reason.

## Checks

`pnpm fern:check` enforces the whole contract and reports `file:line: message` naming the fix. It also
runs from the `Stop` hook whenever published docs change, so a missed step surfaces without being
asked for.

When a page is new or its slug, section, or order changed, run `pnpm docs:fern` and read
`fern-bundle/nav-block.yaml` — that is the artifact that ships, so it settles ordering questions that
reasoning about `children:` indices does not.

## Reporting

Publication is **release-gated**: an edit reaches the site at the next release, not on merge. Say
which state you mean, and say whether the page is wired into `children:` and whether the lock entry
was recorded — a reader cannot tell from "docs updated".

## Not in scope

Guide prose and structure (`optimization-guide-authoring`), migration routing
(`migration-guide-authoring`), fact derivation (`sdk-knowledge-authoring`), and the
`contentful-docs` repository's own conventions.
Comment on lines +62 to +64

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing migration-knowledge-authoring in scope list

The Not in scope section now lists only sdk-knowledge-authoring for fact derivation, but the previous version also referenced migration-knowledge-authoring for migration fact derivation. If that skill still exists, readers with migration fact questions may be directed to the wrong skill.

Code suggestion
Check the AI-generated fix before applying
Suggested change
Guide prose and structure (`optimization-guide-authoring`), migration routing
(`migration-guide-authoring`), fact derivation (`sdk-knowledge-authoring`), and the
`contentful-docs` repository's own conventions.
Guide prose and structure (`optimization-guide-authoring`), migration routing
(`migration-guide-authoring`), SDK or migration fact derivation (`sdk-knowledge-authoring`,
`migration-knowledge-authoring`), and the
`contentful-docs` repository's own conventions.
Suggested based on your custom review guideline "Repo Truth And Boundaries"

Code Review Run #876303


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

9 changes: 9 additions & 0 deletions skills/docs-site-publishing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@contentful/skill-docs-site-publishing",
"version": "0.1.0",
"description": "Wire authored Optimization SDK docs into the published Contentful documentation site",
"license": "MIT",
"files": [
"SKILL.md"
]
}
27 changes: 19 additions & 8 deletions skills/optimization-guide-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ go deeper. Two consequences drive everything below:
owns its Quick-start contract, Milestone contract, `###` inventory/order/category, and what each
section must teach or show. Shared wording lives in `documentation/authoring/fragments/`. Open both
recipe and blueprint before drafting.
- **Site publishing wiring** — the `fern:` frontmatter block, the group README `children:` manifest,
the slug lock, sidebar order, MDX safety, and the release-gated sync belong to
`docs-site-publishing`. This skill decides what a guide says; that one decides whether it ships.
- Concept docs under `documentation/concepts/` — they own deeper mechanics; guides link to them.
- Package READMEs, implementation READMEs, and product docs.
- Generated TypeDoc under `docs/` — it owns exhaustive, method-by-method API reference.
Expand Down Expand Up @@ -111,6 +114,10 @@ procedure previews. Put package tradeoffs in `choosing-the-right-sdk.md` and run
integration guides. Keep the listing order: Node, Web, React Web, Next.js App Router, Next.js Pages
Router, React Native, iOS SwiftUI, iOS UIKit, Android Compose, Android Views.

The README is also the publishing manifest: its frontmatter `children:` list is the only thing the
site exporter reads, and its order is the published sidebar order. A guide missing from `children:` is
not published at all. See `docs-site-publishing`.

## Workflow

1. **Identify the archetype and reader goal.** Open the matching recipe. For an integration guide,
Expand Down Expand Up @@ -163,14 +170,18 @@ Router, React Native, iOS SwiftUI, iOS UIKit, Android Compose, Android Views.

6. **Sync the TOC and anchors**, add `## Production checks` and (if there are known failure modes)
`## Troubleshooting`, and link the reference implementation READMEs.
7. **Self-review** against [references/authoring-checklist.md](references/authoring-checklist.md).
8. **Validate**: run `pnpm exec prettier --write <file>`, `pnpm guides:check`, and
`pnpm knowledge:check`; confirm the collapsible TOC anchors resolve.
9. **Review new or substantially rewritten guides independently.** A newcomer reviewer checks that
the target reader can perform and verify the guide. A technical-foundation reviewer checks
interfaces against `packages/**/src` and behavior against the knowledge base. Resolve blocker and
high-severity findings before acceptance; the writer does not sign off its own draft.
10. **Report the result.** Return the edited guide path and a short summary of what changed and why.
7. **Wire the page for publication**, following `docs-site-publishing`. A guide that is written but
not wired is invisible on the public site, and that failure is silent. On a refresh, a reworded
`#` heading is never a reason to touch `fern.slug`.
8. **Self-review** against [references/authoring-checklist.md](references/authoring-checklist.md).
9. **Validate**: run `pnpm exec prettier --write <file>`, `pnpm guides:check`, `pnpm knowledge:check`,
and `pnpm fern:check`; confirm the collapsible TOC anchors resolve.
10. **Review new or substantially rewritten guides independently.** A newcomer reviewer checks that
the target reader can perform and verify the guide. A technical-foundation reviewer checks
interfaces against `packages/**/src` and behavior against the knowledge base. Resolve blocker and
high-severity findings before acceptance; the writer does not sign off its own draft.
11. **Report the result.** Return the edited guide path and a short summary of what changed and why,
including whether the page is wired for publication and that publication is release-gated.

For a new guide, draft from the recipe and blueprint. Include every required quick-start artifact and
satisfy every “Must teach or show” / “Must route to” item; matching headings alone is incomplete.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Run before finishing any guide edit. Assertions are written to be mechanically checkable so they can
also back a future validation hook on `documentation/guides/**`. Group A applies to all guides; B–F
add per-archetype checks.
add per-archetype checks; G covers the wiring that decides whether the guide reaches the public site.

## A. All guides

Expand Down Expand Up @@ -282,3 +282,18 @@ add per-archetype checks.
- [ ] No fastest-path column, setup summary, tradeoff matrix, or procedure preview.
- [ ] Listing order: Node, Web, React Web, Next.js App Router, Next.js Pages Router, React Native,
iOS SwiftUI, iOS UIKit, Android Compose, Android Views.

## G. Site publishing wiring (every published guide)

`pnpm fern:check` mechanically enforces the publishing contract — the `fern:` block's shape, slug
format, link and anchor resolution, MDX safety, and that the slug is recorded. Run it and fix what it
reports; those rules are deliberately **not** repeated here. What it cannot check is intent, which is
all this section asks about. See `docs-site-publishing`.

- [ ] Its index in `children:` places it where it should appear **within its own `fern.section`** —
ordering is global across the group, not per section, so position N in the one `guides/` list
decides where the page lands inside its section.
- [ ] `fern.slug` is unchanged unless the URL is **deliberately** moving. A reworded `#` heading is
never a reason to change it.
- [ ] The `documentation/fern-slugs.lock.json` diff is committed **with** the guide, not left behind
in a follow-up.
Loading