mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Nonexistent paths return a real 404 everywhere, but always with an HTML body, even when the client asked for markdown via `Accept: text/markdown` or a `.md` suffix. Middleware can't fix this: it gates on a static slug allowlist and can't know a path will 404. I added two `fallback` rewrites (`.md` suffix; Accept header containing `text/markdown` or `text/*`) that run only after every route has failed to match and route the request to a small `md-404` handler returning a short markdown 404 pointing at /docs, /sitemap.xml, and /llms.txt. Real pages are structurally unaffected. **Note:** `lib/rewrites.js` is untouched (the plain rewrites array became the `afterFiles` phase), so #49587 merges independently. I updated next.config.test.ts's rewrites assertion for the phased shape; it now also pins the two fallback rules. ## To test I verified on the Vercel preview: - [x] `curl -s -D - -H "Accept: text/markdown" <preview>/definitely-not-a-page` (404, `Content-Type: text/markdown`, body with the three pointers) - [x] Same URL with a browser Accept header (existing HTML 404, unchanged) - [x] `curl -s -D - <preview>/definitely-not-a-page.md` (markdown 404) - [x] `curl -s -D - -H "Accept: text/markdown" <preview>/auth` (200 markdown, unchanged) and `<preview>/support` (200 HTML, unchanged) - [x] `/homepage.md` still 308s to `/index.md` (redirects phase wins); `Accept: text/*` gets the markdown 404, matching real-page negotiation Known boundary: `/changelog/<unknown>` keeps the HTML 404 body (pages-router `fallback: 'blocking'` routes take priority over fallback rewrites per Next docs); the status is still 404, verified on the preview. ## Linear - fixes GROWTH-1142 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added Markdown-formatted 404 responses for unmatched documentation and `.md` page requests. - Included helpful documentation links in not-found responses. - Requests that explicitly accept Markdown now receive a consistent Markdown response. - Added appropriate response headers for security, caching, and content variation. - **Bug Fixes** - Improved routing for unmatched Markdown paths, ensuring they are handled by the appropriate not-found response instead of returning an unexpected format. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
145 lines
5.8 KiB
TypeScript
145 lines
5.8 KiB
TypeScript
import type { NextConfig } from 'next'
|
|
import { getPathMatch } from 'next/dist/shared/lib/router/utils/path-match'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
vi.mock('@sentry/nextjs', () => ({
|
|
withSentryConfig: (configFn: any) => (typeof configFn === 'function' ? configFn() : configFn),
|
|
}))
|
|
|
|
describe('next.config.mjs', () => {
|
|
it('expect the headers to always have X-Robots-Tag', async () => {
|
|
const { default: config } = (await import('./next.config.mjs')) as { default: NextConfig }
|
|
const headers = (await config.headers?.()) || []
|
|
|
|
expect(headers).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
source: '/(docs|blog)/:path*',
|
|
headers: [{ key: 'X-Robots-Tag', value: 'all' }],
|
|
}),
|
|
expect.objectContaining({
|
|
source: '/dashboard/:path*',
|
|
headers: [{ key: 'X-Robots-Tag', value: 'noindex' }],
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
|
|
it('routes the library and permanently redirects the legacy UI URLs', async () => {
|
|
const { default: config } = (await import('./next.config.mjs')) as { default: NextConfig }
|
|
const rewrites = await config.rewrites?.()
|
|
const afterFiles = (rewrites && 'afterFiles' in rewrites && rewrites.afterFiles) || []
|
|
const redirects = (await config.redirects?.()) || []
|
|
|
|
expect(afterFiles).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ source: '/library' }),
|
|
expect.objectContaining({ source: '/library/:path*' }),
|
|
])
|
|
)
|
|
expect(redirects).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
source: '/images/customers/logos/light/:path*',
|
|
destination: '/images/customers/logos/on-dark/:path*',
|
|
permanent: true,
|
|
}),
|
|
expect.objectContaining({
|
|
source: '/images/customers/logos/:slug((?!dreambase-mark\\.png)[^/.]+).png',
|
|
destination: '/images/customers/logos/on-light/:slug.png',
|
|
permanent: true,
|
|
}),
|
|
expect.objectContaining({ source: '/ui', destination: '/library', permanent: true }),
|
|
expect.objectContaining({
|
|
source: '/ui/:path*',
|
|
destination: '/library/:path*',
|
|
permanent: true,
|
|
}),
|
|
])
|
|
)
|
|
expect(
|
|
redirects.findIndex((redirect) => redirect.source === '/ui/docs/ai-editors-rules/prompts')
|
|
).toBeLessThan(redirects.findIndex((redirect) => redirect.source === '/ui/:path*'))
|
|
})
|
|
|
|
it('routes unmatched markdown-negotiated paths to the md-404 handler via fallback rewrites', async () => {
|
|
const { default: config } = (await import('./next.config.mjs')) as { default: NextConfig }
|
|
const rewrites = await config.rewrites?.()
|
|
const fallback = (rewrites && 'fallback' in rewrites && rewrites.fallback) || []
|
|
|
|
const mdSuffixRule = fallback.find((rule) => rule.destination === '/api-v2/md-404/:path')
|
|
const acceptRule = fallback.find((rule) => rule.destination === '/api-v2/md-404/:path*')
|
|
|
|
expect(mdSuffixRule).toBeDefined()
|
|
expect(acceptRule?.has).toEqual([
|
|
{ type: 'header', key: 'accept', value: '.*text/(markdown|\\*).*' },
|
|
])
|
|
|
|
expect(getPathMatch(mdSuffixRule!.source)('/definitely-not-a-page.md')).toBeTruthy()
|
|
expect(getPathMatch(mdSuffixRule!.source)('/nested/definitely/not-a-page.md')).toBeTruthy()
|
|
expect(getPathMatch(mdSuffixRule!.source)('/definitely-not-a-page')).toBe(false)
|
|
expect(getPathMatch(acceptRule!.source)('/definitely-not-a-page')).toBeTruthy()
|
|
|
|
const acceptHeaderRegex = new RegExp(`^${acceptRule!.has![0].value}$`)
|
|
expect(acceptHeaderRegex.test('text/markdown')).toBe(true)
|
|
expect(acceptHeaderRegex.test('text/html, text/markdown;q=0.9')).toBe(true)
|
|
expect(acceptHeaderRegex.test('text/*')).toBe(true)
|
|
expect(
|
|
acceptHeaderRegex.test('text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')
|
|
).toBe(false)
|
|
})
|
|
|
|
it('permanently redirects the legacy root markdown aliases to /index.md', async () => {
|
|
const { default: config } = (await import('./next.config.mjs')) as { default: NextConfig }
|
|
const redirects = (await config.redirects?.()) || []
|
|
|
|
for (const source of ['/.md', '/homepage.md', '/llms/homepage.txt']) {
|
|
expect(redirects).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ source, destination: '/index.md', permanent: true }),
|
|
])
|
|
)
|
|
}
|
|
|
|
expect(getPathMatch('/.md')('/.md')).toBeTruthy()
|
|
expect(getPathMatch('/.md')('/foo.md')).toBe(false)
|
|
|
|
expect(redirects).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ source: '/index', destination: '/', permanent: true }),
|
|
])
|
|
)
|
|
expect(getPathMatch('/index')('/index.md')).toBe(false)
|
|
})
|
|
|
|
it('preserves the filename when redirecting legacy customer logos', async () => {
|
|
const { default: config } = (await import('./next.config.mjs')) as { default: NextConfig }
|
|
const redirects = (await config.redirects?.()) || []
|
|
|
|
const darkModeRedirect = redirects.find(
|
|
(redirect) => redirect.source === '/images/customers/logos/light/:path*'
|
|
)
|
|
const lightModeRedirect = redirects.find(
|
|
(redirect) =>
|
|
redirect.source === '/images/customers/logos/:slug((?!dreambase-mark\\.png)[^/.]+).png'
|
|
)
|
|
|
|
expect(darkModeRedirect).toBeDefined()
|
|
expect(lightModeRedirect).toBeDefined()
|
|
|
|
const matchLegacyDark = getPathMatch(darkModeRedirect!.source)
|
|
const matchLegacyLight = getPathMatch(lightModeRedirect!.source)
|
|
|
|
expect(matchLegacyDark('/images/customers/logos/light/good-tape.png')).toEqual({
|
|
path: ['good-tape.png'],
|
|
})
|
|
expect(matchLegacyLight('/images/customers/logos/good-tape.png')).toEqual({
|
|
slug: 'good-tape',
|
|
})
|
|
expect(matchLegacyLight('/images/customers/logos/dreambase-mark.png')).toBe(false)
|
|
expect(matchLegacyLight('/images/customers/logos/dreambase-marketing.png')).toEqual({
|
|
slug: 'dreambase-marketing',
|
|
})
|
|
})
|
|
})
|