enhance: move not-found data fetch to client side (#27781)

Fetching recommendations on the server takes too long before the first
load, so it's faster to load the basic not-found page and generate the
recommendations on the client side.
This commit is contained in:
Charis authored and GitHub committed 2024-07-04 15:23:19 +00:00
1 parent cb9eff3c0c
commit 282bd2c73f
3 files changed
+154 -79

No files matched your search

+30 -21
View File
@@ -1,11 +1,11 @@
import { createClient } from '@supabase/supabase-js'
import { type Metadata } from 'next'
import Link from 'next/link'
import { DocsSearchResult, type Database } from 'common'
import { Button } from 'ui'
import { NotFound } from '~/features/recommendations/NotFound.client'
export const fetchCache = 'force-no-store'
import { SearchButton, Recommendations } from '~/features/recommendations/NotFound.client'
const metadata: Metadata = {
robots: {
@@ -15,25 +15,34 @@ const metadata: Metadata = {
}
const NotFoundPage = async ({ searchParams: { page } }: { searchParams: { page?: string } }) => {
const recommendations = page ? await getRecommendations(page) : null
return <NotFound recommendations={recommendations} />
}
const getRecommendations = async (page: string) => {
const query = decodeURIComponent(page.replace(/^\/(?:guides|reference)\//, '')).replace(
/[_\/-]/g,
' '
return (
<article className="prose max-w-[80ch]">
<h1>404: We couldn&apos;t find that page</h1>
<p>
Sorry, we couldn&apos;t find that page. It might be missing, or we had a temporary error
generating it.
</p>
<div className="flex flex-wrap gap-4 pt-4">
<SearchButton />
<Button type="default" size="small" className="p-4" asChild>
<Link href="/" className="no-underline">
Return to homepage
</Link>
</Button>
<Button type="text" size="small" asChild>
<Link
href="https://github.com/supabase/supabase/issues/new?assignees=&labels=documentation&projects=&template=2.Improve_docs.md"
target="_blank"
rel="noreferrer noopener"
className="no-underline"
>
Report missing page
</Link>
</Button>
</div>
<Recommendations page={page} />
</article>
)
if (!query) return null
const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
const { data, error } = await supabase.rpc('docs_search_fts', { query })
if (error || !data?.length) return null
return data as Omit<DocsSearchResult, 'sections'>[]
}
export default NotFoundPage
@@ -1,69 +1,101 @@
'use client'
import { useSupabaseClient } from '@supabase/auth-helpers-react'
import { type SupabaseClient } from '@supabase/supabase-js'
import { X } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useCommandMenu } from '@ui-patterns/Cmdk'
import { GenericSkeletonLoader } from '@ui-patterns/ShimmeringLoader'
import { type DocsSearchResult } from 'common'
import Link from 'next/link'
import { Button } from 'ui'
import { useCommandMenu } from 'ui-patterns/Cmdk'
import { Button, cn } from 'ui'
import ButtonCard from '~/components/ButtonCard'
const NotFound = ({
recommendations,
omitSearch = false,
}: {
recommendations?: Omit<DocsSearchResult, 'sections'>[] | null
omitSearch?: boolean
}) => {
function SearchButton() {
const { setIsOpen: setCommandMenuOpen } = useCommandMenu()
return (
<article className="prose max-w-[80ch]">
<h1>We couldn&apos;t find that page</h1>
<p>
Sorry, we couldn&apos;t find that page. It might be missing, or we had a temporary error
generating it.
</p>
<div className="flex flex-wrap gap-4 pt-4">
{!omitSearch && (
<Button type="primary" size="small" onClick={() => setCommandMenuOpen(true)}>
Search for page
</Button>
)}
<Button type="default" size="small" className="p-4" asChild>
<Link href="/" className="no-underline">
Return to homepage
</Link>
</Button>
<Button type="text" size="small" asChild>
<Link
href="https://github.com/supabase/supabase/issues/new?assignees=&labels=documentation&projects=&template=2.Improve_docs.md"
target="_blank"
rel="noreferrer noopener"
className="no-underline"
>
Report missing page
</Link>
</Button>
</div>
{recommendations && (
<section aria-labelledby="empty-page-recommendations" className="mt-20">
<h2 id="empty-page-recommendations">Are you looking for...?</h2>
<ul className="not-prose grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-4">
{recommendations
.filter(({ title }) => !!title)
.slice(0, 6)
.map(({ path, title, subtitle, description }) => (
<ButtonCard
key={path}
to={path}
title={title}
description={subtitle || description || undefined}
/>
))}
</ul>
</section>
)}
</article>
<Button type="primary" size="small" onClick={() => setCommandMenuOpen(true)}>
Search for page
</Button>
)
}
export { NotFound }
async function getRecommendations(page: string, supabase: SupabaseClient) {
try {
const query = decodeURIComponent(page.replace(/^\/(?:guides|reference)\//, '')).replace(
/[_\/-]/g,
' '
)
if (!query) return []
const { data, error } = await supabase.rpc('docs_search_fts', { query })
if (error || !data?.length) return []
return data as Array<Omit<DocsSearchResult, 'sections'>>
} catch (err) {
console.error(err)
return []
}
}
function Recommendations({ page }: { page: string }) {
const supabase = useSupabaseClient()
const [recommendations, setRecommendations] = useState(
[] as Array<Omit<DocsSearchResult, 'sections'>>
)
const fetched = useRef(false)
useEffect(() => {
getRecommendations(page, supabase).then((data) => {
if (!fetched.current) {
setRecommendations(data)
fetched.current = true
}
})
}, [page, supabase])
return (
<section aria-labelledby="empty-page-recommendations" className="min-h-96 mt-20">
<h2 id="empty-page-recommendations">Are you looking for...?</h2>
{!fetched.current && <LoadingState />}
{fetched.current && recommendations.length === 0 && <NoResults />}
{fetched.current && recommendations.length > 0 && (
<RecommendationsList recommendations={recommendations} />
)}
</section>
)
}
function LoadingState() {
return <GenericSkeletonLoader />
}
function NoResults() {
return <span>No recommendations found.</span>
}
function RecommendationsList({
recommendations,
}: {
recommendations: Array<Omit<DocsSearchResult, 'sections'>>
}) {
return (
<ul className={cn('not-prose', 'grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-4')}>
{recommendations
.filter(({ title }) => !!title)
.slice(0, 6)
.map(({ path, title, subtitle, description }) => (
<ButtonCard
key={path}
to={path}
title={title}
description={subtitle || description || undefined}
/>
))}
</ul>
)
}
export { SearchButton, Recommendations }
+34
View File
@@ -302,3 +302,37 @@ th code {
[data-rmiz-modal-img] {
image-rendering: high-quality;
}
/* Loaders */
.shimmering-loader {
animation: shimmer 2s infinite linear;
background: linear-gradient(
to right,
hsl(var(--border-default)) 4%,
hsl(var(--background-surface-200)) 25%,
hsl(var(--border-default)) 36%
);
background-size: 1000px 100%;
}
.dark .shimmering-loader {
animation: shimmer 2s infinite linear;
background: linear-gradient(
to right,
hsl(var(--border-default)) 4%,
hsl(var(--border-control)) 25%,
hsl(var(--border-default)) 36%
);
background-size: 1000px 100%;
}
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}