Skip to content

docs: add default links and Ask AI to search dialog#2703

Merged
Sushmithamallesh merged 4 commits intonextfrom
docs/ask-ai-search
Feb 20, 2026
Merged

docs: add default links and Ask AI to search dialog#2703
Sushmithamallesh merged 4 commits intonextfrom
docs/ask-ai-search

Conversation

@Sushmithamallesh
Copy link
Collaborator

Summary

  • Custom search dialog replaces the default Fumadocs dialog
  • Shows quick-access links with subtitles (title + description from MDX frontmatter) when search query is empty: Quickstart, Authentication, Configuring Sessions, General FAQs, Troubleshooting
  • Adds "You can also Ask AI" button inside the dialog content that opens the Decimal widget
  • Titles and descriptions are derived from page frontmatter at build time via source.getPage(), so they stay in sync automatically

Test plan

  • Open search dialog (Cmd+K) — verify 5 default links with subtitles appear
  • Type a query — verify links disappear and search results show
  • Click a default link — verify navigation works and dialog closes
  • Click "Ask AI" — verify dialog closes and Decimal widget opens
  • bun run build passes with no type errors

🤖 Generated with Claude Code

Custom search dialog shows quick-access links (Quickstart, Authentication,
Configuring Sessions, General FAQs, Troubleshooting) with subtitles when
search is empty. Titles and descriptions are derived from MDX page
frontmatter at build time. Adds "Ask AI" button inside dialog content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Feb 20, 2026 7:03am

Request Review

@github-actions
Copy link
Contributor

⚠️ Security Audit Warning

The pnpm audit --prod check found security vulnerabilities in production dependencies.

Please review and fix the vulnerabilities. You can try running:

pnpm audit --fix --prod
Audit output
┌─────────────────────┬────────────────────────────────────────────────────────┐
│ high                │ minimatch has a ReDoS via repeated wildcards with      │
│                     │ non-matching literal in pattern                        │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ Package             │ minimatch                                              │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ Vulnerable versions │ <10.2.1                                                │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ Patched versions    │ >=10.2.1                                               │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ Paths               │ ts__examples__google>@google/genai>google-auth-        │
│                     │ library>gaxios>rimraf>glob>minimatch                   │
│                     │                                                        │
│                     │ ts__packages__cli>openapi-typescript>@redocly/openapi- │
│                     │ core>minimatch                                         │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ More info           │ https://github.com/advisories/GHSA-3ppc-4f35-3m26      │
└─────────────────────┴────────────────────────────────────────────────────────┘
┌─────────────────────┬────────────────────────────────────────────────────────┐
│ low                 │ Hono added timing comparison hardening in basicAuth    │
│                     │ and bearerAuth                                         │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ Package             │ hono                                                   │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ Vulnerable versions │ <4.11.10                                               │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ Patched versions    │ >=4.11.10                                              │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ Paths               │ ts__e2e-tests__runtimes__cloudflare__cf-workers-       │
│                     │ basic>hono                                             │
├─────────────────────┼────────────────────────────────────────────────────────┤
│ More info           │ https://github.com/advisories/GHSA-gq3j-xvxp-8hrf      │
└─────────────────────┴────────────────────────────────────────────────────────┘
3 vulnerabilities found
Severity: 1 low | 2 high

@claude
Copy link
Contributor

claude bot commented Feb 20, 2026

Claude finished @Sushmithamallesh's task in 2m 34s —— View job


PR Review

  • Gathering context
  • Reading docs reviewer guidelines
  • Running git diff to analyze changes
  • Applying review checklist

Summary

This PR adds a custom search dialog with default quick-access links and an "Ask AI" button connecting to the Decimal widget. The implementation is largely solid, but there are two issues worth fixing before merge.


Issues

1. Incorrect import path in ask-ai-button.tsx

docs/components/ask-ai-button.tsx lines 5–6 import from @fumadocs/ui which does not exist as a package (no @fumadocs directory in node_modules, no path alias in tsconfig.json):

// Current (incorrect)
import { useSearchContext } from '@fumadocs/ui/contexts/search';
import { useI18n } from '@fumadocs/ui/contexts/i18n';

Every other component in this PR uses the correct fumadocs-ui package name:

// Correct (matches custom-search-dialog.tsx and custom-schema-ui.tsx)
import { useSearchContext } from 'fumadocs-ui/contexts/search';
import { useI18n } from 'fumadocs-ui/contexts/i18n';

docs/components/heading.tsx has the same issue (@fumadocs/ui/cnfumadocs-ui/cn). This would cause build failures if skipLibCheck doesn't hide the resolution error at compile time, and will definitely fail at runtime if the package can't be resolved. Fix this →


2. Module-level widgetOpen state can desync

docs/components/ask-ai-button.tsx line 14 uses a module-level boolean to track whether the Decimal widget is open:

let widgetOpen = false;

export function toggleDecimalWidget() {
  // ...
  widgetOpen ? decimal.hide() : decimal.show();
  widgetOpen = !widgetOpen;
}

