Files
supabase/apps/www/md-alternates.test.ts
T
Pamela Chia 8aade77966 fix(www): gate changelog md alternate on slug set (#49754)
Changelog entry pages advertised a `.md` alternate tag unconditionally
while the page is ISR, so an entry published in the changelog repo
between www deploys pointed agents at a `.md` sibling that 404s until
the next build (the static file and `CHANGELOG_PAGES` are both
build-time artifacts). PR #49357 made bare-URL negotiation fail closed
for those entries; I gate the advertising side here the same way.

**Changed:**
- **No more dead `.md` links on freshly published entries**:
`getStaticProps` passes a `hasMarkdownVariant` flag computed from
`CHANGELOG_PAGES` membership and the page renders the alternate tag only
when true. An entry published between deploys carries no tag until the
build that ships its `.md` file; the set reference stays inside
`getStaticProps`, so the generated module stays out of the client
bundle.
- **Drift coverage**: `md-alternates.test.ts` gains the changelog
direction, source-level like the existing `_app.tsx` drift test; the
assertion pins the full `CHANGELOG_PAGES.has(` +
backtick-`changelog/${entry.slug}`-backtick + `)` expression so a
dropped key prefix fails the suite, and removing the gate fails it too.

**Note:** without changelog sync secrets `CHANGELOG_PAGES` is empty, so
the tag never renders in local dev. Preview and prod are the
verification surface.

## To test
Tested on Vercel preview:
- [x] Open a published changelog entry page and view source: expect
`<link rel="alternate" type="text/markdown"
href="/changelog/<slug>.md">` in the head — observed exact href
`/changelog/19669-supavisor-1-0.md`
- [x] Fetch that href: expect 200 with `content-type: text/markdown` —
observed 200, `text/markdown; charset=utf-8`
- [x] (added) Client-side nav from `/changelog` into an entry: alternate
tag appears with that entry's slug; hopping to a second entry updates
the href (no stale tag)
- [x] (added) Navigating back to `/changelog`: entry tag gone; the index
shows its own pre-existing `/changelog.md` alternate (hardcoded in
`pages/changelog.tsx`, outside this diff), and `/changelog.md` returns
200 `text/markdown`
- [x] (added) Console: zero new errors across all scenarios vs page-load
baseline

## Linear
- fixes GROWTH-1120


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

* **Bug Fixes**
* Changelog pages now advertise a Markdown alternate link only when a
Markdown version is available.
* Prevented links to unavailable Markdown content from appearing on
changelog entries.

* **Tests**
* Added coverage to verify correct Markdown alternate detection and
rendering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-31 16:25:06 +08:00

130 lines
5.2 KiB
TypeScript

import { existsSync, promises as fs } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import { SITE_ORIGIN } from './lib/constants'
import { mdAlternates } from './lib/md-alternates'
const DYNAMIC_SLUGS = ['pricing']
const MDX_SECTIONS = ['blog', 'customers', 'events']
async function collectContentMdSlugs(dir: string, prefix = ''): Promise<string[]> {
const dirents = await fs.readdir(dir, { withFileTypes: true })
const slugs: string[] = []
for (const dirent of dirents) {
const slug = prefix ? `${prefix}/${dirent.name}` : dirent.name
if (dirent.isDirectory()) {
slugs.push(...(await collectContentMdSlugs(path.join(dir, dirent.name), slug)))
} else if (dirent.name.endsWith('.md')) {
slugs.push(slug.replace(/\.md$/, ''))
}
}
return slugs
}
async function collectAppRouterPages(
dir: string,
segments: string[] = []
): Promise<Map<string, string>> {
const pages = new Map<string, string>()
const dirents = await fs.readdir(dir, { withFileTypes: true })
for (const dirent of dirents) {
if (dirent.isDirectory()) {
if (dirent.name.startsWith('[') || dirent.name.startsWith('_')) continue
const nextSegments = dirent.name.startsWith('(') ? segments : [...segments, dirent.name]
const nested = await collectAppRouterPages(path.join(dir, dirent.name), nextSegments)
nested.forEach((filePath, slug) => pages.set(slug, filePath))
} else if (dirent.name === 'page.tsx') {
pages.set(segments.join('/') || 'index', path.join(dir, dirent.name))
}
}
return pages
}
describe('mdAlternates', () => {
it('emits the absolute .md sibling URL', () => {
expect(mdAlternates('auth')).toEqual({
types: { 'text/markdown': `${SITE_ORIGIN}/auth.md` },
})
expect(mdAlternates('modules/vector')).toEqual({
types: { 'text/markdown': `${SITE_ORIGIN}/modules/vector.md` },
})
})
})
describe('markdown alternate drift', () => {
it('every markdown-served slug has a page advertising its .md sibling', async () => {
const contentSlugs = await collectContentMdSlugs(path.join(process.cwd(), 'content/md'))
const expectedSlugs = [...contentSlugs, ...DYNAMIC_SLUGS]
const appPages = await collectAppRouterPages(path.join(process.cwd(), 'app'))
expect(contentSlugs.length).toBeGreaterThan(0)
for (const slug of expectedSlugs) {
const appPagePath = appPages.get(slug)
if (appPagePath) {
const source = await fs.readFile(appPagePath, 'utf-8')
const wiringShapes = [`alternates: mdAlternates('${slug}')`, `...mdAlternates('${slug}')`]
expect(
wiringShapes.some((shape) => source.includes(shape)),
`${path.relative(process.cwd(), appPagePath)} must wire mdAlternates('${slug}') into its metadata export, either as "alternates: mdAlternates('${slug}')" or spread into a wider alternates object as "...mdAlternates('${slug}')"`
).toBe(true)
} else {
expect(
existsSync(path.join(process.cwd(), 'pages', `${slug}.tsx`)),
`no page found for markdown slug "${slug}" — App Router pages need mdAlternates, Pages Router pages are covered by _app.tsx`
).toBe(true)
}
}
})
it('pages/_app.tsx advertises the .md sibling for Pages Router pages', async () => {
const source = await fs.readFile(path.join(process.cwd(), 'pages', '_app.tsx'), 'utf-8')
expect(
source.includes('MD_PAGES.has('),
'pages/_app.tsx must gate the markdown alternate on MD_PAGES membership'
).toBe(true)
expect(
source.includes('rel="alternate" type="text/markdown"'),
'pages/_app.tsx must render the text/markdown alternate link for markdown-served slugs'
).toBe(true)
})
it('changelog entry page gates its .md alternate on CHANGELOG_PAGES membership', async () => {
const source = await fs.readFile(
path.join(process.cwd(), 'pages', 'changelog', '[slug].tsx'),
'utf-8'
)
expect(
source.includes('CHANGELOG_PAGES.has(`changelog/${entry.slug}`)'),
'pages/changelog/[slug].tsx must compute the markdown alternate flag from CHANGELOG_PAGES membership using the changelog/-prefixed key the generator emits'
).toBe(true)
expect(
source.includes('hasMarkdownVariant &&'),
'pages/changelog/[slug].tsx must render the markdown alternate link only when hasMarkdownVariant is true'
).toBe(true)
expect(
source.includes('rel="alternate" type="text/markdown"'),
'pages/changelog/[slug].tsx must advertise the text/markdown alternate for published slugs'
).toBe(true)
})
it.for(MDX_SECTIONS)('%s pages advertise their .md sibling', async (urlPrefix) => {
const appPagePath = path.join(process.cwd(), 'app', urlPrefix, '[slug]', 'page.tsx')
if (!existsSync(appPagePath)) {
expect(
existsSync(path.join(process.cwd(), 'pages', urlPrefix)),
`section "${urlPrefix}" has neither app/${urlPrefix}/[slug]/page.tsx nor pages/${urlPrefix}/`
).toBe(true)
return
}
const source = await fs.readFile(appPagePath, 'utf-8')
const wiring = 'alternates: mdAlternates(`' + urlPrefix + '/${slug}`)'
expect(
source.includes(wiring),
`app/${urlPrefix}/[slug]/page.tsx must contain "${wiring}" in its generateMetadata`
).toBe(true)
})
})