diff --git a/apps/docs/resources/globalSearch/globalSearchModel.ts b/apps/docs/resources/globalSearch/globalSearchModel.ts index 731d0271e2c..0d6b4881f61 100644 --- a/apps/docs/resources/globalSearch/globalSearchModel.ts +++ b/apps/docs/resources/globalSearch/globalSearchModel.ts @@ -128,7 +128,7 @@ function createModelFromMatch({ } else { return null } - case 'github-discussions': + case 'troubleshooting': return new TroubleshootingModel({ title: page_title, href, diff --git a/apps/docs/scripts/search/generate-embeddings.ts b/apps/docs/scripts/search/generate-embeddings.ts index 81263f113a5..d7519d2b156 100644 --- a/apps/docs/scripts/search/generate-embeddings.ts +++ b/apps/docs/scripts/search/generate-embeddings.ts @@ -446,9 +446,6 @@ async function generateEmbeddings() { } requireEnvOrThrow([ - 'DOCS_GITHUB_APP_ID', - 'DOCS_GITHUB_APP_INSTALLATION_ID', - 'DOCS_GITHUB_APP_PRIVATE_KEY', 'NEXT_PUBLIC_MISC_ANON_KEY', 'NEXT_PUBLIC_MISC_URL', 'NEXT_PUBLIC_SUPABASE_URL', diff --git a/apps/docs/scripts/search/sources/index.ts b/apps/docs/scripts/search/sources/index.ts index 88a1de69c2f..e62f320f371 100644 --- a/apps/docs/scripts/search/sources/index.ts +++ b/apps/docs/scripts/search/sources/index.ts @@ -1,10 +1,5 @@ import { type GuideModel } from '../../../resources/guide/guideModel.js' import { GuideModelLoader } from '../../../resources/guide/guideModelLoader.js' -import { - GitHubDiscussionLoader, - type GitHubDiscussionSource, - fetchDiscussions, -} from './github-discussion.js' import { LintWarningsGuideLoader, type LintWarningsGuideSource } from './lint-warnings-guide.js' import { MarkdownLoader, type MarkdownSource } from './markdown.js' import { IntegrationLoader, type IntegrationSource, fetchPartners } from './partner-integrations.js' @@ -16,13 +11,14 @@ import { OpenApiReferenceLoader, type OpenApiReferenceSource, } from './reference-doc.js' +import { fetchTroubleshootingSources, type TroubleshootingSource } from './troubleshooting.js' export type SearchSource = | MarkdownSource | OpenApiReferenceSource | ClientLibReferenceSource | CliReferenceSource - | GitHubDiscussionSource + | TroubleshootingSource | IntegrationSource | LintWarningsGuideSource @@ -150,21 +146,15 @@ export async function fetchAllSources(fullIndex: boolean) { .then((data) => data.flat()) : [] - const githubDiscussionSources = fetchDiscussions( - 'supabase', - 'supabase', - 'DIC_kwDODMpXOc4CUvEr' // 'Troubleshooting' category - ) - .then((discussions) => - Promise.all( - discussions.map((discussion) => - new GitHubDiscussionLoader('supabase/supabase', discussion).load() - ) - ) - ) + // Load troubleshooting articles from local MDX files + const troubleshootingSources = fetchTroubleshootingSources() + .then((loaders) => Promise.all(loaders.map((loader) => loader.load()))) .then((data) => data.flat()) - const sources: SearchSource[] = ( + // Type assertion required because ReferenceLoader.load() returns Promise + // which widens the inferred union type. All concrete sources in this array are valid + // SearchSource types (MarkdownSource, OpenApiReferenceSource, etc.). + const sources = ( await Promise.all([ guideSources, lintWarningsGuideSources, @@ -177,9 +167,9 @@ export async function fetchAllSources(fullIndex: boolean) { ktLibReferenceSource, cliReferenceSource, partnerIntegrationSources, - githubDiscussionSources, + troubleshootingSources, ]) - ).flat() + ).flat() as SearchSource[] return sources } diff --git a/apps/docs/scripts/search/sources/troubleshooting.ts b/apps/docs/scripts/search/sources/troubleshooting.ts new file mode 100644 index 00000000000..424ba8aea59 --- /dev/null +++ b/apps/docs/scripts/search/sources/troubleshooting.ts @@ -0,0 +1,101 @@ +import { createHash } from 'node:crypto' +import { BaseLoader, BaseSource } from './base.js' +import { + getAllTroubleshootingEntriesInternal, + getArticleSlug, +} from '../../../features/docs/Troubleshooting.utils.common.mjs' +import type { ITroubleshootingEntry } from '../../../features/docs/Troubleshooting.utils.js' + +/** + * Loader for troubleshooting articles from local MDX files. + * + * The path format is `/guides/troubleshooting/{slug}` where slug is derived + * from the filename (e.g., `auth-error-handling.mdx` → `auth-error-handling`). + */ +export class TroubleshootingLoader extends BaseLoader { + type = 'troubleshooting' as const + + constructor( + source: string, + public entry: ITroubleshootingEntry + ) { + const slug = getArticleSlug(entry) + super(source, `/guides/troubleshooting/${slug}`) + } + + async load(): Promise { + return [new TroubleshootingSource(this.source, this.path, this.entry)] + } +} + +/** + * Search source for a single troubleshooting article. + * + * Each article becomes one indexed page with a single section containing + * the full content. This differs from guide pages which may have multiple + * sections based on headings. + */ +export class TroubleshootingSource extends BaseSource { + type = 'troubleshooting' as const + + constructor( + source: string, + path: string, + public entry: ITroubleshootingEntry + ) { + super(source, path) + } + + async process() { + const { title, topics, keywords } = this.entry.data + const content = this.entry.contentWithoutJsx + + // Include title and metadata in checksum so any changes trigger re-indexing. + // This ensures updates to title, topics, or keywords are picked up even if + // the main content hasn't changed. + const checksum = createHash('sha256') + .update(JSON.stringify({ title, topics, keywords, content })) + .digest('base64') + + const meta = { title, topics, keywords } + + // Troubleshooting articles are single-section pages (no sub-headings indexed). + // We explicitly set slug to undefined so the database's `get_full_content_url` + // function returns the page URL without a fragment (e.g., no trailing `#slug`). + // This is handled in SQL: `CASE WHEN slug IS NULL THEN '' ELSE concat('#', slug) END` + const sections = [ + { + heading: title, + slug: undefined as string | undefined, + content: `# ${title}\n${content}`, + }, + ] + + this.checksum = checksum + this.meta = meta + this.sections = sections + + return { checksum, meta, sections } + } + + /** + * Returns the full article content formatted for full-text search indexing. + * The title is included as a heading to boost its relevance in search results. + */ + extractIndexedContent(): string { + return `# ${this.entry.data.title}\n\n${this.entry.contentWithoutJsx}` + } +} + +/** + * Loads all troubleshooting articles from `content/troubleshooting/` directory. + * + * Each MDX file is parsed, validated, and converted to a TroubleshootingLoader. + * Hidden files (prefixed with `_`) are excluded. + * + * @returns Array of loaders, one per troubleshooting article + */ +export async function fetchTroubleshootingSources(): Promise { + const entries = (await getAllTroubleshootingEntriesInternal()) as ITroubleshootingEntry[] + return entries.map((entry) => new TroubleshootingLoader('troubleshooting', entry)) +} diff --git a/packages/common/hooks/useDocsSearch.ts b/packages/common/hooks/useDocsSearch.ts index 1b02bb44779..b64f14b6f90 100644 --- a/packages/common/hooks/useDocsSearch.ts +++ b/packages/common/hooks/useDocsSearch.ts @@ -16,6 +16,7 @@ enum PageType { Reference = 'reference', Integration = 'partner-integration', GithubDiscussion = 'github-discussions', + Troubleshooting = 'troubleshooting', } interface PageSection { diff --git a/packages/ui-patterns/src/CommandMenu/prepackaged/DocsSearch/DocsSearchPage.tsx b/packages/ui-patterns/src/CommandMenu/prepackaged/DocsSearch/DocsSearchPage.tsx index db6b2a8457e..afba52261c1 100644 --- a/packages/ui-patterns/src/CommandMenu/prepackaged/DocsSearch/DocsSearchPage.tsx +++ b/packages/ui-patterns/src/CommandMenu/prepackaged/DocsSearch/DocsSearchPage.tsx @@ -1,14 +1,14 @@ 'use client' import { - DocsSearchResultType as PageType, - useDocsSearch, type DocsSearchResult as Page, type DocsSearchResultSection as PageSection, + DocsSearchResultType as PageType, + useDocsSearch, } from 'common' import { Book, ChevronRight, Github, Hash, Loader2, MessageSquare, Search } from 'lucide-react' import { useEffect, useRef } from 'react' -import { Button, cn, CommandGroup_Shadcn_, CommandItem_Shadcn_, CommandList_Shadcn_ } from 'ui' +import { Button, CommandGroup_Shadcn_, CommandItem_Shadcn_, CommandList_Shadcn_, cn } from 'ui' import { StatusIcon } from 'ui/src/components/StatusIcon' import { @@ -83,6 +83,7 @@ const DocsSearchPage = () => { switch (pageType) { case PageType.Markdown: case PageType.Reference: + case PageType.Troubleshooting: if (BASE_PATH === '/docs') { router.push(link) setIsOpen(false) @@ -299,8 +300,9 @@ export function formatSectionUrl(page: Page, section: PageSection) { return `${page.path}#${section.slug ?? ''}` case PageType.Reference: return `${page.path}/${section.slug ?? ''}` + case PageType.Troubleshooting: + // [Charis] Markdown headings on integrations pages don't have slugs yet case PageType.Integration: - // [Charis] Markdown headings on integrations pages don't have slugs yet return page.path default: throw new Error(`Unknown page type '${page.type}'`) @@ -312,6 +314,7 @@ export function getPageIcon(page: Page) { case PageType.Markdown: case PageType.Reference: case PageType.Integration: + case PageType.Troubleshooting: return case PageType.GithubDiscussion: return @@ -325,6 +328,7 @@ export function getPageSectionIcon(page: Page) { case PageType.Markdown: case PageType.Reference: case PageType.Integration: + case PageType.Troubleshooting: return case PageType.GithubDiscussion: return