mirror of
https://github.com/supabase/supabase.git
synced 2026-05-07 06:27:16 +08:00
feat(docs): return page suggestions in markdown 404 pages (#45439)
Markdown guides (`/docs/guides/**/*.md`) that 404 currently return a text/plain `Not found` response. Since agents often hallucinate URL paths, this PR proactively provides page suggestions so that agents can find the page they are looking forward without further guessing. It uses the docs Content API to fetch related pages, similar to the [HTML 404 page](https://supabase.com/docs/404): ``` # 404 - Page Not Found The page `/docs/guides/mcp.md` does not exist. ## You might be looking for... - [Model context protocol (MCP)](https://supabase.com/docs/guides/getting-started/mcp.md) - [Building an MCP Server with mcp-lite](https://supabase.com/docs/guides/functions/examples/mcp-server-mcp-lite.md) - [Model Context Protocol (MCP) Authentication](https://supabase.com/docs/guides/auth/oauth-server/mcp-authentication.md) - [Deploy MCP servers](https://supabase.com/docs/guides/getting-started/byo-mcp.md) - [Enabling MCP Server Access](https://supabase.com/docs/guides/self-hosting/enable-mcp.md) See also: [Changelog](https://supabase.com/changelog.md) ``` ## How to test 1. Use curl to fetch a non-existent page with an `.md` extension: ```shell curl -i https://docs-git-docs-markdown-404-suggestions-supabase.vercel.app/docs/guides/mcp.md ``` Confirm that relevant pages are suggested (ballpark - our search algo needs some improvement). Also confirm that the response has content type `text/markdown`. 2. Use curl to fetch a non-existent page using the `Accept: text/markdown` header ```shell curl -i -H 'Accept: text/markdown' \ https://docs-git-docs-markdown-404-suggestions-supabase.vercel.app/docs/guides/mcp ``` And confirm the same result as 1. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * 404 pages for missing guides now return a formatted Markdown response with proper content-type and no-cache headers, and include up to five related documentation suggestions to help users find relevant content. * **Chores** * Build environment now preserves an additional hosting URL variable to improve build/task consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { BASE_PATH, IS_PRODUCTION, PROD_URL } from '~/lib/constants'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string[] }> }) {
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ slug: string[] }> }) {
|
||||
const { slug } = await params
|
||||
const baseDir = path.join(process.cwd(), 'public/docs/guides')
|
||||
const filePath = path.join(baseDir, `${slug.join('/')}.md`)
|
||||
|
||||
if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir) {
|
||||
return new NextResponse('Not found', { status: 404 })
|
||||
return notFoundResponse(request, slug)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -20,6 +21,86 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
return new NextResponse('Not found', { status: 404 })
|
||||
return notFoundResponse(request, slug)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the docs base URL from trusted sources only.
|
||||
* Avoids host-header-driven SSRF from request origin.
|
||||
*/
|
||||
function resolveDocsBaseUrl() {
|
||||
if (IS_PRODUCTION) return PROD_URL
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}${BASE_PATH}`
|
||||
if (process.env.NEXT_PUBLIC_SITE_URL) return `${process.env.NEXT_PUBLIC_SITE_URL}${BASE_PATH}`
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function notFoundResponse(_request: Request, slug: string[]) {
|
||||
const baseUrl = resolveDocsBaseUrl()
|
||||
const query = slug.join(' ').replace(/[_-]/g, ' ')
|
||||
const suggestions = baseUrl ? await searchForSuggestions(baseUrl, query) : []
|
||||
const markdown = buildNotFoundMarkdown(slug, suggestions)
|
||||
return new NextResponse(markdown, {
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface SearchSuggestion {
|
||||
title: string
|
||||
href: string
|
||||
}
|
||||
|
||||
async function searchForSuggestions(baseUrl: string, query: string): Promise<SearchSuggestion[]> {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
query SearchDocs($query: String!) {
|
||||
searchDocs(query: $query, limit: 5) {
|
||||
nodes {
|
||||
title
|
||||
href
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { query },
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) return []
|
||||
|
||||
const data = await response.json()
|
||||
return (data?.data?.searchDocs?.nodes ?? []).filter(
|
||||
(n: Partial<SearchSuggestion>): n is SearchSuggestion => !!n.title && !!n.href
|
||||
)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function buildNotFoundMarkdown(slug: string[], suggestions: SearchSuggestion[]): string {
|
||||
const pagePath = slug.join('/')
|
||||
|
||||
const suggestionsMd =
|
||||
suggestions.length > 0
|
||||
? `## You might be looking for...\n\n${suggestions
|
||||
.map(({ title, href }) => `- [${title}](${href}.md)`)
|
||||
.join('\n')}\n`
|
||||
: ''
|
||||
|
||||
return `# 404 - Page Not Found
|
||||
|
||||
The page \`/docs/guides/${pagePath}.md\` does not exist.
|
||||
|
||||
${suggestionsMd}
|
||||
See also: [Changelog](https://supabase.com/changelog.md)
|
||||
`
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"VERCEL",
|
||||
"VERCEL_ENV",
|
||||
"VERCEL_GIT_COMMIT_SHA",
|
||||
"VERCEL_URL",
|
||||
// These envs are used in the packages
|
||||
"NEXT_PUBLIC_STORAGE_KEY",
|
||||
"NEXT_PUBLIC_AUTH_DEBUG_KEY",
|
||||
|
||||
Reference in New Issue
Block a user