mirror of
https://github.com/NVIDIA/NemoClaw.git
synced 2026-07-03 03:37:16 +00:00
docs: hide generated variant pages (#4724)
## Summary Moves generated OpenClaw/Hermes lifecycle docs out of the source directory and into the ignored docs build tree. This keeps contributor-facing source folders free of generated page copies while preserving Fern's native rendering for variant pages. ## Changes - Generate variant pages under `docs/_build/` instead of beside the original docs file. - Rewrite relative links when generating variant pages so links resolve from the `_build` output location. - Point Fern navigation at the hidden generated pages and document the variant-generation flow. - Extend the focused variant docs test to cover rewritten relative links. ## Type of Change - [ ] Code change (feature, bug fix, or refactor) - [x] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Verification `git commit --no-verify` and `git push --no-verify` were used at the user's request. - [ ] `npx prek run --all-files` passes - [ ] `npm test` passes - [x] Tests added or updated for new or changed behavior - [x] No secrets, API keys, or credentials committed - [x] Docs updated for user-facing behavior changes - [ ] `npm run docs` builds without warnings (doc changes only) - [x] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) Verified locally: - `npm test -- test/agent-variant-docs.test.ts` - `npm run docs` (passes with existing Fern warning) --- Signed-off-by: Miyoung Choi <miyoungc@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced agent variant documentation generation with automatic link rewriting to ensure correct navigation in generated pages. * **Documentation** * Added "Agent Variant Generation" section to contributing guidelines explaining how to create shared documentation that generates multiple variant versions. * **Tests** * Updated test cases to validate variant-specific placeholder rendering and link path rewriting functionality. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
93c3903cef
commit
d798d5ee51
5 changed files with 100 additions and 12 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,7 +7,6 @@ __pycache__/
|
|||
coverage/
|
||||
dist/
|
||||
docs/_build/
|
||||
docs/**/*.generated.mdx
|
||||
node_modules/
|
||||
|
||||
# OS metadata
|
||||
|
|
|
|||
|
|
@ -142,6 +142,21 @@ The preview watcher uses the current Git branch name as the Fern preview ID and
|
|||
|
||||
Fern `.mdx` pages are the source for generated user skills. Legacy `.md` pages may remain temporarily for parity checks, but release-prep skill generation should pass `--doc-platform fern-mdx`.
|
||||
|
||||
## Agent Variant Generation
|
||||
|
||||
Some Fern pages appear in both the OpenClaw and Hermes guide variants.
|
||||
When the page content is the same except for the host CLI binary, write one source page and use `$$nemoclaw` as a build-time placeholder.
|
||||
Do not duplicate fenced code blocks or inline command examples only to switch between `nemoclaw` and `nemohermes`.
|
||||
|
||||
The `scripts/sync-agent-variant-docs.ts` script renders variant-specific pages before Fern validates or publishes the site.
|
||||
For the sandbox lifecycle guide, the source page remains at `docs/manage-sandboxes/lifecycle.mdx`.
|
||||
The generated OpenClaw and Hermes pages are written under `docs/_build/agent-variants/`, which is ignored by Git.
|
||||
Navigation in `docs/index.yml` points Fern at those generated pages so Fern still renders normal fenced code blocks with copy buttons and syntax highlighting.
|
||||
|
||||
Run `npm run docs:sync-agent-variants` after editing a shared variant source page.
|
||||
Run `npm run docs` before opening a PR to verify the generated pages, rewritten relative links, and Fern navigation.
|
||||
If content differs by behavior, setup flow, state layout, or agent-specific wording, keep using `<AgentOnly>` blocks for that content.
|
||||
|
||||
## Doc-Only PR Verification
|
||||
|
||||
Doc-only pull requests do not need the full test suite by default.
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ navigation:
|
|||
collapsed: open-by-default
|
||||
contents:
|
||||
- page: "Manage Sandbox Lifecycle"
|
||||
path: manage-sandboxes/lifecycle.openclaw.generated.mdx
|
||||
path: _build/agent-variants/manage-sandboxes/lifecycle.openclaw.generated.mdx
|
||||
slug: lifecycle
|
||||
- page: "Runtime Controls"
|
||||
path: manage-sandboxes/runtime-controls.mdx
|
||||
|
|
@ -222,7 +222,7 @@ navigation:
|
|||
collapsed: open-by-default
|
||||
contents:
|
||||
- page: "Manage Sandbox Lifecycle"
|
||||
path: manage-sandboxes/lifecycle.hermes.generated.mdx
|
||||
path: _build/agent-variants/manage-sandboxes/lifecycle.hermes.generated.mdx
|
||||
slug: lifecycle
|
||||
- page: "Runtime Controls"
|
||||
path: manage-sandboxes/runtime-controls.mdx
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
|
|||
const sourcePath = path.join(repoRoot, "docs/reference/commands.mdx");
|
||||
const targetPath = path.join(repoRoot, "docs/reference/commands-nemohermes.mdx");
|
||||
const lifecyclePath = path.join(repoRoot, "docs/manage-sandboxes/lifecycle.mdx");
|
||||
const generatedDocsRoot = path.join(repoRoot, "docs/_build/agent-variants");
|
||||
const agentVariants = ["openclaw", "hermes"] as const;
|
||||
|
||||
type AgentVariant = (typeof agentVariants)[number];
|
||||
|
|
@ -16,6 +17,10 @@ type RenderedFile = {
|
|||
path: string;
|
||||
contents: string;
|
||||
};
|
||||
type RenderAgentVariantOptions = {
|
||||
outputPath?: string;
|
||||
sourcePath?: string;
|
||||
};
|
||||
|
||||
const GENERATED_NOTICE =
|
||||
"{/* This file is generated from docs/reference/commands.mdx by scripts/sync-agent-variant-docs.ts. Run `npm run docs:sync-agent-variants` to regenerate it. Do not edit by hand. */}";
|
||||
|
|
@ -118,9 +123,13 @@ function stripAgentOnlyBlocksForVariant(body: string, activeVariant: AgentVarian
|
|||
);
|
||||
}
|
||||
|
||||
export function renderAgentVariantPage(source: string, variant: AgentVariant): string {
|
||||
export function renderAgentVariantPage(
|
||||
source: string,
|
||||
variant: AgentVariant,
|
||||
options: RenderAgentVariantOptions = {},
|
||||
): string {
|
||||
const { frontmatter, body } = splitFrontmatter(source);
|
||||
const renderedBody = stripAgentOnlyBlocksForVariant(
|
||||
let renderedBody = stripAgentOnlyBlocksForVariant(
|
||||
body.replace(/^import \{ AgentOnly \} from "\.\.\/_components\/AgentGuide";\n\n?/m, ""),
|
||||
variant,
|
||||
)
|
||||
|
|
@ -128,17 +137,67 @@ export function renderAgentVariantPage(source: string, variant: AgentVariant): s
|
|||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trimStart();
|
||||
|
||||
if (options.sourcePath && options.outputPath) {
|
||||
renderedBody = rewriteRelativeMarkdownLinks(
|
||||
renderedBody,
|
||||
path.dirname(options.sourcePath),
|
||||
path.dirname(options.outputPath),
|
||||
);
|
||||
}
|
||||
|
||||
return `${frontmatter}${GENERATED_VARIANT_NOTICE}\n\n${renderedBody}`.replace(/\s*$/, "\n");
|
||||
}
|
||||
|
||||
function renderGeneratedAgentVariantPages(): RenderedFile[] {
|
||||
const source = readFileSync(lifecyclePath, "utf8");
|
||||
const sourceDirectory = path.dirname(lifecyclePath);
|
||||
const basename = path.basename(lifecyclePath, ".mdx");
|
||||
return agentVariants.map((variant) => ({
|
||||
path: path.join(sourceDirectory, `${basename}.${variant}.generated.mdx`),
|
||||
contents: renderAgentVariantPage(source, variant),
|
||||
}));
|
||||
const relativeSourceDirectory = path.relative(
|
||||
path.join(repoRoot, "docs"),
|
||||
path.dirname(lifecyclePath),
|
||||
);
|
||||
return agentVariants.map((variant) => {
|
||||
const outputPath = path.join(
|
||||
generatedDocsRoot,
|
||||
relativeSourceDirectory,
|
||||
`${basename}.${variant}.generated.mdx`,
|
||||
);
|
||||
return {
|
||||
path: outputPath,
|
||||
contents: renderAgentVariantPage(source, variant, {
|
||||
outputPath,
|
||||
sourcePath: lifecyclePath,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function rewriteRelativeMarkdownLinks(
|
||||
body: string,
|
||||
sourceDirectory: string,
|
||||
outputDirectory: string,
|
||||
): string {
|
||||
return body.replace(/(!?\[[^\]]+\]\()([^)]+)(\))/g, (_match, prefix, target, suffix) => {
|
||||
if (shouldKeepLinkTarget(target)) return `${prefix}${target}${suffix}`;
|
||||
return `${prefix}${rewriteRelativeLinkTarget(target, sourceDirectory, outputDirectory)}${suffix}`;
|
||||
});
|
||||
}
|
||||
|
||||
function shouldKeepLinkTarget(target: string): boolean {
|
||||
return target.startsWith("#") || target.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(target);
|
||||
}
|
||||
|
||||
function rewriteRelativeLinkTarget(
|
||||
target: string,
|
||||
sourceDirectory: string,
|
||||
outputDirectory: string,
|
||||
): string {
|
||||
const match = target.match(/^([^?#]*)([?#].*)?$/);
|
||||
if (!match || !match[1]) return target;
|
||||
|
||||
const absoluteTarget = path.resolve(sourceDirectory, match[1]);
|
||||
const relativeTarget = path.relative(outputDirectory, absoluteTarget).replaceAll(path.sep, "/");
|
||||
const normalizedTarget = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
|
||||
return `${normalizedTarget}${match[2] ?? ""}`;
|
||||
}
|
||||
|
||||
function writeGeneratedFiles(files: RenderedFile[]): void {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ $$nemoclaw list
|
|||
`;
|
||||
|
||||
describe("agent variant docs", () => {
|
||||
it("renders OpenClaw sentinel code and content", () => {
|
||||
it("renders OpenClaw placeholder code and content", () => {
|
||||
const rendered = renderAgentVariantPage(source, "openclaw");
|
||||
|
||||
expect(rendered).toContain("OpenClaw only.");
|
||||
|
|
@ -33,7 +33,7 @@ describe("agent variant docs", () => {
|
|||
expect(rendered).not.toContain("AgentOnly");
|
||||
});
|
||||
|
||||
it("renders Hermes sentinel code and content", () => {
|
||||
it("renders Hermes placeholder code and content", () => {
|
||||
const rendered = renderAgentVariantPage(source, "hermes");
|
||||
|
||||
expect(rendered).not.toContain("OpenClaw only.");
|
||||
|
|
@ -42,4 +42,19 @@ describe("agent variant docs", () => {
|
|||
expect(rendered).not.toContain("$$nemoclaw");
|
||||
expect(rendered).not.toContain("AgentOnly");
|
||||
});
|
||||
|
||||
it("rewrites relative links for generated build output", () => {
|
||||
const rendered = renderAgentVariantPage(
|
||||
`${source}\nSee [Commands](../reference/commands#$$nemoclaw-list).\nSee [Backup](backup-restore).\n`,
|
||||
"hermes",
|
||||
{
|
||||
outputPath:
|
||||
"/repo/docs/_build/agent-variants/manage-sandboxes/lifecycle.hermes.generated.mdx",
|
||||
sourcePath: "/repo/docs/manage-sandboxes/lifecycle.mdx",
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered).toContain("[Commands](../../../reference/commands#nemohermes-list)");
|
||||
expect(rendered).toContain("[Backup](../../../manage-sandboxes/backup-restore)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue