Files
supabase/apps/docs/lib/docs.ts
Charis a29dc5de4b feat(docs): main troubleshooting page (#29120)
* feat(docs): main troubleshooting page

* feat(docs): global troubleshooting page layout

* feat(docs): global troubleshooting page filter logic

* enhance(docs): troubleshooting page ui & filters

Improve UX and accessibility of global troubleshooting search page.

- Implement TroubleshootingFilterStateProvider for managing filter state
- Add TroubleshootingFilterEmptyState component
- Improve TroubleshootingFilter UI with applied filters display
- Update TroubleshootingPreview component with better layout and accessibility
- Add TroubleshootingEntryAssociatedErrors component for displaying related errors
- Refactor Troubleshooting utils and schemas
- Update troubleshooting content template

* feat(docs): individual troubleshooting pages

Add individual pages for detailed information on troubleshooting
entries. Small fixes to UI for troubleshooting displays.

- Wrap GlobalTroubleshootingPage with SidebarSkeleton
- Add TroubleshootingErrorListDetailed component for detailed error display
- Implement TroubleshootingBackLink component for navigation
- Update TroubleshootingPreview to use Next.js Link and include parent page
- Refactor Footer component to accept className prop
- Add hideFooter option to SidebarSkeleton
- Minor updates to TopNavBar and other utility functions

* fix: minor styling issues

* tweak(troubleshooting docs): use breadcrumbs

* tweak(troubleshooting docs): use sidebar for filter

* enhance(troubleshooting docs): navigation, keywords

Better navigation for individual troubleshooting entries, using a
version of the sidebar + keywords handling via search params for good
linking behavior

Details:

- Add TroubleshootingSidebar to TroubleshootingPage
- Implement query state management for keywords using nuqs
- Improve accessibility and styling of troubleshooting components
- Update empty state handling and related keywords display
- Upgrade nuqs package to version 1.19.1

* fix(docs): wrap useSearchParams in Suspense

* seo(docs): add canonical link to troubleshooting

* enhance(docs,troubleshooting): auto-reveal errors

On troubleshooting search pages, if the searched term matches a hidden
error, auto-reveal it when searching. When the search term is cleared,
hide programmatically-opened errors while preserving user-opened ones.

* enhance(docs,troubleshooting): search bar

Add search bar to individual troubleshooting pages

* fix(docs): breadcrumb links

Fix breadcrumb linking so it uses next/link instead of reloading entire
app on navigation.

* fix(docs): typo

* enh(docs): new troubleshooting design

* enh(docs): use new multi-select

* enh(docs): troubleshooting responsive styles

* Update apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx
2024-10-02 17:10:12 +00:00

158 lines
4.9 KiB
TypeScript

import { type CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
import matter from 'gray-matter'
import { serialize } from 'next-mdx-remote/serialize'
import type { SerializeOptions } from 'next-mdx-remote/dist/types'
import { existsSync } from 'node:fs'
import { readdir, readFile } from 'node:fs/promises'
import { join, extname, sep, basename } from 'node:path'
import remarkGfm from 'remark-gfm'
import rehypeKatex from 'rehype-katex'
import remarkMath from 'remark-math'
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
// MUST be process.cwd() here, not import.meta.url, or files that are added
// with outputFileTracingIncludes (not auto-traced) will not be found at
// runtime.
export const DOCS_DIRECTORY = process.cwd()
export const EXAMPLES_DIRECTORY = join(DOCS_DIRECTORY, '..', '..', 'examples')
export const GUIDES_DIRECTORY = join(DOCS_DIRECTORY, 'content/guides')
export const REF_DOCS_DIRECTORY = join(DOCS_DIRECTORY, 'docs/ref')
export const SPEC_DIRECTORY = join(DOCS_DIRECTORY, 'spec')
export type GuideFrontmatter = {
title: string
subtitle?: string
description?: string
canonical?: string
hideToc?: boolean
// @deprecated
hide_table_of_contents?: boolean
tocVideo?: string
}
/**
* Validate the frontmatter for guide MDX files.
*
* @throws Throws if frontmatter is invalid.
*/
export function isValidGuideFrontmatter(obj: object): obj is GuideFrontmatter {
if (!('title' in obj) || typeof obj.title !== 'string') {
throw Error(
// @ts-expect-error - Getting undefined for unknown property is desired here.
`Invalid guide frontmatter: Title must exist and be a string. Received: ${obj.title}`
)
}
if ('subtitle' in obj && typeof obj.subtitle !== 'string') {
throw Error(`Invalid guide frontmatter: Subtitle must be a sring. Received: ${obj.subtitle}`)
}
if ('description' in obj && typeof obj.description !== 'string') {
throw Error(
`Invalid guide frontmatter: Description must be a string. Received: ${obj.description}`
)
}
if ('canonical' in obj && typeof obj.canonical !== 'string') {
throw Error(`Invalid guide frontmatter: Canonical must be a string. Received: ${obj.canonical}`)
}
if ('hideToc' in obj && typeof obj.hideToc !== 'boolean') {
throw Error(`Invalid guide frontmatter: hideToc must be a boolean. Received: ${obj.hideToc}`)
}
if ('hide_table_of_contents' in obj && typeof obj.hide_table_of_contents !== 'boolean') {
throw Error(
`Invalid guide frontmatter: hide_table_of_contents must be a boolean. Received ${obj.hide_table_of_contents}`
)
}
if ('tocVideo' in obj && typeof obj.tocVideo !== 'string') {
throw Error(`Invalid guide frontmatter: tocVideo must be a string. Received ${obj.tocVideo}`)
}
return true
}
export async function getGuidesStaticPaths(section: string) {
const directory = join(GUIDES_DIRECTORY, section)
const files = (await readdir(directory, { recursive: true }))
.filter((file) => extname(file) === '.mdx' && !basename(file).startsWith('_'))
.map((file) => ({
params: {
slug: file.replace(/\.mdx$/, '').split(sep),
},
}))
// Index page isn't included in the directory
const indexFile = join(GUIDES_DIRECTORY, `${section}.mdx`)
if (existsSync(indexFile)) {
files.push({ params: { slug: [] } })
}
return {
paths: files,
fallback: false,
}
}
export async function getGuidesStaticProps(
section: string,
{ params }: { params?: { slug?: string | Array<string> } }
) {
let relPath: string
switch (typeof params?.slug) {
case 'string':
relPath = section + sep + params.slug
break
case 'object': // actually an array
relPath = section + sep + params.slug.join(sep)
break
case 'undefined':
relPath = section
}
const fullPath = join(GUIDES_DIRECTORY, relPath + '.mdx')
/**
* SAFETY CHECK:
* Prevent accessing anything outside of GUIDES_DIRECTORY
*/
if (!fullPath.startsWith(GUIDES_DIRECTORY)) {
throw Error('Accessing forbidden route. Content must be within the GUIDES_DIRECTORY.')
}
const mdx = await readFile(fullPath, 'utf-8')
const editLink = `supabase/supabase/blob/master/apps/docs/content/guides/${relPath}.mdx`
const { data: frontmatter, content } = matter(mdx)
if (!isValidGuideFrontmatter(frontmatter)) {
// Will have thrown
return
}
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
lineNumbers: true,
showCopyButton: true,
skipLanguages: [],
autoImport: false,
}
const mdxOptions: SerializeOptions = {
mdxOptions: {
useDynamicImport: true,
remarkPlugins: [
[remarkMath, { singleDollarTextMath: false }],
remarkGfm,
[remarkCodeHike, codeHikeOptions],
],
rehypePlugins: [rehypeKatex as any],
},
}
const mdxSource = await serialize(content, mdxOptions)
return {
props: {
frontmatter,
mdxSource,
editLink,
},
}
}