Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0ba54fc
docs(specs): example-first docs content — ExampleCode include, guards…
blove Sep 5, 2026
978a8f5
docs(plans): PR 1 of example-first docs — ExampleCode include, guards…
blove Sep 5, 2026
22e41aa
feat(cockpit-shell): carry raw example sources in the content bundle
blove Sep 5, 2026
5c793b1
feat(website): pure resolution for example-code includes
blove Sep 5, 2026
50247b3
fix(website): example-code regions track nesting and tolerate tight H…
blove Sep 5, 2026
3b4184b
feat(website): ExampleCode server component renders example files thr…
blove Sep 5, 2026
8b3da38
fix(website): ExampleCode title assertions, accessible group name, se…
blove Sep 5, 2026
642faac
feat(website): bind ExampleCode to each docs page's example
blove Sep 5, 2026
53137a9
fix(website): ExampleCode names the page in docs-only errors; raw sou…
blove Sep 5, 2026
36d74bd
style(website): prettier on the new example-code files
blove Sep 5, 2026
0ac2bdd
test(website): guard that mapped docs pages include their example code
blove Sep 5, 2026
a72507a
docs(langgraph): streaming page includes the running example's component
blove Sep 5, 2026
6fd4429
test(website): docs example-code guard cannot pass vacuously; clearer…
blove Sep 5, 2026
f2aff94
docs(plans): the Website prod build needs GROWTH_FORM_POLICY and writ…
blove Sep 5, 2026
d2a3438
docs(langgraph): bridge the hand-written snippet and the running exam…
blove Sep 5, 2026
1107be9
refactor(cockpit-registry): drop docsAssetPaths; walkthroughs are no …
blove Sep 5, 2026
41ff528
refactor(cockpit-shell): delete the walkthrough renderer and narrativ…
blove Sep 5, 2026
49d0a71
refactor(workspace-react): remove the narrative Docs panel and its an…
blove Sep 5, 2026
7e7c095
style(cockpit-registry): prettier on the simplified Docs-mode assertion
blove Sep 5, 2026
57321c5
refactor(website): drop the narrative-action analytics hook
blove Sep 5, 2026
4f5f1ec
docs(contributing): how docs pages include example code
blove Sep 5, 2026
1236f1a
test(workspace-react): Docs panel test distinguishes empty from crash…
blove Sep 5, 2026
11f3482
docs: the Code tab keeps region markers; only the docs slice strips t…
blove Sep 5, 2026
d02552c
fix(website): unnamed nested regions keep depth balanced; non-docs MD…
blove Sep 5, 2026
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
32 changes: 32 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,38 @@ own Protection Bypass for Automation secret:
flight does not reach that run; re-run after provisioning. Never pass
`--skip-domain` to a preview deploy; Vercel requires it to accompany `--prod`.

## Docs pages and example code

A docs page whose capability ships a runnable example (the page shows Run and
Code tabs) teaches through that example. Its code comes from the example
files, never from a hand-typed copy:

```mdx
<ExampleCode file="streaming.component.ts" />
<ExampleCode file="graph.py" region="stream-modes" title="Stream modes" />
```

- `file` is a basename or a repo-relative path among the capability's
`codeAssetPaths` and `backendAssetPaths` in
`libs/cockpit-registry/src/lib/content-descriptors.ts`. An unknown or
ambiguous name fails the build.
- `region` names a marker pair in that file. Markers are `// #region name` …
`// #endregion` in TypeScript, `# region name` … `# endregion` in Python,
and `<!-- #region name -->` … `<!-- #endregion -->` in HTML. Regions may
nest. The marker lines are stripped from the slice on the docs page and
the slice is de-indented. The Code tab shows the whole file, markers
included, and the region name surfaces in the build error when a region is
missing or unterminated, so keep the names meaningful.
- Hand-written fences stay allowed for fragments the example does not cover,
such as another runtime's variant.

`apps/website/src/lib/docs-example-code.spec.ts` fails when a mapped page
includes nothing, when an include does not resolve, or when a docs-only page
uses the tag. Its scan is textual, so the tag must not appear in prose,
fenced code, or MDX comments on any docs page. Pages not yet rewritten sit in
its `PENDING_PAGES` list; a page that gains its first include must leave the
list in the same change.

## Code review

