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 -->
This commit is contained in:
Pamela Chia authored and GitHub committed 2026-08-31 16:25:06 +08:00
1 parent 9d5c19e580
commit 8aade77966
2 files changed
+35 -4

No files matched your search

+19
View File
@@ -91,6 +91,25 @@ describe('markdown alternate drift', () => {
).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)) {
+16 -4
View File
@@ -5,6 +5,7 @@ import { NextSeo } from 'next-seo'
import Head from 'next/head'
import Link from 'next/link'
import { CHANGELOG_PAGES } from '@/app/api-v2/md/content.generated'
import { ChangelogDetailSidebar } from '@/components/Changelog/ChangelogDetailSidebar'
import { ChangelogInlineMarkdown } from '@/components/Changelog/ChangelogInlineMarkdown'
import CTABanner from '@/components/CTABanner'
@@ -20,15 +21,25 @@ type PageProps = {
slug: string
frontmatter: ChangelogEntryFrontmatter
source: MDXRemoteSerializeResult
hasMarkdownVariant: boolean
}
const ChangelogDetailPage = ({ title, created_at, slug, frontmatter, source }: PageProps) => {
const ChangelogDetailPage = ({
title,
created_at,
slug,
frontmatter,
source,
hasMarkdownVariant,
}: PageProps) => {
const plainTitle = stripTitleMarkdown(title)
return (
<>
<Head>
<link rel="alternate" type="text/markdown" href={`/changelog/${slug}.md`} />
</Head>
{hasMarkdownVariant && (
<Head>
<link rel="alternate" type="text/markdown" href={`/changelog/${slug}.md`} />
</Head>
)}
<NextSeo
title={`${plainTitle} · Changelog`}
description={plainTitle}
@@ -106,6 +117,7 @@ export const getStaticProps: GetStaticProps<PageProps> = async ({ params }) => {
slug: entry.slug,
frontmatter: entry.frontmatter,
source,
hasMarkdownVariant: CHANGELOG_PAGES.has(`changelog/${entry.slug}`),
},
revalidate: 900,
}