docs: Set path on 404 errors (#46848)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* 404 handling now returns path-aware not-found pages for missing docs
and guides, improving accurate user-facing 404 responses.
* Improved file-missing errors for guides so missing content cases
surface clearer diagnostic info.

* **Chores**
* Enhanced 404 telemetry so missing-path information is recorded for
better monitoring and quicker troubleshooting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Chris Chinchilla authored and GitHub committed 2026-06-15 11:33:11 +02:00
1 parent ebcd052018
commit 954c861b11
11 files changed
+55 -36

No files matched your search

@@ -6,8 +6,8 @@ import {
wrapInMarkdownCodeBlock,
} from '~/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
import { source } from 'common-tags'
import { notFound } from 'next/navigation'
export const dynamicParams = false
@@ -18,7 +18,7 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str
const prompt = await getAiPrompt(slug)
if (!prompt) {
notFound()
notFoundWithPathname(`/guides/ai-tools/ai-prompts/${slug}`)
}
let { heading, content } = prompt
@@ -1,14 +1,13 @@
import { notFound } from 'next/navigation'
import { relative } from 'path'
import rehypeSlug from 'rehype-slug'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { getGitHubFileContents } from '~/lib/octokit'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import { getGitHubFileContents } from '~/lib/octokit'
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
import rehypeSlug from 'rehype-slug'
export const dynamicParams = false
@@ -76,7 +75,7 @@ const getContent = async ({ slug }: Params) => {
const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug)
if (!page) {
notFound()
notFoundWithPathname(`/guides/ai/python/${slug}`)
}
const { remoteFile, meta } = page
@@ -5,6 +5,7 @@ import {
genGuidesStaticParams,
removeRedundantH1,
} from '~/features/docs/GuidesMdx.utils'
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
import { newEditLink } from '~/features/helpers.edit-link'
import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from '~/features/ui/guide'
// End of third-party imports
@@ -20,7 +21,6 @@ import type { SerializeOptions } from '~/types/next-mdx-remote-serialize'
import { isFeatureEnabled } from 'common'
import matter from 'gray-matter'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import rehypeSlug from 'rehype-slug'
import emoji from 'remark-emoji'
import { Button } from 'ui'
@@ -334,11 +334,14 @@ interface Params {
}
const WrappersDocs = async (props: { params: Promise<Params> }) => {
const params = await props.params
if (!isFeatureEnabled('docs:fdw')) {
notFound()
notFoundWithPathname(
`/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}`
)
}
const params = await props.params
const { isExternal, meta, assetsBaseUrl, ...data } = await getContent(params)
// Create a combined URL transformer that handles both regular URLs and asset URLs
@@ -1,15 +1,14 @@
import { notFound } from 'next/navigation'
import { relative } from 'node:path'
import rehypeSlug from 'rehype-slug'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { getGitHubFileContents } from '~/lib/octokit'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
import { linkTransform, UrlTransformFunction } 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'
import { getGitHubFileContents } from '~/lib/octokit'
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
import rehypeSlug from 'rehype-slug'
export const dynamicParams = false
@@ -75,7 +74,7 @@ const getContent = async ({ slug }: Params) => {
const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug)
if (!page) {
notFound()
notFoundWithPathname(`/guides/deployment/ci/${slug}`)
}
const { remoteFile, meta } = page
@@ -1,5 +1,6 @@
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
import { getEmptyArray } from '~/features/helpers.fn'
import { IS_DEV } from '~/lib/constants'
import { isValidGuideFrontmatter } from '~/lib/docs'
@@ -10,7 +11,6 @@ import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
import { getGitHubFileContents } from '~/lib/octokit'
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
import matter from 'gray-matter'
import { notFound } from 'next/navigation'
import rehypeSlug from 'rehype-slug'
import {
@@ -106,7 +106,7 @@ const getContent = async ({ slug }: Params) => {
const page = pageMap.find((page) => page.slug === requestedSlug)
if (!page) {
notFound()
notFoundWithPathname(`/guides/deployment/terraform${slug?.length ? `/${slug.join('/')}` : ''}`)
}
const { meta, remoteFile, useRoot } = page
@@ -1,6 +1,7 @@
import { isAbsolute, relative } from 'path'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
import { getEmptyArray } from '~/features/helpers.fn'
import { IS_DEV } from '~/lib/constants'
import { linkTransform, UrlTransformFunction } from '~/lib/mdx/plugins/rehypeLinkTransform'
@@ -9,7 +10,6 @@ import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
import { getGitHubFileContents } from '~/lib/octokit'
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
import { notFound } from 'next/navigation'
import rehypeSlug from 'rehype-slug'
// We fetch these docs at build time from an external repo
@@ -128,7 +128,7 @@ const getContent = async ({ slug }: Params) => {
const page = pageMap.find((page) => page.slug === slug?.at(0))
if (!page) {
notFound()
notFoundWithPathname(`/guides/graphql${slug?.length ? `/${slug.join('/')}` : ''}`)
}
const { remoteFile, meta } = page
@@ -1,5 +1,4 @@
import { notFound } from 'next/navigation'
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
import TroubleshootingPage from '~/features/docs/Troubleshooting.page'
import { getAllTroubleshootingEntries, getArticleSlug } from '~/features/docs/Troubleshooting.utils'
import { PROD_URL } from '~/lib/constants'
@@ -20,7 +19,7 @@ export default async function TroubleshootingEntryPage(props: {
const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry) === slug)
if (!entry) {
notFound()
notFoundWithPathname(`/guides/troubleshooting/${slug}`)
}
return <TroubleshootingPage entry={entry} />
+5 -5
View File
@@ -1,6 +1,5 @@
import { notFound } from 'next/navigation'
import { REFERENCES } from '~/content/navigation.references'
import { notFoundWithPathname } from '~/features/docs/notFound.utils'
import { ApiReferencePage } from '~/features/docs/Reference.apiPage'
import { CliReferencePage } from '~/features/docs/Reference.cliPage'
import { ClientSdkReferencePage } from '~/features/docs/Reference.sdkPage'
@@ -18,9 +17,10 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr
const params = await props.params
const { slug } = params
const referencePath = `/reference/${slug.join('/')}`
if (!Object.keys(REFERENCES).includes(slug[0].replaceAll('-', '_'))) {
notFound()
notFoundWithPathname(referencePath)
}
const parsedPath = parseReferencePath(slug)
@@ -34,7 +34,7 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr
const sdkData = REFERENCES[sdkId]
if (sdkData.enabled === false) {
notFound()
notFoundWithPathname(referencePath)
}
const latestVersion = sdkData.versions[0]
@@ -52,7 +52,7 @@ export default async function ReferencePage(props: { params: Promise<{ slug: Arr
<SelfHostingReferencePage service={parsedPath.service} servicePath={parsedPath.servicePath} />
)
} else {
notFound()
notFoundWithPathname(referencePath)
}
}
+4 -4
View File
@@ -14,10 +14,10 @@ import { fromMarkdown } from 'mdast-util-from-markdown'
import { gfmFromMarkdown } from 'mdast-util-gfm'
import { gfm } from 'micromark-extension-gfm'
import { type Metadata, type ResolvingMetadata } from 'next'
import { notFound } from 'next/navigation'
import { newEditLink } from './GuidesMdx.template'
import { checkGuidePageEnabled } from './NavigationPageStatus.utils'
import { notFoundWithPathname } from './notFound.utils'
const { metadataTitle } = getCustomContent(['metadata:title'])
@@ -56,7 +56,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => {
!fullPath.startsWith(GUIDES_DIRECTORY) ||
!PUBLISHED_SECTIONS.some((section) => relPath.startsWith(section))
) {
notFound()
notFoundWithPathname(guidesPath)
}
/**
@@ -65,7 +65,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => {
*/
if (!checkGuidePageEnabled(guidesPath)) {
console.log('Page is disabled: %s', guidesPath)
notFound()
notFoundWithPathname(guidesPath)
}
try {
@@ -100,7 +100,7 @@ const getGuidesMarkdownInternal = async (slug: string[]) => {
)
Sentry.captureException(error)
}
notFound()
notFoundWithPathname(guidesPath)
}
}
+19
View File
@@ -0,0 +1,19 @@
import * as Sentry from '@sentry/nextjs'
import { notFound } from 'next/navigation'
/**
* Triggers a Next.js 404 while recording the requested path as a Sentry tag.
*
* `notFound()` itself does not create a Sentry event, but tagging the active
* scope means any error captured during the same request (and any event the
* Sentry SDK records for the render) carries the exact path under
* `404.pathname`. That makes 404s filterable and groupable in Sentry/Discover
* instead of only being inferable from the route's transaction name.
*
* @param pathname - The user-facing path that was not found, e.g.
* `/guides/functions/runtimes/node-22`.
*/
export function notFoundWithPathname(pathname: string): never {
Sentry.setTag('404.pathname', pathname)
notFound()
}
@@ -1,13 +1,13 @@
import matter from 'gray-matter'
import { promises as fs } from 'node:fs'
import { join, relative, resolve } from 'node:path'
import { extractMessageFromAnyError, FileNotFoundError, MultiError } from '~/app/api/utils'
import { preprocessMdxWithDefaults } from '~/features/directives/utils'
import { checkGuidePageEnabled } from '~/features/docs/NavigationPageStatus.utils'
import { Both, Result } from '~/features/helpers.fn'
import { GUIDES_DIRECTORY } from '~/lib/docs'
import { processMdx } from '~/scripts/helpers.mdx'
import matter from 'gray-matter'
import { GuideModel } from './guideModel'
/**
@@ -140,7 +140,7 @@ export class GuideModelLoader {
},
(error) => {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
throw new FileNotFoundError('', error)
throw new FileNotFoundError(join(GUIDES_DIRECTORY, relPath), error)
}
throw new Error(
`Failed to load guide from ${relPath}: ${extractMessageFromAnyError(error)}`,