Every PR gets a genuine advisory AI code review
Expand Down
6 changes: 6 additions & 0 deletions apps/website/content/docs/langgraph/guides/streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ export class ChatComponent {
</Tab>
</Tabs>

### The running example

The demo in the Run tab is the smallest real integration of this pattern. The snippet above sketches the pieces by hand; the real component uses the prebuilt `<chat>` composition instead. It injects the agent configured in `app.config.ts` and hands it to `<chat>`, which owns message rendering, input, and the typing indicator.

<ExampleCode file="streaming.component.ts" />

## Stream status

The `status()` signal reports the current lifecycle state of the SSE connection:
Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default async function BlogPostPage({ params }: Params) {
<TagChips tags={post.frontmatter.tags} />
) : null}
</header>
<MdxRenderer source={post.content} />
<MdxRenderer source={post.content} docsPath={`/blog/${slug}`} />
</article>
<DocsTOC headings={headings} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ interface ElementProps {
docsSlot?: ReactNode;
requestedMode?: string | null;
resolution?: { kind?: string; identity?: { availableModes?: string[] } };
contentBundle?: { runtimeUrl?: string | null };
contentBundle?: {
runtimeUrl?: string | null;
codeSources?: Record<string, string>;
};
contextTrail?: readonly { label: string; href?: string; icon?: ReactNode }[];
docsContext?: unknown;
docsPath?: string;
exampleCode?: {
assetPaths?: readonly string[];
sources?: Record<string, string>;
} | null;
}

function findElement(
Expand Down Expand Up @@ -63,6 +72,20 @@ describe('unified docs workspace route', () => {
activeSection: 'guides',
activeSlug: 'streaming',
});

const mdx = findElement(
workspace?.props.docsSlot,
MdxRenderer as ComponentType<never>
);
expect(mdx?.props.exampleCode?.assetPaths).toContain(
'cockpit/langgraph/streaming/angular/src/app/streaming.component.ts'
);
// The server-rendered <ExampleCode> keeps the raw sources; the client
// boundary must not carry a second copy of them into the RSC payload.
expect(Object.keys(mdx?.props.exampleCode?.sources ?? {})).toContain(
'cockpit/langgraph/streaming/angular/src/app/streaming.component.ts'
);
expect(workspace?.props.contentBundle?.codeSources).toEqual({});
});

