Skip to content

Commit 70f41de

Browse files
committed
fix: restore RSS and metadata parity
1 parent 8bf598b commit 70f41de

8 files changed

Lines changed: 102 additions & 30 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"predev": "node scripts/buildRscWorker.mjs",
1010
"dev": "next dev",
1111
"prebuild:rsc": "node scripts/buildRscWorker.mjs",
12-
"build": "node scripts/buildRscWorker.mjs && next build && node --experimental-modules ./scripts/downloadFonts.mjs && node ./scripts/generateOgImages.mjs && node ./scripts/validateMetadata.mjs",
12+
"build": "node scripts/buildRscWorker.mjs && node scripts/generateRss.js && next build && node --experimental-modules ./scripts/downloadFonts.mjs && node ./scripts/generateOgImages.mjs && node ./scripts/validateMetadata.mjs",
1313
"lint": "eslint \"{src,plugins}/**/*.{js,jsx,ts,tsx}\" && eslint \"src/content/**/*.md\"",
1414
"lint:fix": "eslint --fix \"{src,plugins}/**/*.{js,jsx,ts,tsx}\" && eslint --fix \"src/content/**/*.md\"",
1515
"format:source": "prettier --config .prettierrc --write \"{plugins,src}/**/*.{js,ts,jsx,tsx,css}\"",

scripts/validateMetadata.mjs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ const pages = walk(appDir).filter(
3636
);
3737
const errors = [];
3838

