The __itemsCache has no invalidation hook and lives for the life of
the Node process. That's correct for the current `next start`
deployment but silently fragile if we ever add ISR to /reference —
the cache would serve stale items forever.
Replace the two-line inline comment with a JSDoc block that spells
out the prod vs dev behaviour and the ISR caveat, so future changes
know where the assumption lives.
Three loosely-coupled shim hardenings in one commit (all in mdx-registry.tsx):
- Tooltip / TooltipProvider were children-only shims that silently
dropped Radix-style `content` / `label` props. Route through
stub() so dev gets a one-shot warning with the dropped prop names.
- YouTubeVideo + IframeSwitcher now validate author-controlled URLs
via a shared validateIframeSrc() helper: only https:// is accepted,
malformed URLs fall back to rendering nothing, and dev gets a
targeted warning. YouTubeVideo also enforces the 11-char video-id
shape so a stray value can't traversal-inject query params into the
embed URL.
- Link shim now detects external hrefs (https://..., mailto:..., etc.)
and routes them through a plain <a> with sensible defaults
(target=_blank, rel=noopener noreferrer). Internal hrefs continue to
use next/link for client-side navigation. next/link should never be
used for external URLs — it spuriously prefetches them.
The inlineSnippets regex deliberately only matches strict self-closing
JSX with an optional `components={...}` attr, which confused readers
into thinking tags like `<Snippet region="x" />` fall through by
accident. Document the two-path design (SNIPPET_MAP inlining vs MDX
component map) so future edits don't try to "fix" the selectivity.
No behaviour change. SNIPPET_MAP aliases were already documented
in-place (finding noted earlier); this commit covers the regex comment
only.
readTitle/readMeta are backed by process-scoped Maps. Without a dev
bypass, editing an MDX title or meta.json required a server restart to
see the change — the nav sidebar would keep showing the old title.
Match the convention already used in reference-items.ts: gate both the
read and write paths on isProd() so `next dev` re-reads the source on
every request. Production behaviour is unchanged.
Both props were accepted but ignored (underscore-destructured). scope
was originally intended to switch resolution logic, but both branches
collapsed to the same href rule. fallbackHref was a pre-hydration
placeholder that became unnecessary once the provider made framework
URL-derived.
Remove them from the public SidebarLinkProps and from the only caller
(docs-page-view), which was computing a scope based on slugHrefPrefix
solely to pass it through.
cell and region are authored on the interface for MDX usage + tooling
but the component doesn't consume them at runtime — the parent MDX
renderer pre-renders one <Snippet> per framework on the server and
emits them as children. Mark them optional and document the contract.
Also filter React.Children.toArray output to valid elements before the
index→framework map, so stray whitespace text nodes injected by MDX
don't shift the mapping and render the wrong snippet.
iOS Safari does not reliably fire mousedown for taps landing outside
focusable UI, so the panel stayed open on mobile. Mirror the existing
click-outside handler on touchstart and widen the handler type to
MouseEvent | TouchEvent. stripFrameworkPrefix already restricts its
match to the first path segment (docstring makes this explicit).
- Dev-only console.warn when author-supplied items.length diverges from
the number of <Tab> children — silent drop of extra entries hid bugs.
- Dev-only console.warn on duplicate labels — React keys on label below
so duplicates broke reconciliation.
- Tab button keys now include the index so duplicate labels don't cause
React key collisions.
- Added a useEffect that re-seeds the active tab when the set of labels
changes (e.g. MDX edit swaps titles, HMR reload). Previously useState
initialized once on mount, so a tab whose label disappeared left the
panel stuck empty.
Previously any valid child element received the injected __index/__total
props — stray non-Step valid elements (e.g. spacer divs) would have those
props passed straight to the DOM, triggering React warnings. Now filter
to elements whose type === Step before cloning.
Also add role='list' to the Steps container and role='listitem' to each
Step so assistive tech can enumerate the sequence.
Silent fallback to 'info' masked typos in MDX authors' type props. Now
emit a dev-only console.warn listing the known types when an unknown
one slips past TypeScript (e.g. from raw MDX string literals).
- activeBrandFromPath now matches /ag-ui exactly or /ag-ui/..., not any
path starting with those six chars — prevents misclassifying a future
/ag-ui-* slug as AG-UI.
- Mobile menu panel z-index bumped to z-[51] so layering above the z-50
backdrop doesn't rely on DOM sibling order.
- BrandNavProps (frameworkOptions, frameworkCategoryOrder) were unused
dead API — removed from the interface and function signature. Call
site passes no props, so no caller changes.
React.Children.map only visits *direct* children. When an author
wraps nested <PropertyReference> in a <div> or <Fragment> (a common
MDX pattern), the nested references silently lost `collapsable:
true` propagation because the walk stopped at the wrapper.
Replace the single-level `React.Children.map` with
`deepMapPropertyReferences`, a recursive helper that:
- clones + enhances any PropertyReference at any depth with
`collapsable: true`
- preserves arbitrary wrapper elements (div, Fragment, other
components) and recurses into their children
- stops recursing once it hits a PropertyReference — that
reference's own render will deep-walk ITS children on its turn,
which is the correct nesting semantic
Updates the JSDoc above the enhanced-children block to reflect the
new behavior — nested wrappers are now supported.
Both FrameworkGuardedContent and RouterPivot used to render against
`storedFramework` on the very first client render, when it is always
null (localStorage is read in a mount effect inside
FrameworkProvider). A returning user who picked LangChain would
briefly see the full pivot grid + MDX body flash in before
storedFramework flipped from null to "langgraph-python" and the
redirect fired.
Gate both components behind a local `hasHydrated` flag flipped in a
mount useEffect. Pre-hydration we render null (for the MDX body) or
a minimal "Loading…" placeholder (for the pivot itself), so
returning users go straight to the redirect placeholder instead of
flashing content that's about to disappear. Fresh visitors see the
pivot on the next tick — imperceptible in practice.
Previously readStoredFramework returned null for both "never set" and
"localStorage unavailable" — consumers couldn't distinguish a fresh
visitor from a private-mode browser. Introduce a tri-state: keep
storedFramework: string | null for the value, and add
storageAvailable: boolean so UIs can branch on "we can't persist
your pick" separately from "you haven't picked yet".
Also subscribe to window `storage` events so that when another tab
clears or changes selectedFramework, this tab's stored state stays
in sync (StoredFrameworkHighlight badge + RouterPivot redirect
react immediately without a reload).
Strengthens the comment above the URL-persistence effect explaining
why `stored` is intentionally excluded from the deps array (prevents
a setState ping-pong loop), and adds behavioral "Covered by:" notes
above each non-trivial fix.
Replace the argv-sniffing 'process.argv.includes("build")' check with
process.env.NEXT_PHASE === "phase-production-build", the Next.js
canonical signal for a production build. The argv approach is fragile:
it breaks under wrappers, programmatic invocation, or any tool that
invokes next via a different argv shape.
COMMIT_SHA defaulted to 'unknown', so a caller that forgot to pass
--build-arg COMMIT_SHA would silently ship an image whose footer,
health checks, and error reports all report 'unknown'. Add a guard in
the builder stage that refuses to proceed when COMMIT_SHA is unset or
still the 'unknown' default.
showcase/scripts/ now ships a package-lock.json (committed here for the
first time), so the Docker builder can use 'npm ci' for deterministic
installs instead of 'npm install'. Update the comment to match actual
state and copy the lockfile into the scripts stage.
The runner stage was copying the entire builder node_modules tree,
shipping tailwindcss, postcss, tsx, typescript, and every @types/*
package into the production image. Add 'npm prune --omit=dev' after
the node_modules copy (and also copy package-lock.json so prune runs
deterministically) to strip dev-only packages from the runtime image.
The ag-ui and reference pages each carried their own near-identical
stripImportsFenceAware copy with a TODO(dedup) flag. The canonical
implementation already exists in docs-render.tsx as stripLeadingImports
and has better semantics — it only strips imports that appear in the
top-of-file header region (before the first non-import content line),
rather than any import anywhere outside a fence. That distinction
matters for doc bodies that legitimately discuss imports in prose
outside code fences.
Export stripLeadingImports from docs-render and point both pages at it;
remove the local duplicates.
Card's type accepted icon, className, and children but the component
body never read them — silent prop drop. Render icon above the title,
merge className onto the wrapper, and render children below the
description so MDX authors get the behavior the type signature
promises.
Two Callout components coexisted: a restricted variant in
mdx-components.tsx (info | warn | error only) and the broader-surface
variant in docs-callout.tsx (info | tip | warn | warning | error |
danger | note). reference/[...slug]/page.tsx imported from
mdx-components, silently limiting authors to three types. Collapse to
a single implementation by re-exporting docs-callout's Callout from
mdx-components so existing import paths keep working.
The registry's <Link> shim rendered a plain <a>, forcing a full page
reload on every internal MDX link. next/link was already imported in
this file; now the shim routes through it when href is present, falling
back to a bare <a> only when no href is provided.
The page wrapper renders the extracted title inside its own <h1>.
When no frontmatter title is present the title comes from the MDX
body's first H1 — which MDXRemote then also renders, producing two
stacked h1s at the top of every ag-ui doc. Strip the first leading
`# …` line (after any blank lines) from the MDX content before
rendering so the wrapper provides the single h1 and the body flows
from the first paragraph.
Both sidebar-framework-selector.tsx and the catch-all docs page.tsx
kept local copies of FRAMEWORK_CATEGORY_ORDER that mirrored the
canonical one exported from docs-render. Drift between the copies
would have shown up as divergent category ordering across the UI.
Remove the local duplicates and import the single source of truth.
The non-greedy regex matches through the first same-family close tag,
so nested containers like <Card>outer <Card>inner</Card> rest</Card>
closed at the inner </Card> and left the remaining 'rest</Card>' as
literal text. Capture the opening tag name and bail when the inner
body contains another occurrence of it — renders correctly through
MDX's own JSX handling instead of producing broken markup.
Accept \r?\n in extractFrontmatter so Windows-authored MDX files
don't silently skip frontmatter extraction. In stripLeadingImports,
store the full fence marker (``` or ~~~) rather than its first
character so a stray single backtick inside a fenced block doesn't
prematurely close it.
Replace the hand-rolled frontmatter regex in loadDoc with gray-matter
(already a dep) so quoted values, folded YAML, multiline descriptions,
and malformed frontmatter no longer fall through silently. Wrap every
fs read in readTitle, loadDoc, inlineSnippets, and buildNavTreeFromFilesystem
in try/catch — a single unreadable file / permission error used to
crash the entire page render.
Move parseTableRow, isTableRow, isTableSeparator to module scope
so they aren't recreated on every convertTablesInJSX call. Fixes
eslint-plugin-unicorn(consistent-function-scoping) warnings. No
behavior change.
- Document every SNIPPET_MAP alias inline (AgUI/AGUI,
FrontendTools/FrontEndToolsImpl) so future maintainers can see why
the duplicates exist and don't collapse them blindly. Confirmed
FrontEndToolsImpl is still referenced from live MDX under
integrations/langgraph/*.
- Verified every SNIPPET_MAP target exists on disk with matching
casing (including migrate-to-1.10.X.mdx and migrate-to-1.8.2.mdx).
- Export FRAMEWORK_CATEGORY_ORDER (+ FrameworkCategory type) from
docs-render as the single source of truth. Consumer files
(sidebar-framework-selector.tsx, [[...slug]]/page.tsx) each define
the same constant independently today; a follow-up agent can
retire those duplicates once they pick up the import.
- Log the full Error instance (not just .message/.digest) so the stack
trace actually reaches the server log / browser devtools.
- Include the pathname (via usePathname) in the log message so reports
can be tied back to the page that crashed.
- Replace misleading migration-specific copy ("This page will be fixed
shortly. Some ... components are still being migrated.") with a
generic user-facing message — the error boundary catches errors from
many causes, not just migration gaps.
- Show error.digest explicitly as a reportable "Error ID" so users can
include it when reporting the issue; drop the fallback to
error.message because it can leak internal details in a user-facing
surface.
- Extract duplicated loadItems/getAllItems into @/lib/reference-items so the
/reference index page and the /reference/[...slug] page read the same
tree the same way (same subdirs, same walker, same gray-matter path,
same caching).
- Walker is now recursive: subfolder files like components/inputs/textarea.mdx
are indexed and statically generated. Previously only top-level .mdx
files under components/ and hooks/ were picked up.
- Wrap gray-matter and fs reads in try/catch per file: a single malformed
frontmatter block no longer crashes the whole static-generation pass —
we log the offending path and skip that file.
- Fence-aware import stripper in the slug page so code samples containing
import ... lines inside fences are not corrupted. (Duplicated inline
here and in ag-ui page with a TODO(dedup) marker pointing at a future
shared helper in @/lib/docs-render.)
- loadReferenceItems memoizes in production (module-scope cache keyed by
subdir). Dev bypasses the cache so edits show up without a server
restart.
buildNavTree walks the entire content tree on every page render and
calls readTitle / readMeta O(pages) times per request. Each call
previously reopened and re-parsed the file from disk.
- Process-scoped Map caches keyed by resolved absolute path.
- Null results are cached too — a missing or malformed file still
short-circuits on the second visit.
- Memory footprint is trivial: titles are short strings, meta is a
small JSON object.
- Stale-during-process semantics are acceptable for Next.js build
and server runtime lifetimes; authors re-run the process to pick
up MDX/meta.json edits (same as before in production builds).
- Replace local MDX components map with spread of shared docsComponents
from @/lib/mdx-registry, so AG-UI pages get the full component set
(Tabs, FrameworkTabs, Snippet, Steps from @/components/docs-steps,
etc.) instead of silently rendering raw JSX when MDX uses anything
outside the tiny local subset.
- Use the shared InlineDemo from mdx-registry, whose Open full demo
link uses an absolute NEXT_PUBLIC_SHELL_URL/integrations/... URL.
The prior local copy used a relative /integrations/... URL that 404d
on the docs host (no /integrations route on docs.showcase.copilotkit.ai).
- Parse frontmatter with gray-matter instead of a whole-file title:
regex that could match any title: line buried inside an MDX body.
- Wrap fs.readFileSync in try/catch with meaningful error logs naming
the offending path; fall back gracefully instead of crashing render.
- Fence-aware leading-import stripper so MDX code samples that show
import ... lines are not corrupted (duplicated here; TODO(dedup) in
comment to hoist into @/lib/docs-render next time another route needs
it).
- JSX_CONTAINER_TAGS now covers Callout, Card, Cards, Step, Steps,
Tabs alongside the original Accordion/Tab. Previously a Markdown
table inside e.g. <Callout> silently failed to promote to HTML
and rendered as raw pipes.
- convertMarkdownTableToHtml's parseRow and the row/separator
detection in convertTablesInJSX now accept GFM tables written
WITHOUT outer leading/trailing pipes (valid GFM — previously
rejected because we sliced (1, -1) unconditionally).
- readMeta now logs the offending path + parse error when JSON.parse
fails, replacing the silent catch. Malformed meta.json no longer
collapses to 'no nav ordering' with zero diagnostic signal.
- Clear selection button was hidden on /docs/* because it branched on
framework (URL-derived, always null there). A user landing on /docs
with a stale storedFramework from a previous session had no way to
clear the preference from the selector UI. Show the button whenever
framework OR storedFramework is set; skip the navigation when we're
not on a framework-scoped route (it's a pure preference change).
- router.push -> router.replace on framework change. Picking a backend
is a pivot on the same logical page, not forward navigation. Using
push clutters the back stack with every framework the user clicked
through, making the browser Back button useless.
- Type-guard e.target with instanceof Node before passing to contains,
rather than hard-casting. EventTarget can be non-DOM (e.g. events
against window); the cast silently degrades to contains(null) which
returns false in most engines but is undefined behaviour in spec.
- Document that stripFrameworkPrefix intentionally only inspects
parts[0] and does NOT recurse — /<fw>/<fw>/x must keep the inner
<fw>/x as the feature tail.
- Rename INTEGRATION_CATEGORY_IDS to FRAMEWORK_CATEGORY_ORDER to match
the identically-defined constant in /[[...slug]]/page.tsx (the two
will be consolidated into @/lib/registry in a follow-up owned by the
registry refactor; left a TODO pointing at the canonical home).
- sort_order tiebreak: Array#sort is not guaranteed stable for ties
across engines. When multiple integrations default to 999 the
rendered order shuffles between V8 revisions. Add an explicit
alphabetical-by-slug tiebreak so the rendered panel is
deterministic.
fallbackHref was assigned to href and then immediately overwritten in
every branch of the scope switch — the value was never read. Since
useFramework() returns the URL-derived framework identically during
SSR and post-hydration, the resolved href matches what fallbackHref
was pre-computing server-side, so the 'client takes over on mount'
comment was describing a no-op.
- Remove the dead assignment; resolve href directly from framework.
- Keep fallbackHref as an optional prop (ignored) to avoid churning
call sites that still pass it.
The effect had no dependency array, so it re-ran on every render and
hijacked the user's own sidebar scroll whenever any unrelated re-render
fired (state/context flips, parent rerenders). Key the effect on
pathname so scroll-to-active only runs on route change.
Also:
- behavior: 'instant' is a Chromium non-standard extension; other
engines silently ignore it. Use 'auto' which is the spec-compliant
'no smooth animation' value.
- Wrap scrollIntoView in try/catch — it can throw on detached nodes
and in obscure iframe/security contexts; a sidebar scroll
restoration never justifies taking down the route.
- Replace the global /^import\s+.+$/gm pass with a fence-aware walker
(stripLeadingImports). The old regex silently mangled docs code
samples like 'import os' inside Python fences; the new pass only
drops top-of-file MDX import lines and leaves fence bodies alone.
- Add Set<string> cycle tracking through inlineSnippets' recursion so
a self- or mutually-referencing snippet can't loop until the stack
overflows. On a repeat, emit a warning and inline a comment marker
so the author sees something diagnosable.
- Log warn when a <Component /> reference can't be mapped to a
snippet file (either missing from SNIPPET_MAP or missing on disk);
previously the reference silently rendered nothing.
- isMac now starts as null and is only resolved in useEffect, so SSR output matches the first client render; reserve horizontal space on the shortcut pill with a non-breaking-space placeholder (+ suppressHydrationWarning) so ⌘K/Ctrl+K swap-in doesn't reflow the button
- Cmd/Ctrl+K no longer hijacks the browser shortcut when focus is inside an unrelated <input>, <textarea>, <select>, or contenteditable; still toggles when focus is within the search modal itself (data-search-modal boundary)
- Trigger buttons use setOpen((prev) => !prev) so Cmd+K-on-top-of-existing-modal toggles correctly and the hint reflects the true action
- Replace useState<any> for registryData with typed Registry | null
- Surface registry-load failures: .catch logs + inline "Search index failed to load" banner
- Track the setTimeout focus id and clear it in effect cleanup
- Let static search-index matches render immediately; show a '[loading...]' hint until registry.json resolves; show 'no results' only after loading completes
- Use a ref for selectedIndex so Enter never reads a stale value after reset-on-input
- Dedupe results by type+href before slicing to 12 (stable key becomes type-href)
- Route navigation through a shared helper that detects external URLs (http(s)://, //) and uses window.location.assign for them, router.push otherwise
- Warn once in dev when an integration has no description so it gets fixed upstream