diff --git a/documentation/AGENTS.md b/documentation/AGENTS.md index 3836cc800..64ead62e3 100644 --- a/documentation/AGENTS.md +++ b/documentation/AGENTS.md @@ -30,6 +30,9 @@ 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. @@ -37,8 +40,11 @@ maintainer can publish out of band by dispatching the sync workflow from `main`. 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 diff --git a/documentation/fern-slugs.lock.json b/documentation/fern-slugs.lock.json index 64b775369..cc9b15dfb 100644 --- a/documentation/fern-slugs.lock.json +++ b/documentation/fern-slugs.lock.json @@ -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" }, diff --git a/scripts/documentation-integrity-hook.sh b/scripts/documentation-integrity-hook.sh index f0ca7b357..4d6ebe690 100755 --- a/scripts/documentation-integrity-hook.sh +++ b/scripts/documentation-integrity-hook.sh @@ -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: diff --git a/scripts/fern/build.ts b/scripts/fern/build.ts index c6ccd9c9b..c32023e53 100644 --- a/scripts/fern/build.ts +++ b/scripts/fern/build.ts @@ -87,7 +87,20 @@ 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, @@ -95,7 +108,6 @@ export function buildBundle(options: BuildOptions): BuildResult { }) } } - // A document missing from the lock has never been published; that is expected for a new page. } const reconciled = reconcileLock(lock, docs) diff --git a/scripts/validate-fern-export.ts b/scripts/validate-fern-export.ts index a620b89aa..3d721a056 100644 --- a/scripts/validate-fern-export.ts +++ b/scripts/validate-fern-export.ts @@ -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 ] */ diff --git a/skills/docs-site-publishing/SKILL.md b/skills/docs-site-publishing/SKILL.md new file mode 100644 index 000000000..758b25ec6 --- /dev/null +++ b/skills/docs-site-publishing/SKILL.md @@ -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. diff --git a/skills/docs-site-publishing/package.json b/skills/docs-site-publishing/package.json new file mode 100644 index 000000000..7ac80d2c3 --- /dev/null +++ b/skills/docs-site-publishing/package.json @@ -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" + ] +} diff --git a/skills/optimization-guide-authoring/SKILL.md b/skills/optimization-guide-authoring/SKILL.md index b8df5e113..30423cbb5 100644 --- a/skills/optimization-guide-authoring/SKILL.md +++ b/skills/optimization-guide-authoring/SKILL.md @@ -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. @@ -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, @@ -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 `, `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 `, `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. diff --git a/skills/optimization-guide-authoring/references/authoring-checklist.md b/skills/optimization-guide-authoring/references/authoring-checklist.md index 29a226bf7..6238c0aad 100644 --- a/skills/optimization-guide-authoring/references/authoring-checklist.md +++ b/skills/optimization-guide-authoring/references/authoring-checklist.md @@ -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 @@ -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.