From 8aade779669424cedf471985d9dd70c8d4130789 Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Mon, 31 Aug 2026 16:25:06 +0800 Subject: [PATCH] fix(www): gate changelog md alternate on slug set (#49754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` 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 ## 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. --- apps/www/md-alternates.test.ts | 19 +++++++++++++++++++ apps/www/pages/changelog/[slug].tsx | 20 ++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/www/md-alternates.test.ts b/apps/www/md-alternates.test.ts index d610d636063..27791115c60 100644 --- a/apps/www/md-alternates.test.ts +++ b/apps/www/md-alternates.test.ts @@ -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)) { diff --git a/apps/www/pages/changelog/[slug].tsx b/apps/www/pages/changelog/[slug].tsx index c27fd4281f2..692a8ab1ade 100644 --- a/apps/www/pages/changelog/[slug].tsx +++ b/apps/www/pages/changelog/[slug].tsx @@ -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 ( <> - - - + {hasMarkdownVariant && ( + + + + )} = async ({ params }) => { slug: entry.slug, frontmatter: entry.frontmatter, source, + hasMarkdownVariant: CHANGELOG_PAGES.has(`changelog/${entry.slug}`), }, revalidate: 900, }