mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
The www root markdown lived at an accidental URL: `/.md` served the
homepage markdown only because middleware strips the `.md` suffix and
the empty slug fell through to the homepage allowlist entry, while the
canonical-looking `/index.md` 404'd. The served markdown also opened
with stale legacy positioning copy that no longer matches the site. I
renamed the homepage content slug to `index` end-to-end so `/index.md`
is the one canonical markdown URL.
**Changed:**
- **`/index.md` serves the homepage markdown (200 `text/markdown`)**:
`content/md/homepage.md` renamed to `index.md`; the middleware bare-root
slug mapping, the generator's sort special-case, and the homepage
alternate tag follow, so the tag now advertises `/index.md`.
- **Legacy aliases 308 to the canonical URL**: `/.md`, `/homepage.md`,
and bare `/index` redirect via `lib/redirects.js`; `/llms/homepage.txt`
retargeted straight to `/index.md` to avoid a redirect chain. New
`next.config.test.ts` assertions pin all four.
- **Positioning refreshed**: the markdown now opens with "Supabase is
the Postgres development platform" (matching the site title), replacing
the outdated tagline.
- **Generator safety**: the redirect-exclusion filter in
`generateMdContent.mjs` now exempts the `index` slug (its HTML page is
`/`, not `/index`, so a `/index` redirect never refers to it), and the
build fails if `content/md/index.md` ever goes missing while middleware
still maps `/` to the `index` slug.
- **CI actually runs the new assertions**: I widened the `www-tests.yml`
paths filter to include `apps/www/lib/**/*.js`,
`apps/www/content/md/**`, and `apps/www/scripts/**/*.mjs`. It previously
only matched `.ts*` and the next.config files, so a PR touching only
`lib/redirects.js`, the markdown content, or the generator would skip
the tests that pin these redirects.
**Note:** the existing homepage alternate tag still exists, re-pointed
to the canonical URL. Whether the homepage should advertise a markdown
sibling at all is a separate decision; leaving it aimed at a 308 would
break tag consumers. Positioning wording is editorial, happy to tweak.
## To test
Tested on Vercel preview:
- [x] `curl -si <preview>/index.md`: expect 200 `content-type:
text/markdown`, body opens with the Postgres development platform
positioning and no longer contains the old tagline
- [x] `curl -sI <preview>/.md`: expect 308 with `location: /index.md`
- [x] `curl -sI <preview>/homepage.md` and `curl -sI
<preview>/llms/homepage.txt`: expect 308 with `location: /index.md`
- [x] `curl -sI <preview>/index`: expect 308 with `location: /`
- [x] `curl -s -H "Accept: text/markdown" -o /dev/null -w "%{http_code}
%{content_type}" <preview>/`: expect `200 text/markdown` (bare-URL
negotiation unchanged)
- [x] `curl -s <preview>/ | grep -o 'type="text/markdown"
href="[^"]*"'`: expect href ending `/index.md`
## Linear
- fixes GROWTH-1117
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added support for `/index.md` as the canonical Markdown representation
of the homepage.
- Added permanent redirects for legacy homepage Markdown and text URLs.
- Added `/index` to `/` redirect handling.
- **Bug Fixes**
- Updated homepage metadata, alternate links, Markdown negotiation, and
content generation to consistently use the new canonical path.
- Improved homepage content description.
- **Tests**
- Expanded coverage for homepage Markdown routes, redirects, and URL
matching.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
117 lines
4.3 KiB
TypeScript
117 lines
4.3 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 redirects = (await config.redirects?.()) || []
|
|
|
|
expect(rewrites).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('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',
|
|
})
|
|
})
|
|
})
|