it('keeps an unmapped page as a complete server Docs slot', async () => {
Expand All @@ -78,6 +101,12 @@ describe('unified docs workspace route', () => {
findElement(slot, DocsPageHeader as ComponentType<never>)
).toBeTruthy();
expect(findElement(slot, MdxRenderer as ComponentType<never>)).toBeTruthy();
expect(
findElement(slot, MdxRenderer as ComponentType<never>)?.props.exampleCode
).toBeNull();
expect(
findElement(slot, MdxRenderer as ComponentType<never>)?.props.docsPath
).toBe('/docs/langgraph/guides/testing');
expect(findElement(slot, DocsTOC as ComponentType<never>)).toBeTruthy();
});

Expand Down
17 changes: 14 additions & 3 deletions apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import {
type LibraryId,
} from '../../../../../lib/docs-config';
import { WebsiteWorkspace } from '../../../../../components/workspace/WebsiteWorkspace';
import { getWebsiteWorkspacePage } from '../../../../../lib/workspace-page';
import {
getExampleCodeContext,
getWebsiteWorkspacePage,
} from '../../../../../lib/workspace-page';
import fs from 'fs';
import path from 'path';

Expand Down Expand Up @@ -168,7 +171,11 @@ export default async function DocsPage({ params }: DocsRouteProps) {
/>
</div>
<article className="flex-1 py-8 px-4 sm:px-6 md:px-12 md:max-w-3xl">
<MdxRenderer source={doc.body} />
<MdxRenderer
source={doc.body}
exampleCode={getExampleCodeContext(workspacePage)}
docsPath={pathname}
/>
</article>
{section === 'api' &&
(() => {
Expand Down Expand Up @@ -226,7 +233,11 @@ export default async function DocsPage({ params }: DocsRouteProps) {
<WebsiteWorkspace
resolution={workspacePage.resolution}
presentation={workspacePage.presentation}
contentBundle={workspacePage.contentBundle}
// Raw sources exist only for the server-rendered `<ExampleCode>` above
// (docsSlot already captured them); the workspace shell renders the
// highlighted `codeFiles`, so shipping them again would only add dead
// weight to this client boundary's RSC payload.
contentBundle={{ ...workspacePage.contentBundle, codeSources: {} }}
navigationTree={workspacePage.navigationTree}
routePath={pathname}
docsSlot={docsSlot}
Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/app/docs/choosing-an-adapter/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function ChoosingAnAdapterPage() {
aria-label={PAGE_TITLE}
className="flex-1 py-8 px-4 sm:px-6 md:px-12 md:max-w-3xl"
>
<MdxRenderer source={source} />
<MdxRenderer source={source} docsPath="/docs/choosing-an-adapter" />
</article>
</div>
{/* This page carries as many headings as any library page, so it gets
Expand Down
65 changes: 65 additions & 0 deletions apps/website/src/components/docs/MdxRenderer.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { isValidElement, type ReactElement, type ReactNode } from 'react';
import { describe, expect, it } from 'vitest';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { MdxRenderer } from './MdxRenderer';

function findMdx(
node: ReactNode
): ReactElement<{ components: Record<string, unknown> }> | null {
if (
!isValidElement<{
components: Record<string, unknown>;
children?: ReactNode;
}>(node)
)
return null;
if (node.type === MDXRemote) return node;
return findMdx(node.props.children);
}

describe('MdxRenderer', () => {
it('always registers ExampleCode, bound to the page context', () => {
const withContext = findMdx(
MdxRenderer({
source: '# x',
exampleCode: {
docsPath: '/docs/p',
assetPaths: ['a/b.ts'],
sources: { 'a/b.ts': '' },
},
})
);
const without = findMdx(
MdxRenderer({ source: '# x', docsPath: '/docs/only' })
);

expect(typeof withContext?.props.components['ExampleCode']).toBe(
'function'
);
// Not just "a function": an unresolvable file must fail against THIS
// page's context, which a hard-coded createExampleCode(null) could not do.
expect(() =>
(
withContext?.props.components['ExampleCode'] as (p: {
file: string;
}) => unknown
)({ file: 'nope.ts' })
).toThrow(/\/docs\/p/);

expect(typeof without?.props.components['ExampleCode']).toBe('function');
expect(() =>
(
without?.props.components['ExampleCode'] as (p: {
file: string;
}) => unknown
)({ file: 'b.ts' })
).toThrow(/mapped example/);
expect(() =>
(
without?.props.components['ExampleCode'] as (p: {
file: string;
}) => unknown
)({ file: 'b.ts' })
).toThrow(/\/docs\/only/);
});
});
32 changes: 16 additions & 16 deletions apps/website/src/components/docs/MdxRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import {
MiddlewareHowItFits,
TelemetryHowItFits,
} from './diagrams';
import rehypePrettyCode from 'rehype-pretty-code';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import { mdxCompileOptions } from './mdx-options';
import { createExampleCode } from './mdx/ExampleCode';
import type { ExampleCodeContext } from '../../lib/example-code';

/**
* Intrinsic size of each SVG diagram in `public/blog/diagrams`.
Expand Down Expand Up @@ -87,28 +87,28 @@ const mdxComponents = {
...mdxHeadingComponents,
};

const rehypeOptions = {
theme: 'tokyo-night',
keepBackground: true,
};

interface MdxRendererProps {
source: string;
/** Present on docs pages that embed a runnable example; null elsewhere. */
exampleCode?: ExampleCodeContext | null;
/** Route of the page being rendered, so a docs-only failure can name it. */
docsPath?: string;
}

export function MdxRenderer({ source }: MdxRendererProps) {
export function MdxRenderer({
source,
exampleCode = null,
docsPath,
}: MdxRendererProps) {
return (
<div className="docs-prose">
<MDXRemote
source={source}
components={mdxComponents}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rehypePlugins: [rehypeSlug, [rehypePrettyCode, rehypeOptions] as any],
},
components={{
...mdxComponents,
ExampleCode: createExampleCode(exampleCode, docsPath),
}}
options={mdxCompileOptions}
/>
</div>
);
Expand Down
21 changes: 21 additions & 0 deletions apps/website/src/components/docs/mdx-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import rehypePrettyCode from 'rehype-pretty-code';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';

const rehypeOptions = {
theme: 'tokyo-night',
keepBackground: true,
};

/**
* The one MDX compile configuration. `MdxRenderer` uses it for whole pages and
* `ExampleCode` for the fence it synthesizes, so included code is highlighted
* and styled exactly like a hand-written block.
*/
export const mdxCompileOptions = {
mdxOptions: {
remarkPlugins: [remarkGfm],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rehypePlugins: [rehypeSlug, [rehypePrettyCode, rehypeOptions] as any],
},
};
Loading
Loading