mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
cb9eff3c0c
commit
282bd2c73f
3 files changed
+154
-79
No files matched your search
@@ -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't find that page</h1>
|
||||
<p>
|
||||
Sorry, we couldn'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't find that page</h1>
|
||||
<p>
|
||||
Sorry, we couldn'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 }
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user