Files
supabase/apps/www/next.config.mjs
Greg Richardson e3f04a8384 fix: redirects for docs/guides markdown files (#45379)
## Problem

We added support for `*.md` guides in our docs, but our redirects don't
apply to them. This means that when we directly link to a guide using
the `.md` extension and that guide gets renamed or deleted, the links
begin to 404.

This happened already once in our agent-skills where we linked to
`https://supabase.com/docs/guides/database/data-api.md` which was
deleted and moved yesterday to
`https://supabase.com/docs/guides/api/securing-your-api.md`. Note that
there was a redirect on the regular path `/docs/guides/database/data-api
-> /docs/guides/api/securing-your-api`, but this didn't apply to the
`.md` version.

## Fix

This PR adds rules to redirect all the `/docs/guides/**/*.md` files to
their respective pages. Rather than manually duplicating all our
existing (and future) redirects by hand for `.md`, this dynamically
generates the `.md` redirect rules based on the path. Specifically it
assumes that all redirect rules under `/docs/guides` support the `.md`
extension, so it generates a redirect for all of these rules
automatically.

## How to test

Use curl to confirm that `.md` redirects are applied:

```shell
curl -I https://zone-www-dot-com-git-fix-doc-markdown-redirects-supabase.vercel.app/docs/guides/database/data-api.md

HTTP/2 308
cache-control: public, max-age=0, must-revalidate
content-type: text/plain
date: Wed, 29 Apr 2026 17:35:58 GMT
location: /docs/guides/api/securing-your-api.md
...
```

You can also verify that this didn't previously work:

```shell
curl -I https://supabase.com/docs/guides/database/data-api.md

HTTP/2 404
age: 0
cache-control: public, max-age=0, must-revalidate
content-type: text/plain;charset=UTF-8
date: Wed, 29 Apr 2026 17:38:00 GMT
...
```

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

## Summary by CodeRabbit

* **Bug Fixes**
* Enhanced documentation route redirects to support markdown file
extensions for guides, ensuring proper navigation for both standard and
markdown-variant paths.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-30 13:41:22 -06:00

259 lines
7.4 KiB
JavaScript

import { remarkCodeHike } from '@code-hike/mdx'
import bundleAnalyzer from '@next/bundle-analyzer'
import nextMdx from '@next/mdx'
import { withSentryConfig } from '@sentry/nextjs'
import codeHikeTheme from 'config/code-hike.theme.json' with { type: 'json' }
import rehypeSlug from 'rehype-slug'
import remarkGfm from 'remark-gfm'
import redirects from './lib/redirects.js'
import remotePatterns from './lib/remotePatterns.js'
import rewrites from './lib/rewrites.js'
const withMDX = nextMdx({
extension: /\.mdx?$/,
options: {
remarkPlugins: [
[
remarkCodeHike,
{
theme: codeHikeTheme,
lineNumbers: true,
showCopyButton: true,
},
],
remarkGfm,
],
rehypePlugins: [rehypeSlug],
// This is required for `MDXProvider` component
providerImportSource: '@mdx-js/react',
},
})
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
basePath: '',
assetPrefix: getAssetPrefix(),
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
trailingSlash: false,
transpilePackages: [
'ui',
'ui-patterns',
'common',
'shared-data',
'icons',
'api-types',
'marketing',
// needed to make the octokit packages work in /changelog
'@octokit/plugin-paginate-graphql',
],
experimental: {
// needed to make the octokit packages work in /changelog
esmExternals: 'loose',
},
/**
* Exclude huge directories from being traced into serverless functions
* to avoid the max size limit for Serverless Functions on Vercel:
* https://vercel.com/guides/troubleshooting-function-250mb-limit
*/
outputFileTracingExcludes: {
'*': [
// Next.js build artifacts
'.next/cache/**/*',
'.next/static/**/*',
// Static assets
'public/**/*',
],
},
reactStrictMode: true,
images: {
dangerouslyAllowSVG: false,
remotePatterns,
},
async headers() {
return [
{
source: '/changelog-rss.xml',
headers: [
{ key: 'Content-Type', value: 'application/rss+xml; charset=utf-8' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Cache-Control', value: 'public, s-maxage=900, stale-while-revalidate=900' },
],
},
{
source: '/changelog-rss/:slug.xml',
headers: [
{ key: 'Content-Type', value: 'application/rss+xml; charset=utf-8' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Cache-Control', value: 'public, s-maxage=900, stale-while-revalidate=900' },
],
},
{
source: '/changelog/:slug.md',
headers: [
{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Cache-Control', value: 'public, s-maxage=900, stale-while-revalidate=900' },
{ key: 'Vary', value: 'Accept' },
],
},
{
source: '/changelog.md',
headers: [
{ key: 'Content-Type', value: 'text/markdown; charset=utf-8' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Cache-Control', value: 'public, s-maxage=900, stale-while-revalidate=900' },
{ key: 'Vary', value: 'Accept' },
],
},
{
source: '/',
headers: [
{
key: 'Link',
value: '</.well-known/api-catalog>; rel="api-catalog"',
},
],
},
{
source: '/.well-known/api-catalog',
headers: [
{ key: 'content-type', value: 'application/linkset+json' },
{ key: 'access-control-allow-origin', value: '*' },
],
},
{
source: '/.well-known/vercel/flags',
headers: [
{
key: 'content-type',
value: 'application/json',
},
],
},
{
source: '/favicon/:slug*',
headers: [{ key: 'cache-control', value: 'public, max-age=86400' }],
},
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value:
process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && process.env.VERCEL === '1'
? 'max-age=31536000; includeSubDomains; preload'
: '',
},
],
},
{
source: '/(docs|blog)/:path*',
headers: [
{
key: 'X-Robots-Tag',
value: 'all',
},
],
},
{
source: '/dashboard/:path*',
headers: [
{
key: 'X-Robots-Tag',
value: 'noindex',
},
],
},
{
source: '/enterprise-terms',
headers: [
{
key: 'X-Robots-Tag',
value: 'noindex, nofollow',
},
],
},
]
},
async rewrites() {
return rewrites
},
async redirects() {
// For /docs/guides/ redirects, auto-generate .md variants so renamed/deleted pages
// redirect correctly when fetched as markdown
const docsMdRedirects = redirects
.filter(
(r) =>
r.source.startsWith('/docs/guides/') &&
typeof r.destination === 'string' &&
r.destination.startsWith('/')
)
.map((r) => ({ ...r, source: `${r.source}.md`, destination: `${r.destination}.md` }))
return [...docsMdRedirects, ...redirects]
},
typescript: {
// On previews, typechecking is run via GitHub Action only for efficiency
// On production, we turn it on to prevent errors from conflicting PRs getting into
// prod
ignoreBuildErrors: process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' ? false : true,
},
eslint: {
// We are already running linting via GH action, this will skip linting during production build on Vercel.
ignoreDuringBuilds: true,
},
}
// next.config.js.
const configExport = () => {
const plugins = [withMDX, withBundleAnalyzer]
return plugins.reduce((acc, next) => next(acc), nextConfig)
}
export default withSentryConfig(configExport, {
// For all available options, see:
// https://www.npmjs.com/package/@sentry/webpack-plugin#options
org: 'supabase',
project: 'www',
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
// tunnelRoute: "/monitoring",
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
})
function getAssetPrefix() {
// If not force enabled, but not production env, disable CDN
if (process.env.FORCE_ASSET_CDN !== '1' && process.env.VERCEL_ENV !== 'production') {
return undefined
}
// Force disable CDN
if (process.env.FORCE_ASSET_CDN === '-1') {
return undefined
}
return `https://frontend-assets.supabase.com/${process.env.SITE_NAME}/${process.env.VERCEL_GIT_COMMIT_SHA.substring(0, 12)}`
}