39+
if (!fs.existsSync(path.join(root, 'public', 'rss.xml'))) {
40+
errors.push('public/rss.xml: missing generated RSS feed');
41+
}
42+
3943
for (const file of pages) {
4044
const head = getHead(fs.readFileSync(file, 'utf8'));
4145
const relative = path.relative(appDir, file);
@@ -49,7 +53,6 @@ for (const file of pages) {
4953
if (!head.includes('<meta name="robots" content="noindex"')) {
5054
errors.push(`${relative}: missing noindex metadata`);
5155
}
52-
continue;
5356
}
5457

5558
const required = [
@@ -68,6 +71,24 @@ for (const file of pages) {
6871
}
6972
}
7073

74+
if (
75+
relative === 'index.html' &&
76+
!head.includes('<link rel="canonical" href="https://react.dev/"')
77+
) {
78+
errors.push(
79+
`${relative}: homepage canonical URL is missing trailing slash`
80+
);
81+
}
82+
83+
if (
84+
file.endsWith('_not-found.html') &&
85+
!head.includes(
86+
'<meta property="og:image" content="https://react.dev/images/og-unknown.png"'
87+
)
88+
) {
89+
errors.push(`${relative}: incorrect not-found OG image`);
90+
}
91+
7192
const ogImageTag = head.match(/<meta property="og:image"[^>]*>/)?.[0];
7293
const ogImage = ogImageTag && getAttribute(ogImageTag, 'content');
7394
if (ogImage) {

src/app/community/not-found.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import sidebarCommunity from '../../sidebarCommunity.json';
99
import {NotFoundContent} from 'components/Layout/NotFoundContent';
1010
import type {RouteItem} from 'components/Layout/getRouteMeta';
11+
import type {Metadata} from 'next';
12+
import {buildNotFoundMetadata} from 'lib/buildPageMetadata';
13+
14+
export const metadata: Metadata = buildNotFoundMetadata();
1115

1216
export default function NotFound() {
1317
return (

src/app/learn/not-found.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import sidebarLearn from '../../sidebarLearn.json';
99
import {NotFoundContent} from 'components/Layout/NotFoundContent';
1010
import type {RouteItem} from 'components/Layout/getRouteMeta';
11+
import type {Metadata} from 'next';
12+
import {buildNotFoundMetadata} from 'lib/buildPageMetadata';
13+
14+
export const metadata: Metadata = buildNotFoundMetadata();
1115

1216
export default function NotFound() {
1317
return (

src/app/not-found.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ import Link from 'components/MDX/Link';
1111
import sidebarLearn from '../sidebarLearn.json';
1212
import type {RouteItem} from 'components/Layout/getRouteMeta';
1313
import type {Metadata} from 'next';
14+
import {buildNotFoundMetadata} from 'lib/buildPageMetadata';
1415

15-
export const metadata: Metadata = {
16-
title: 'Not Found – React',
17-
};
16+
export const metadata: Metadata = buildNotFoundMetadata();
1817

1918
export default function NotFound() {
2019
return (

src/app/page.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {notFound} from 'next/navigation';
1010
import sidebarHome from '../sidebarHome.json';
1111
import type {RouteItem} from 'components/Layout/getRouteMeta';
1212
import {readMarkdownPage} from 'lib/readMarkdownPage';
13-
import {buildPageMetadata} from 'lib/buildPageMetadata';
13+
import {buildPageMetadata, getPageUrls} from 'lib/buildPageMetadata';
1414
import {DocsPage} from './DocsPage';
1515
import {HomeContent} from 'components/Layout/HomeContent';
1616

@@ -20,16 +20,32 @@ export async function generateMetadata(): Promise<Metadata> {
2020
return buildPageMetadata({data, pathname: '/', section: 'home'});
2121
}
2222

23+
function HomePageUrlMetadata() {
24+
const {canonicalUrl, languages} = getPageUrls('/');
25+
return (
26+
<>
27+
<link rel="canonical" href={canonicalUrl} />
28+
{Object.entries(languages).map(([language, url]) => (
29+
<link key={language} rel="alternate" hrefLang={language} href={url} />
30+
))}
31+
<meta property="og:url" content={canonicalUrl} />
32+
</>
33+
);
34+
}
35+
2336
export default async function HomePage() {
2437
const data = await readMarkdownPage([]);
2538
if (!data) notFound();
2639
return (
27-
<DocsPage
28-
data={data}
29-
pathname="/"
30-
section="home"
31-
routeTree={sidebarHome as RouteItem}>
32-
<HomeContent />
33-
</DocsPage>
40+
<>
41+
<HomePageUrlMetadata />
42+
<DocsPage
43+
data={data}
44+
pathname="/"
45+
section="home"
46+
routeTree={sidebarHome as RouteItem}>
47+
<HomeContent />
48+
</DocsPage>
49+
</>
3450
);
3551
}

src/app/reference/not-found.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import sidebarReference from '../../sidebarReference.json';
99
import {NotFoundContent} from 'components/Layout/NotFoundContent';
1010
import type {RouteItem} from 'components/Layout/getRouteMeta';
11+
import type {Metadata} from 'next';
12+
import {buildNotFoundMetadata} from 'lib/buildPageMetadata';
13+
14+
export const metadata: Metadata = buildNotFoundMetadata();
1115

1216
export default function NotFound() {
1317
return (

src/lib/buildPageMetadata.ts

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,30 @@ function getDomain(languageCode: string): string {
1717
return subdomain + 'react.dev';
1818
}
1919

20+
export function getPageUrls(pathname: string): {
21+
canonicalUrl: string;
22+
languages: Record<string, string>;
23+
siteDomain: string;
24+
} {
25+
const siteDomain = getDomain(siteConfig.languageCode);
26+
const canonicalUrl = `https://${siteDomain}${pathname}`;
27+
const languages: Record<string, string> = {
28+
'x-default': canonicalUrl.replace(siteDomain, getDomain('en')),
29+
};
30+
for (const code of finishedTranslations) {
31+
languages[code] = canonicalUrl.replace(siteDomain, getDomain(code));
32+
}
33+
return {canonicalUrl, languages, siteDomain};
34+
}
35+
2036
export function buildPageMetadata({
2137
data,
2238
pathname,
2339
section,
2440
routeTree,
2541
title: titleOverride,
2642
}: {
27-
data: PageData;
43+
data: Pick<PageData, 'meta'>;
2844
pathname: string;
2945
section: PageSection;
3046
title?: string;
@@ -48,27 +64,23 @@ export function buildPageMetadata({
4864
? 'React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript. React is designed to let you seamlessly combine components written by independent people, teams, and organizations.'
4965
: 'The library for web and native user interfaces';
5066

51-
const siteDomain = getDomain(siteConfig.languageCode);
52-
const canonicalUrl = `https://${siteDomain}${pathname}`;
67+
const {canonicalUrl, languages, siteDomain} = getPageUrls(pathname);
5368
// OG images are generated per page at build time by
5469
// scripts/generateOgImages.mjs. Pages without a generated card
55-
// (home, errors) fall back to the static section image.
70+
// (home, errors, 404, 500) fall back to the static section image.
5671
const ogImage =
57-
isHomePage || !title || pathname.startsWith('/errors')
72+
isHomePage ||
73+
!title ||
74+
pathname.startsWith('/errors') ||
75+
pathname === '/404' ||
76+
pathname === '/500'
5877
? `https://${siteDomain}/images/og-${
5978
section === 'unknown' ? 'unknown' : section
6079
}.png`
6180
: `https://${siteDomain}/images/og/${pathname
6281
.slice(1)
6382
.replace(/\//g, '-')}.png`;
6483

65-
const languages: Record<string, string> = {
66-
'x-default': canonicalUrl.replace(siteDomain, getDomain('en')),
67-
};
68-
for (const code of finishedTranslations) {
69-
languages[code] = canonicalUrl.replace(siteDomain, getDomain(code));
70-
}
71-
7284
// Match the Pages Router behavior: emit `algolia-search-order` on Learn
7385
// pages and Blog post pages (not the Blog index) so Algolia can preserve
7486
// the docs sidebar ordering in search results.
@@ -86,13 +98,17 @@ export function buildPageMetadata({
8698
return {
8799
title: pageTitle,
88100
description: isHomePage ? description : undefined,
89-
alternates: {
90-
canonical: canonicalUrl,
91-
languages,
92-
},
101+
// Next serializes the root URL as its bare origin. The home page renders
102+
// these URL tags directly so its canonical URL keeps the trailing slash.
103+
alternates: isHomePage
104+
? undefined
105+
: {
106+
canonical: canonicalUrl,
107+
languages,
108+
},
93109
openGraph: {
94110
type: 'website',
95-
url: canonicalUrl,
111+
url: isHomePage ? undefined : canonicalUrl,
96112
title: pageTitle,
97113
description,
98114
images: [{url: ogImage}],
@@ -108,3 +124,11 @@ export function buildPageMetadata({
108124
other: Object.keys(other).length > 0 ? other : undefined,
109125
};
110126
}
127+
128+
export function buildNotFoundMetadata(): Metadata {
129+
return buildPageMetadata({
130+
data: {meta: {title: 'Not Found'}},
131+
pathname: '/404',
132+
section: 'unknown',
133+
});
134+
}

0 commit comments

Comments
 (0)