feat: federated docs for linter (#22472)

* feat: federated docs for linter

* style: clean up

* fix: toc and link rewrite

* fix: remove console logs
This commit is contained in:
Charis authored and GitHub committed 2024-04-04 13:35:31 -04:00
1 parent b5ef7af6b7
commit 1d52dfacde
15 files changed
+348 -388

No files matched your search

+79 -22
View File
@@ -1,5 +1,14 @@
import { FC } from 'react'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'
import { cn, TAB_CHANGE_EVENT_NAME } from 'ui'
import { ExpandableVideo } from 'ui-patterns'
import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils'
import useHash from '~/hooks/useHash'
import { useRerenderOnEvt } from '~/hooks/useManualRerender'
import { removeAnchor } from './CustomHTMLElements/CustomHTMLElements.utils'
import { Feedback } from './Feedback'
const formatSlug = (slug: string) => {
// [Joshen] We will still provide support for headers declared like this:
@@ -28,30 +37,78 @@ const formatTOCHeader = (content: string) => {
return res.join('')
}
interface TOCHeader {
id: number
level: number
text: string
link: string
}
const GuidesTableOfContents = ({
className,
overrideToc,
video,
}: {
className?: string
overrideToc?: Array<{ text: string; link: string; level: number }>
video?: string
}) => {
const [tocList, setTocList] = useState([])
const pathname = usePathname()
const [hash] = useHash()
interface Props {
list: TOCHeader[]
}
const displayedList = overrideToc ?? tocList
useEffect(() => {
if (overrideToc) return
const headings = Array.from(
document.querySelector('#sb-docs-guide-main-article')?.querySelectorAll('h2, h3') ?? []
)
const newHeadings = headings
.filter((heading) => heading.id)
.map((heading) => {
const text = heading.textContent.replace('#', '')
const link = heading.querySelector('a').getAttribute('href')
const level = heading.tagName === 'H2' ? 2 : 3
return { text, link, level }
})
setTocList(newHeadings)
}, [pathname]) // needed to recalculate the toc when path changes
useEffect(() => {
if (hash && displayedList.length > 0) {
highlightSelectedTocItem(hash)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hash, JSON.stringify(displayedList)])
/**
* Displayed headings may change if the tab changes, so the table of contents
* needs to rerender.
*/
useRerenderOnEvt(TAB_CHANGE_EVENT_NAME)
if (!displayedList.length) return
const tocVideoPreview = `http://img.youtube.com/vi/${video}/0.jpg`
const GuidesTableOfContents: FC<Props> = ({ list }) => {
return (
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
{list.map((item, i) => (
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
<a
href={`#${formatSlug(item.link)}`}
className="text-foreground-lighter hover:text-brand-link transition-colors"
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.text)) }}
/>
</li>
))}
</ul>
<div className={cn('border-l', 'thin-scrollbar overflow-y-auto', 'px-2', className)}>
{video && (
<div className="relative mb-6 pl-5">
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
</div>
)}
<Feedback key={pathname} />
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-6">
On this page
</span>
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
{displayedList.map((item, i) => (
<li key={`${item.level}-${i}`} className={item.level === 3 ? 'ml-4' : ''}>
<a
href={`#${formatSlug(item.link)}`}
className="text-foreground-lighter hover:text-brand-link transition-colors"
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.text)) }}
/>
</li>
))}
</ul>
</div>
)
}
@@ -794,7 +794,14 @@ export const database: NavMenuConstant = {
name: 'Query optimization',
url: '/guides/database/query-optimization',
},
{ name: 'Debugging and monitoring', url: '/guides/database/inspect' },
{
name: 'Database linter',
url: '/guides/database/database-linter',
},
{
name: 'Debugging and monitoring',
url: '/guides/database/inspect',
},
{
name: 'Debugging performance issues',
url: '/guides/database/debugging-performance',
-83
View File
@@ -1,83 +0,0 @@
// [Terry]
// Delete this after we've implemented GuidesTableofContents and moved all guides
// and rename GuidesTableofContents to TableOfContents
import { FC } from 'react'
import { getAnchor, removeAnchor } from './CustomHTMLElements/CustomHTMLElements.utils'
interface TOCHeader {
id: number
lvl: number
seen: number
content: string
slug: string
}
interface Props {
toc: any
video?: string
}
const formatSlug = (slug: string) => {
// [Joshen] We will still provide support for headers declared like this:
// ## REST API {#rest-api-overview}
// At least for now, this was a docusaurus thing.
if (slug.includes('#')) return slug.split('-#')[1]
return slug
}
const formatTOCHeader = (content: string) => {
let begin = false
const res = []
for (const x of content) {
if (x === '`') {
if (!begin) {
begin = true
res.push(`<code class="text-xs border rounded bg-surface-200 border-overlay">`)
} else {
begin = false
res.push(`</code>`)
}
} else {
res.push(x)
}
}
return res.join('')
}
const TableOfContents: FC<Props> = ({ toc, video }) => {
// [Joshen] markdown-toc doesn't seem to read maxdepth from the options passed in
// Our first level headers will be H2s (H1 is ignored), and we only show up to H3
return (
<>
{video && (
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/0Fs96oZ4se0"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
)}
<ul className="toc-menu list-none pl-4 text-[0.8rem] grid gap-2 mt-12">
{(toc.json as TOCHeader[])
.filter((item) => item.lvl !== 1 && item.lvl <= 3)
.map((item: any, i: number) => {
return (
<li key={i} id={item.lvl} style={{ marginLeft: `${(item.lvl - 2) * 1}rem` }}>
<a
href={`#${formatSlug(item.slug)}`}
className="text-foreground-lighter hover:text-brand transition-colors"
dangerouslySetInnerHTML={{ __html: formatTOCHeader(removeAnchor(item.content)) }}
/>
</li>
)
})}
</ul>
</>
)
}
export default TableOfContents
+2 -2
View File
@@ -14,7 +14,7 @@ const useHash = () => {
return () => {
window.removeEventListener('hashchange', hashChangeHandler)
}
}, [])
}, [hashChangeHandler])
const updateHash = useCallback(
(newHash) => {
@@ -23,7 +23,7 @@ const useHash = () => {
[hash]
)
return [hash, updateHash]
return [hash, updateHash] as const
}
export default useHash
+16
View File
@@ -0,0 +1,16 @@
import { useEffect, useReducer } from 'react'
/**
* An escape hatch to manually trigger component rerenders in response to
* external events, such as fired custom events.
*/
const useRerenderOnEvt = (event: string, listeningElem?: Document | Window | HTMLElement) => {
const [, rerender] = useReducer((state) => !state, true)
useEffect(() => {
;(listeningElem ?? window).addEventListener(event, rerender)
return () => (listeningElem ?? window).removeEventListener(event, rerender)
}, [event, listeningElem, rerender])
}
export { useRerenderOnEvt }
+1 -86
View File
@@ -1,91 +1,6 @@
import { MDXProvider } from '@mdx-js/react'
import { NextSeo } from 'next-seo'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { FC, PropsWithChildren } from 'react'
import components from '~/components'
import TableOfContents from '~/components/TableOfContents'
import { MainSkeleton } from './MainSkeleton'
interface Props {
meta: {
title: string
description?: string
hide_table_of_contents?: boolean
video?: string
tocVideo?: string
canonical?: string
}
children: any
toc?: any
menuItems: any
}
const Layout: FC<Props> = (props: Props) => {
const { asPath } = useRouter()
const hasTableOfContents =
props.toc !== undefined &&
props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
return (
<>
<Head>
<title>{asPath === '/' ? 'Supabase Docs' : `${props.meta?.title} | Supabase Docs`}</title>
<meta name="description" content={props.meta?.description} />
<meta property="og:image" content={`https://supabase.com/docs/img/supabase-og-image.png`} />
<meta
name="twitter:image"
content={`https://supabase.com/docs/img/supabase-og-image.png`}
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<NextSeo
canonical={props.meta?.canonical ?? `https://supabase.com/docs${asPath}`}
openGraph={{
url: `https://supabase.com/docs${asPath}`,
type: 'article',
videos: props.meta?.video && [
{
// youtube based video meta
url: props.meta?.video,
width: 640,
height: 385,
type: 'application/x-shockwave-flash',
},
],
article: {
publishedTime: new Date().toISOString(),
modifiedTime: new Date().toISOString(),
authors: ['Supabase'],
},
}}
/>
<LayoutMainContent>
<div className={['relative transition-all ease-out', 'duration-150 '].join(' ')}>
<article className="prose max-w-none">
<h1>{props.meta.title}</h1>
<MDXProvider components={components}>{props.children}</MDXProvider>
</article>
</div>
{hasTableOfContents && !props.meta?.hide_table_of_contents && (
<div
className={[
'border-default bg-background table-of-contents-height border-l',
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
].join(' ')}
>
<TableOfContents toc={props.toc} video={props.meta.video} />
</div>
)}
</LayoutMainContent>
</>
)
}
import { type FC, type PropsWithChildren } from 'react'
export const LayoutMainContent: FC<PropsWithChildren<{ className?: string }>> = ({
className,
children,
}) => <div className={['max-w-7xl px-5 mx-auto py-16', className].join(' ')}>{children}</div>
export default Layout
+2 -20
View File
@@ -6,30 +6,22 @@ import { FC } from 'react'
import components from '~/components'
import HomePageCover from '~/components/HomePageCover'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import TableOfContents from '~/components/TableOfContents'
import { LayoutMainContent } from './DefaultLayout'
import { MainSkeleton } from './MainSkeleton'
interface Props {
meta: {
title: string
description?: string
hide_table_of_contents?: boolean
video?: string
canonical?: string
description?: string
video?: string
}
children: any
toc?: any
menuItems: any
}
const HomeLayout: FC<Props> = (props: Props) => {
const { asPath } = useRouter()
const hasTableOfContents =
props.toc !== undefined &&
props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
return (
<>
<Head>
@@ -72,16 +64,6 @@ const HomeLayout: FC<Props> = (props: Props) => {
<MDXProvider components={components}>{props.children}</MDXProvider>
</div>
</div>
{hasTableOfContents && !props.meta?.hide_table_of_contents && (
<div
className={[
'border-default bg-background table-of-contents-height border-l',
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
].join(' ')}
>
<TableOfContents toc={props.toc} video={props.meta.video} />
</div>
)}
</LayoutMainContent>
</article>
</MainSkeleton>
+15 -63
View File
@@ -1,22 +1,18 @@
import { MDXProvider } from '@mdx-js/react'
import 'katex/dist/katex.min.css'
import { ExternalLink } from 'lucide-react'
import { NextSeo } from 'next-seo'
import { usePathname } from 'next/navigation'
import { useRouter } from 'next/router'
import { FC, useEffect, useRef, useState } from 'react'
import { type FC } from 'react'
import { IconExternalLink, cn } from 'ui'
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
import { cn } from 'ui'
import components from '~/components'
import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils'
import { FooterHelpCalloutType } from '~/components/FooterHelpCallout'
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
import { type MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import useHash from '~/hooks/useHash'
import { Feedback } from '~/components/Feedback'
import { LayoutMainContent } from '~/layouts/DefaultLayout'
import { MainSkeleton } from '~/layouts/MainSkeleton'
import 'katex/dist/katex.min.css'
interface Props {
meta: {
@@ -39,12 +35,6 @@ interface Props {
}
const Layout: FC<Props> = (props) => {
const pathname = usePathname()
const [hash] = useHash()
const articleRef = useRef()
const [tocList, setTocList] = useState([])
const { asPath } = useRouter()
const router = useRouter()
@@ -52,31 +42,6 @@ const Layout: FC<Props> = (props) => {
const EDIT_BUTTON_EXCLUDE_LIST = ['/404']
useEffect(() => {
if (hash && tocList.length > 0) {
highlightSelectedTocItem(hash as string)
}
}, [hash, JSON.stringify(tocList)])
useEffect(() => {
const articleEl = articleRef.current as HTMLElement
if (!articleRef.current) return
const headings = Array.from(articleEl.querySelectorAll('h2, h3'))
const newHeadings = headings
.filter((heading) => heading.id)
.map((heading) => {
const text = heading.textContent.replace('#', '')
const link = heading.querySelector('a').getAttribute('href')
const level = heading.tagName === 'H2' ? 2 : 3
return { text, link, level }
})
setTocList(newHeadings)
}, [pathname]) // needed to recalculate the toc when path changes
const hasTableOfContents = tocList.length > 0
const tocVideoPreview = `http://img.youtube.com/vi/${props.meta?.tocVideo}/0.jpg`
// page type, ie, Auth, Database, Storage etc
const ogPageType = asPath.split('/')[2]
// open graph image url constructor
@@ -141,10 +106,9 @@ const Layout: FC<Props> = (props) => {
<p className="text-brand tracking-wider mb-3">{props.meta.breadcrumb}</p>
)}
<article
ref={articleRef}
className={`${
props.meta?.hide_table_of_contents || !hasTableOfContents ? '' : ''
} prose max-w-none`}
// Used to get headings for the table of contents
id="sb-docs-guide-main-article"
className="prose max-w-none"
>
<h1 className="mb-0">{props.meta.title}</h1>
{props.meta?.subtitle && (
@@ -166,35 +130,23 @@ const Layout: FC<Props> = (props) => {
`}
className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200 w-fit"
>
Edit this page on GitHub <IconExternalLink size={14} strokeWidth={1.5} />
Edit this page on GitHub <ExternalLink size={14} strokeWidth={1.5} />
</a>
</div>
</div>
)}
</article>
</div>
{!props.hideToc && hasTableOfContents && !props.meta?.hide_table_of_contents && (
<div
{!props.hideToc && !props.meta?.hide_table_of_contents && (
<GuidesTableOfContents
video={props.meta?.tocVideo}
className={cn(
'col-span-3 self-start',
'border-overlay bg-background',
'thin-scrollbar overflow-y-auto hidden md:block md:col-span-3 px-2',
'sticky top-[calc(var(--mobile-header-height,40px)+2rem)] lg:top-[calc(var(--desktop-header-height,60px)+2rem)]'
'hidden md:block md:col-span-3',
'sticky top-[calc(var(--mobile-header-height,40px)+2rem)] lg:top-[calc(var(--desktop-header-height,60px)+2rem)]',
'max-h-[calc(100vh-60px-5rem)]'
)}
>
<div className="border-l">
{props.meta?.tocVideo && !!tocVideoPreview && (
<div className="relative mb-6 pl-5">
<ExpandableVideo imgUrl={tocVideoPreview} videoId={props.meta.tocVideo} />
</div>
)}
<Feedback key={pathname} />
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-6">
On this page
</span>
<GuidesTableOfContents list={tocList} />
</div>
</div>
/>
)}
</div>
</LayoutMainContent>
@@ -1,87 +0,0 @@
import { MDXProvider } from '@mdx-js/react'
import Head from 'next/head'
import { FC, useEffect, useState } from 'react'
import components from '~/components'
import SideBar from '~/components/Navigation/SideBar'
import TableOfContents from '~/components/TableOfContents'
interface Props {
meta: {
title: string
description?: string
hide_table_of_contents?: boolean
video?: string
tocVideo?: string
}
children: any
toc?: any
menuItems: any
currentPage: string
}
const Layout: FC<Props> = (props: Props) => {
const [active, setActive] = useState(false)
useEffect(() => {
setTimeout(function () {
setActive(true)
}, 150)
}, [])
// const contentString = renderToString(props.children)
// const content = serialize(contentString || '')
// console.log('contentString', contentString)
// const _toc = toc('#hello world', { maxdepth: 1, firsth1: false })
const hasTableOfContents =
props.toc !== undefined &&
props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0
return (
<>
<Head>
<title>{props.meta?.title} | Supabase</title>
<meta name="description" content={props.meta?.description} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/docs/favicon.ico" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={props.meta?.title} />
<meta property="og:description" content={props.meta?.description} />
<meta property="og:title" content={props.meta?.title} />
</Head>
<div
className={[
'relative transition-all ease-out',
'duration-150',
active ? 'opacity-100 left-0' : 'opacity-0 left-10',
].join(' ')}
>
<div>
<p className="text-brand tracking-wider">Tutorials</p>
<article className={['prose', 'max-w-none'].join(' ')}>
<h1>{props.meta.title}</h1>
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-300 to-brand my-16"></div>
<MDXProvider components={components} children={props.children} />
</article>
</div>
{hasTableOfContents && !props.meta?.hide_table_of_contents && (
<div
className={[
'border-overlay bg-background table-of-contents-height border-l',
'thin-scrollbar overflow-y-auto sticky hidden md:block md:col-span-3 px-2',
].join(' ')}
>
<TableOfContents toc={props.toc} video={props.meta.video} />
</div>
)}
</div>
</>
)
}
export default Layout
+1 -1
View File
@@ -25,7 +25,7 @@
"@next/bundle-analyzer": "^14.1.4",
"@next/mdx": "^14.1.4",
"@octokit/auth-app": "^6.0.3",
"@octokit/core": "^5.0.2",
"@octokit/core": "^5.1.0",
"@octokit/graphql": "^7.0.2",
"@octokit/plugin-paginate-graphql": "^4.0.0",
"@radix-ui/react-accordion": "^1.1.2",
+18 -15
View File
@@ -1,12 +1,14 @@
import specFile from '~/spec/cli_v1_config.yaml' assert { type: 'yml' }
import { Parameter } from '~/lib/refGenerator/refTypes'
import ReactMarkdown from 'react-markdown'
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
import { Heading } from '~/components/CustomHTMLElements'
import Head from 'next/head'
import { CodeBlock } from 'ui'
import { MainSkeleton } from '~/layouts/MainSkeleton'
import ReactMarkdown from 'react-markdown'
import { CodeBlock, cn } from 'ui'
import { Heading } from '~/components/CustomHTMLElements'
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import { MainSkeleton } from '~/layouts/MainSkeleton'
import { Parameter } from '~/lib/refGenerator/refTypes'
import specFile from '~/spec/cli_v1_config.yaml' assert { type: 'yml' }
// Parameters are grouped on the page by tag
const tocList = []
@@ -41,14 +43,15 @@ export default function Config() {
<div>{content}</div>
</div>
</div>
<div className="md:col-span-3">
<div className="sticky overflow-auto max-h-[calc(100vh-60px-5rem)] pb-3 top-32 border-l">
<span className="block font-mono text-xs uppercase text-foreground pl-5 mb-4">
On this page
</span>
<GuidesTableOfContents list={tocList} />
</div>
</div>
<GuidesTableOfContents
overrideToc={tocList}
className={cn(
'col-span-3 self-start',
'hidden md:block md:col-span-3',
'sticky top-[calc(var(--mobile-header-height,40px)+2rem)] lg:top-[calc(var(--desktop-header-height,60px)+2rem)]',
'max-h-[calc(100vh-60px-5rem)]'
)}
/>
</div>
</MainSkeleton>
</>
@@ -0,0 +1,190 @@
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
import { Octokit } from '@octokit/core'
import { capitalize } from 'lodash'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { MDXRemote } from 'next-mdx-remote'
import { serialize } from 'next-mdx-remote/serialize'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
import { Tabs } from 'ui'
import components from '~/components'
import { Heading } from '~/components/CustomHTMLElements'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import Layout from '~/layouts/DefaultGuideLayout'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
// We fetch these docs at build time from an external repo
export const org = 'supabase'
export const repo = 'splinter'
export const branch = 'main'
export const docsDir = 'docs'
const meta = {
title: 'Database Linter',
subtitle: 'Check your database for performance and security issues',
}
const editLink = 'https://github.com/supabase/splinter/tree/main/docs'
const markdownIntro = `
You can use the Project Linter to check your database for issues such as missing indexes and improperly set-up RLS policies.
## Using the linter
In the dashboard, navigate to [Database Linter](/dashboard/project/_/database/linter) under Database. The linter runs automatically. You can also manually rerun it after you're resolved issues, or ignore individual lints.
`.trim()
const getBasename = (path: string) => path.split('/').at(-1).replace(/\.md$/, '')
export default function ProjectLinterDocs({
intro,
lints,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<Layout menuId={MenuId.Database} meta={meta} editLink={editLink}>
<MDXRemote {...intro} components={components} />
<Heading tag="h2">Available lints</Heading>
<Tabs listClassNames="flex flex-wrap gap-2 [&>button]:!m-0" queryGroup="lint">
{lints.map((lint) => (
<Tabs.Panel
key={lint.path}
id={lint.path}
label={capitalize(getBasename(lint.path).replace(/_/g, ' '))}
>
<section id={getBasename(lint.path)}>
<MDXRemote {...lint.content} components={components} />
</section>
</Tabs.Panel>
))}
</Tabs>
</Layout>
)
}
/**
* The GitHub repo uses relative links, which don't lead to the right locations
* in docs.
*
* @param url The original link, as written in the Markdown file
* @returns The rewritten link
*/
const urlTransform: (lints: Array<{ path: string }>) => UrlTransformFunction = (lints) => (url) => {
try {
const placeholderHostname = 'placeholder'
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
// Don't modify a url with a FQDN or a url that's only a hash
if (hostname !== placeholderHostname || pathname === '/') {
return url
}
const relativePath = getBasename(pathname)
const section = lints.find(({ path }) => path === relativePath)
if (section) {
const url = new URL(window.location.href)
url.searchParams.set('lint', relativePath)
return url.toString()
}
// If we don't have this page in our docs, link to GitHub repo
return `https://github.com/${org}/${repo}/blob/${branch}${pathname}${hash}`
} catch (err) {
console.error('Error transforming markdown URL', err)
return url
}
}
const transformMarkdown = async (
rawContent: string,
{ replacementLinks = [] }: { replacementLinks?: Array<{ path: string }> } = {}
) => {
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
lineNumbers: true,
showCopyButton: true,
skipLanguages: [],
autoImport: false,
}
const content = await serialize(rawContent, {
scope: {
chCodeConfig: codeHikeOptions,
},
mdxOptions: {
remarkPlugins: [
remarkGfm,
remarkMkDocsAdmonition,
remarkPyMdownTabs,
[removeTitle, meta.title],
[remarkCodeHike, codeHikeOptions],
],
rehypePlugins: [[linkTransform, urlTransform(replacementLinks)], rehypeSlug],
},
})
return content
}
/**
* Fetch markdown from external repo and transform links
*/
export const getStaticProps = (async () => {
const octokit = new Octokit()
const response = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
owner: org,
repo: repo,
path: docsDir,
ref: branch,
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
})
if (response.status >= 400) {
throw Error(`Could not get contents of repo ${org}/${repo}`)
}
if (!Array.isArray(response.data)) {
throw Error(
`Reading a directory, not a file. Should not reach this, solely to appease Typescript.`
)
}
const [intro, ...lints] = await Promise.all([
await transformMarkdown(markdownIntro),
...response.data
.filter(({ path }) => /docs\/\d+.+\.md$/.test(path))
.map(async ({ path }, _, data) => {
const fileResponse = await fetch(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${path}`
)
if (fileResponse.status >= 400) {
throw Error(`Could not get contents of file ${org}/${repo}/${path}`)
}
const rawContent = await fileResponse.text()
const content = await transformMarkdown(rawContent, { replacementLinks: data })
return {
path: getBasename(path),
content,
}
}),
])
return {
props: {
intro,
lints,
},
}
}) satisfies GetStaticProps
+4 -4
View File
@@ -230,7 +230,7 @@
"@next/bundle-analyzer": "^14.1.4",
"@next/mdx": "^14.1.4",
"@octokit/auth-app": "^6.0.3",
"@octokit/core": "^5.0.2",
"@octokit/core": "^5.1.0",
"@octokit/graphql": "^7.0.2",
"@octokit/plugin-paginate-graphql": "^4.0.0",
"@radix-ui/react-accordion": "^1.1.2",
@@ -8378,9 +8378,9 @@
}
},
"node_modules/@octokit/core": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.2.tgz",
"integrity": "sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz",
"integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==",
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.0.0",
+3
View File
@@ -375,3 +375,6 @@ export * from './src/components/Icon/icons/IconZap'
// Export hooks
export * from './src/lib/Hooks'
// Shared constants/utils
export { TAB_CHANGE_EVENT_NAME } from './src/lib/events'
+9 -4
View File
@@ -81,7 +81,11 @@ const Tabs: React.FC<PropsWithChildren<TabsProps>> & TabsSubComponents = ({
*/
function handleChange(e: CustomEvent) {
if (e.detail.queryGroup === queryGroup && tabIds.includes(e.detail.id)) {
if (
e.detail.queryGroup &&
e.detail.queryGroup === queryGroup &&
tabIds.includes(e.detail.id)
) {
setActiveTab(e.detail.id)
setGroupActiveId?.(e.detail.id)
}
@@ -113,11 +117,12 @@ const Tabs: React.FC<PropsWithChildren<TabsProps>> & TabsSubComponents = ({
const url = new URL(document.location.href)
url.searchParams.set(queryGroup, id)
window.history.replaceState(undefined, '', url)
currentTarget.dispatchEvent(
new CustomEvent(TAB_CHANGE_EVENT_NAME, { bubbles: true, detail: { queryGroup, id } })
)
}
currentTarget.dispatchEvent(
new CustomEvent(TAB_CHANGE_EVENT_NAME, { bubbles: true, detail: { queryGroup, id } })
)
onClick?.(id)
if (id !== active) {
onChange?.(id)