If a user dismisses the Decimal widget using the widget's own close button (not through toggleDecimalWidget), widgetOpen stays true. The next click on "Ask AI" will call decimal.hide() instead of decimal.show(), making the button appear broken. Subsequent clicks will toggle correctly again (one click wasted).

This is a UX bug but not a blocker — it affects users who close the widget via the widget's own UI. One mitigation: check whether the Decimal API exposes an onClose callback or event to reset widgetOpen. If not, the current approach is the pragmatic choice given the third-party widget constraint.


What Looks Good

  • Default links logic in app/layout.tsx correctly uses source.getPage() with flatMap to silently skip missing pages and falls back to '' for missing descriptions — stays in sync with frontmatter automatically.
  • custom-search-dialog.tsx: The isEmpty / SearchDialogList branching is correct. Passing onOpenChange(false) on link click is the right pattern.
  • decimal-widget.tsx: Theme sync via MutationObserver on document.documentElement is a clean approach for following dark/light mode switches.
  • SearchAndAskAIMobile has proper aria-label attributes on icon-only buttons.
  • The Cmd+I / Ctrl+I shortcut registration cleans up correctly on unmount.

@claude
Copy link
Contributor

claude bot commented Feb 20, 2026

Claude finished @Sushmithamallesh's task in 2m 58s —— View job


PR Review

  • Gathering context
  • Reading docs reviewer guidelines
  • Running git diff to analyze changes
  • Applying review checklist
  • Posting review feedback

The approach is sound — custom SearchDialog composition, build-time page resolution via source.getPage(), and reusing toggleDecimalWidget from the existing ask-ai-button.tsx component. Two issues worth fixing:


Issue 1: isEmpty is defined on line 41 but the else branch on line 74 repeats the condition instead of using it.

File: docs/components/custom-search-dialog.tsx:74

// Before
<SearchDialogList items={query.data === 'empty' ? null : query.data} />

// After
<SearchDialogList items={isEmpty ? null : query.data} />

Fix this →


Issue 2: options is cast with as Record<string, unknown> in layout.tsx:118 to pass defaultLinks through the Fumadocs search config. This bypasses TypeScript and silently relies on Fumadocs internally spreading options as props to the custom SearchDialog. If Fumadocs changes this behavior, the default links would stop showing with no type error to catch it.

A more explicit alternative is to wrap the dialog:

// Instead of passing defaultLinks through options with a type cast:
const CustomSearchDialogWithLinks = (props: SharedProps) => (
  <CustomSearchDialog defaultLinks={defaultLinks} {...props} />
);

// Then in layout:
search={{
  SearchDialog: CustomSearchDialogWithLinks,
  options: { api: '/api/search' },  // ← also restore the api option here
}}

Note that the api: '/api/search' option was also removed from options in this PR — it's now hardcoded inside CustomSearchDialog. If the API route ever changes, it'll need updating in two places. The wrapper approach above lets you restore it to options.


Everything else looks good: all 5 linked pages have description in their frontmatter (verified), the Link elements are keyboard-navigable and semantically correct, and the component follows existing patterns in docs/components/.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

api: '/api/search',
},
defaultLinks,
} as Record<string, unknown>,
Copy link

Choose a reason for hiding this comment

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

Default links never reach custom search dialog component

High Severity

defaultLinks is passed via search.options in the RootProvider, but Fumadocs' options field configures the search client (API endpoint, tags, etc.) — it does not forward those properties as props to the custom SearchDialog component. Fumadocs only passes SharedProps (open, onOpenChange) to custom dialog components. The as Record<string, unknown> cast masks the type error. As a result, CustomSearchDialog always receives defaultLinks as [] (its default), the defaultLinks.length > 0 condition is always false, and the quick-access links never render — breaking the primary feature of this PR.

Additional Locations (1)

Fix in Cursor Fix in Web

