Files
supabase/apps/www/components/MarkdownActions.tsx
Pamela Chia 21a27eeb4f feat(www): canonicalize homepage markdown at /index.md (#49384)
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 -->
2026-08-24 15:19:56 +08:00

97 lines
2.6 KiB
TypeScript

'use client'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { askAiUrls, useCopyMarkdownFromUrl } from 'common'
import type { MarkdownAffordancePageType } from 'common/telemetry-constants'
import { Chatgpt, Claude } from 'icons'
import { Check, Copy } from 'lucide-react'
import { cn } from 'ui'
import { SITE_ORIGIN } from '@/lib/constants'
type Props = {
pagePath: string
pageType: MarkdownAffordancePageType
orientation?: 'vertical' | 'horizontal'
className?: string
}
const itemClass =
'flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors'
export function MarkdownActions({
pagePath,
pageType,
orientation = 'vertical',
className,
}: Props) {
const { copied, copyMarkdown } = useCopyMarkdownFromUrl()
const sendTelemetryEvent = useSendTelemetryEvent()
const mdPath = pagePath === '/' ? '/index.md' : `${pagePath}.md`
const urls = askAiUrls(`${SITE_ORIGIN}${pagePath === '/' ? '' : pagePath}`)
async function handleCopy() {
const ok = await copyMarkdown(mdPath)
if (ok) {
sendTelemetryEvent({ action: 'copy_as_markdown_clicked', properties: { pageType } })
}
}
return (
<div
className={cn(
'flex',
orientation === 'vertical' ? 'flex-col gap-2' : 'flex-row flex-wrap items-center gap-4',
className
)}
>
<button
tabIndex={0}
type="button"
onClick={handleCopy}
className={cn(itemClass, 'cursor-pointer text-left')}
>
{copied ? (
<Check size={14} strokeWidth={1.5} className="text-brand" aria-hidden />
) : (
<Copy size={14} strokeWidth={1.5} aria-hidden />
)}
{copied ? 'Copied!' : 'Copy as Markdown'}
</button>
<span className="sr-only" role="status">
{copied ? 'Copied to clipboard' : ''}
</span>
<a
href={urls.chatgpt}
target="_blank"
rel="noreferrer noopener"
onClick={() =>
sendTelemetryEvent({
action: 'ask_ai_clicked',
properties: { agent: 'chatgpt', pageType },
})
}
className={itemClass}
>
<Chatgpt size={14} aria-hidden />
Ask ChatGPT
</a>
<a
href={urls.claude}
target="_blank"
rel="noreferrer noopener"
onClick={() =>
sendTelemetryEvent({
action: 'ask_ai_clicked',
properties: { agent: 'claude', pageType },
})
}
className={itemClass}
>
<Claude size={14} aria-hidden />
Ask Claude
</a>
</div>
)
}