- Restore api: '/api/search' in search options (single source of truth)
- CustomSearchDialog accepts api prop from options spread
- Remove unused isEmpty variable (TS can't narrow through intermediates)
- Keep options spread approach (verified: SearchProvider line 52 does ...options)
- Type cast needed because RootProvider types options more narrowly than
  SearchProvider actually consumes them

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Contributor

claude bot commented Feb 20, 2026

Claude finished @Sushmithamallesh's task in 2m 45s —— View job


PR Review

  • Gathering context
  • Reading docs reviewer guidelines
  • Running git diff to analyze changes
  • Applying review checklist
  • Posting review feedback

Summary

This PR adds a custom search dialog with default quick-access links and an "Ask AI" button. The latest commit (0aa7ed7) addressed previous review feedback by restoring api: '/api/search' in options and adding an api prop to CustomSearchDialog.


One Issue to Verify

defaultLinks may not reach CustomSearchDialog

docs/app/layout.tsx:118 passes defaultLinks inside search.options:

search={{
  SearchDialog: CustomSearchDialog,
  options: {
    api: '/api/search',
    defaultLinks,
  } as Record<string, unknown>,
}}

The commit message says this was verified against SearchProvider line 52, which reportedly spreads ...options as props to the custom dialog. If that's correct, the feature works as intended. If options is only consumed by the search client (not forwarded to the dialog), CustomSearchDialog will always receive defaultLinks = [] and the default links will never render.

Verify in the Vercel preview: Open the search dialog (Cmd+K) — if 5 default links appear before typing anything, the options spread is working. If the dialog is empty until you type, the fix is to use a wrapper component:

// In layout.tsx, replace:
SearchDialog: CustomSearchDialog,
options: { api: '/api/search', defaultLinks } as Record<string, unknown>,

// With:
SearchDialog: (props: SharedProps) => (
  <CustomSearchDialog defaultLinks={defaultLinks} api="/api/search" {...props} />
),
options: undefined,

This approach is type-safe and doesn't rely on internal Fumadocs behavior.


What Looks Good

  • source.getPage() with flatMap for build-time link resolution — silently skips missing pages, stays in sync with frontmatter automatically.
  • query.data === 'empty' && defaultLinks.length > 0 branching is correct: falls back to SearchDialogList items={null} when no default links are configured and search is empty.
  • onClick={() => props.onOpenChange(false)} on default links correctly closes the dialog on navigation.
  • "You can also Ask AI" button: proper type="button", visible text (no aria-label needed), correct handler sequence (close dialog then show widget).
  • api prop with default '/api/search' is a cleaner pattern than hardcoding it inside the hook.

| Vercel preview

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Contributor

claude bot commented Feb 20, 2026

Claude finished @Sushmithamallesh's task in 1m 34s —— View job


PR Review

  • Gathering context
  • Reading docs reviewer guidelines
  • Running git diff to analyze changes
  • Applying review checklist

One issue to fix before merge

defaultLinks never reaches CustomSearchDialog — default links won't render

In docs/app/layout.tsx:116-119, defaultLinks is placed inside search.options:

search={{
  SearchDialog: CustomSearchDialog,
  options: {
    api: '/api/search',
    defaultLinks,           // ← this goes to the Fumadocs search client config
  } as Record<string, unknown>,
}}

Fumadocs only passes SharedProps (open, onOpenChange) to the custom SearchDialog component — it doesn't forward the options object as props. So CustomSearchDialog always receives its default defaultLinks = [], the defaultLinks.length > 0 condition on line 66 is always false, and the quick-access links never render. This breaks the primary feature of the PR.

Fix: wrap the dialog to inject defaultLinks as a real prop. Define the wrapper at module scope (outside the Layout function) to avoid re-creating it on every render:

// docs/app/layout.tsx — add this after the defaultLinks computation, before the Layout function
import type { SharedProps } from 'fumadocs-ui/components/dialog/search';

const CustomSearchDialogWithLinks = (props: SharedProps) => (
  <CustomSearchDialog defaultLinks={defaultLinks} {...props} />
);

// Then in RootProvider:
search={{
  SearchDialog: CustomSearchDialogWithLinks,
  options: { api: '/api/search' },
}}

Fix this →


Everything else looks good: the source.getPage() + flatMap pattern for build-time link resolution is correct, the SearchDialogList branching handles loading/error/results states properly, and the "Ask AI" button closes the dialog before opening the widget.

@Sushmithamallesh
Copy link
Collaborator Author

Re: defaultLinks never reaches CustomSearchDialog

Verified that defaultLinks does reach the dialog. Here's why:

Source code proof@fumadocs/ui/dist/contexts/search.js line 48-52:

jsx(SearchDialog, {
  open: isOpen,
  onOpenChange: setIsOpen,
  links,
  ...options  // ← options are spread as props onto SearchDialog
})

SearchProvider destructures options from its props, then spreads ...options onto the custom SearchDialog component. Since defaultLinks is inside options, it becomes a prop on CustomSearchDialog.

Runtime proof — fetching the RSC flight response from the dev server:

curl -sL -H 'RSC: 1' 'http://localhost:3000/docs' | grep -o 'defaultLinks\|Common issues\|Frequently asked'

Output:

defaultLinks
Frequently asked
Common issues
Frequently asked
Common issues

The defaultLinks key and its description values are present in the serialized RSC payload sent to the client.

Why the wrapper approach fails — defining const SearchDialogWithLinks = (props) => <CustomSearchDialog .../> in layout.tsx (a Server Component) causes: Error: Functions cannot be passed directly to Client Components. Server Components can only pass module references (imports) to Client Components, not inline functions.

The as Record<string, unknown> cast is needed because RootProvider types options as Partial<DefaultSearchDialogProps> (which doesn't know about defaultLinks), but SearchProvider actually accepts Partial<SharedProps & Record<string, unknown>> and spreads them all.

@Sushmithamallesh Sushmithamallesh merged commit ae818b6 into next Feb 20, 2026
8 checks passed
@Sushmithamallesh Sushmithamallesh deleted the docs/ask-ai-search branch February 20, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant