Skip to content

Commit 6d38b60

Browse files
authored
Merge pull request #16601 from ethereum/noindex-untranslated-pages
Noindex untranslated pages
2 parents c53bb93 + cb08593 commit 6d38b60

File tree

8 files changed

+167
-77
lines changed

8 files changed

+167
-77
lines changed

app/[locale]/community/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export async function generateMetadata({
5151
const { locale } = params
5252

5353
const t = await getTranslations({ locale, namespace: "page-community" })
54-
5554
return await getMetadata({
5655
locale,
5756
slug: ["community"],

src/i18n/request.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { getRequestConfig } from "next-intl/server"
33

44
import { Lang } from "@/lib/types"
55

6-
import { loadMessages } from "./loadMessages"
76
import { routing } from "./routing"
87

8+
import { loadMessages } from "@/lib/i18n/loadMessages"
9+
910
export default getRequestConfig(async ({ requestLocale }) => {
1011
// This typically corresponds to the `[locale]` segment
1112
let locale = await requestLocale

src/i18n/loadMessages.ts renamed to src/lib/i18n/loadMessages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export async function loadMessages(locale: string) {
2323
const namespaces = getNamespaces(localePath)
2424

2525
for (const ns of namespaces) {
26-
messages[ns] = (await import(`../intl/${locale}/${ns}.json`)).default
26+
messages[ns] = (await import(`../../intl/${locale}/${ns}.json`)).default
2727
}
2828
}
2929

src/lib/i18n/pageTranslation.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { getPrimaryNamespaceForPath } from "../utils/translations"
2+
3+
import { areNamespacesTranslated } from "./translationStatus"
4+
5+
/**
6+
* Determine if a page should be considered translated for a given locale.
7+
*
8+
* This checks only the primary namespace inferred from the provided path. When
9+
* no primary namespace exists for the path, the page is assumed translated
10+
* because it depends solely on globally available shared namespaces (like
11+
* "common") rather than page-specific strings.
12+
*
13+
* @param locale - Locale code, e.g., "en", "es"
14+
* @param slug - Page path or slug, e.g., "/wallets/"
15+
* @returns Promise resolving to whether the page is translated
16+
* @example
17+
* await isPageTranslated("es", "/wallets/") // => true | false
18+
*/
19+
export async function isPageTranslated(
20+
locale: string,
21+
slug: string
22+
): Promise<boolean> {
23+
const primaryNamespace = getPrimaryNamespaceForPath(slug)
24+
25+
if (!primaryNamespace) {
26+
return true
27+
}
28+
29+
return areNamespacesTranslated(locale, [primaryNamespace])
30+
}

src/lib/i18n/translationStatus.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { DEFAULT_LOCALE } from "@/lib/constants"
2+
3+
import { loadMessages } from "@/lib/i18n/loadMessages"
4+
5+
/**
6+
* Determine whether all required i18n namespaces exist for a given locale.
7+
* Default locale is always considered translated.
8+
*/
9+
export async function areNamespacesTranslated(
10+
locale: string,
11+
namespaces: string[]
12+
): Promise<boolean> {
13+
if (locale === DEFAULT_LOCALE) return true
14+
15+
const localeMessages = await loadMessages(locale)
16+
return namespaces.every((ns) =>
17+
Object.prototype.hasOwnProperty.call(localeMessages, ns)
18+
)
19+
}

src/lib/md/metadata.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const getMdMetadata = async ({
1212
}) => {
1313
const slug = slugArray.join("/")
1414

15-
const { markdown } = await importMd(locale, slug)
15+
const { markdown, isTranslated } = await importMd(locale, slug)
1616
const { frontmatter } = await compile({
1717
markdown,
1818
slugArray: slug.split("/"),
@@ -28,12 +28,14 @@ export const getMdMetadata = async ({
2828
const image = frontmatter.image
2929
const author = frontmatter.author
3030

31-
return await getMetadata({
31+
const metadata = await getMetadata({
3232
locale,
3333
slug: slugArray,
3434
title: pageTitle,
3535
description,
3636
image,
3737
author,
38+
noIndex: !isTranslated,
3839
})
40+
return metadata
3941
}

src/lib/utils/metadata.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { getTranslations } from "next-intl/server"
33

44
import { DEFAULT_OG_IMAGE, SITE_URL } from "@/lib/constants"
55

6+
import { isPageTranslated } from "../i18n/pageTranslation"
7+
68
import { isLocaleValidISO639_1 } from "./translations"
79
import { getFullUrl } from "./url"
810

@@ -42,6 +44,7 @@ export const getMetadata = async ({
4244
twitterDescription,
4345
image,
4446
author,
47+
noIndex = false,
4548
}: {
4649
locale: string
4750
slug: string[]
@@ -50,6 +53,7 @@ export const getMetadata = async ({
5053
twitterDescription?: string
5154
image?: string
5255
author?: string
56+
noIndex?: boolean
5357
}): Promise<Metadata> => {
5458
const slugString = slug.join("/")
5559
const t = await getTranslations({ locale, namespace: "common" })
@@ -66,7 +70,7 @@ export const getMetadata = async ({
6670
/* Set fallback ogImage based on path */
6771
const ogImage = image || getOgImage(slug)
6872

69-
return {
73+
const base: Metadata = {
7074
title,
7175
description,
7276
metadataBase: new URL(SITE_URL),
@@ -110,4 +114,13 @@ export const getMetadata = async ({
110114
"docsearch:description": description,
111115
},
112116
}
117+
118+
if (noIndex) {
119+
return { ...base, robots: { index: false } }
120+
}
121+
122+
const isTranslated = await isPageTranslated(locale, slugString)
123+
124+
// If the page is not translated, do not index the page
125+
return isTranslated ? base : { ...base, robots: { index: false } }
113126
}

src/lib/utils/translations.ts

Lines changed: 97 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,111 @@ export const getRequiredNamespacesForPage = (
7272
const getRequiredNamespacesForPath = (relativePath: string) => {
7373
const path = url.addSlashes(relativePath)
7474

75-
let primaryNamespace: string | undefined // the primary namespace for the page
75+
const primaryNamespace = getPrimaryNamespaceForPath(path) // the primary namespace for the page
7676
let requiredNamespaces: string[] = [] // any additional namespaces required for the page
7777

78+
if (path === "/") {
79+
requiredNamespaces = [...requiredNamespaces, "page-10-year-anniversary"]
80+
}
81+
82+
if (path.startsWith("/energy-consumption/")) {
83+
requiredNamespaces = [...requiredNamespaces, "page-about"]
84+
}
85+
86+
if (path.startsWith("/glossary/")) {
87+
requiredNamespaces = [...requiredNamespaces, "glossary"]
88+
}
89+
90+
if (path.startsWith("/developers/docs/scaling/")) {
91+
requiredNamespaces = [...requiredNamespaces, "page-layer-2"]
92+
}
93+
94+
if (path.startsWith("/roadmap/vision/")) {
95+
requiredNamespaces = [
96+
...requiredNamespaces,
97+
"page-upgrades-index",
98+
"page-roadmap-vision",
99+
]
100+
}
101+
102+
if (path.startsWith("/gas/")) {
103+
requiredNamespaces = [...requiredNamespaces, "page-gas", "page-community"]
104+
}
105+
106+
if (path.endsWith("/wallets/find-wallet/")) {
107+
requiredNamespaces = [...requiredNamespaces, "page-wallets", "table"]
108+
}
109+
110+
if (path.startsWith("/layer-2/networks/")) {
111+
requiredNamespaces = [...requiredNamespaces, "table"]
112+
}
113+
114+
if (path.startsWith("/start/")) {
115+
requiredNamespaces = [...requiredNamespaces]
116+
}
117+
118+
if (path.startsWith("/10years/")) {
119+
requiredNamespaces = [...requiredNamespaces, "page-10-year-anniversary"]
120+
}
121+
122+
// Glossary tooltips
123+
if (
124+
path.startsWith("/apps/") ||
125+
path.startsWith("/layer-2/") ||
126+
path.startsWith("/layer-2/learn/") ||
127+
path.startsWith("/get-eth/") ||
128+
path.startsWith("/stablecoins/") ||
129+
path.startsWith("/staking/") ||
130+
path.startsWith("/run-a-node/") ||
131+
path.startsWith("/what-is-ethereum/") ||
132+
path.startsWith("/eth/") ||
133+
path.startsWith("/wallets/") ||
134+
path.startsWith("/gas/")
135+
) {
136+
requiredNamespaces = [...requiredNamespaces, "glossary-tooltip"]
137+
}
138+
139+
// Quizzes
140+
// Note: Add any URL paths that have quizzes here
141+
if (
142+
path.startsWith("/defi/") ||
143+
path.startsWith("/eth/") ||
144+
path.startsWith("/gas/") ||
145+
path.startsWith("/layer-2/") ||
146+
path.startsWith("/layer-2/learn/") ||
147+
path.startsWith("/nft/") ||
148+
path.startsWith("/quizzes/") ||
149+
path.startsWith("/roadmap/merge/") ||
150+
path.startsWith("/roadmap/scaling/") ||
151+
path.startsWith("/run-a-node/") ||
152+
path.startsWith("/security/") ||
153+
path.startsWith("/smart-contracts/") ||
154+
path.startsWith("/stablecoins/") ||
155+
path.startsWith("/staking/solo/") ||
156+
path.startsWith("/wallets/") ||
157+
path.startsWith("/web3/") ||
158+
path.startsWith("/what-is-ethereum/")
159+
) {
160+
requiredNamespaces = [...requiredNamespaces, "learn-quizzes"]
161+
}
162+
163+
// Ensures that the primary namespace is always the first item in the array
164+
return primaryNamespace
165+
? [primaryNamespace, ...requiredNamespaces]
166+
: [...requiredNamespaces]
167+
}
168+
169+
export const getPrimaryNamespaceForPath = (relativePath: string) => {
170+
const path = url.addSlashes(relativePath)
171+
172+
let primaryNamespace: string | undefined
173+
78174
if (path === "/assets/") {
79175
primaryNamespace = "page-assets"
80176
}
81177

82178
if (path === "/") {
83179
primaryNamespace = "page-index"
84-
requiredNamespaces = [...requiredNamespaces, "page-10-year-anniversary"]
85180
}
86181

87182
if (path === "/collectibles/") {
@@ -106,17 +201,12 @@ const getRequiredNamespacesForPath = (relativePath: string) => {
106201

107202
if (path.startsWith("/energy-consumption/")) {
108203
primaryNamespace = "page-energy-consumption"
109-
requiredNamespaces = [...requiredNamespaces, "page-about"]
110204
}
111205

112206
if (path.startsWith("/eth/")) {
113207
primaryNamespace = "page-eth"
114208
}
115209

116-
if (path.startsWith("/glossary/")) {
117-
requiredNamespaces = [...requiredNamespaces, "glossary"]
118-
}
119-
120210
if (path.startsWith("/ethereum-forks/")) {
121211
primaryNamespace = "page-history"
122212
}
@@ -157,25 +247,12 @@ const getRequiredNamespacesForPath = (relativePath: string) => {
157247
primaryNamespace = "page-developers-tutorials"
158248
}
159249

160-
if (path.startsWith("/developers/docs/scaling/")) {
161-
requiredNamespaces = [...requiredNamespaces, "page-layer-2"]
162-
}
163-
164250
if (path === "/get-eth/") {
165251
primaryNamespace = "page-get-eth"
166252
}
167253

168-
if (path.startsWith("/roadmap/vision/")) {
169-
requiredNamespaces = [
170-
...requiredNamespaces,
171-
"page-upgrades-index",
172-
"page-roadmap-vision",
173-
]
174-
}
175-
176254
if (path.startsWith("/gas/")) {
177255
primaryNamespace = "page-gas"
178-
requiredNamespaces = [...requiredNamespaces, "page-gas", "page-community"]
179256
}
180257

181258
if (path.startsWith("/what-is-ethereum/")) {
@@ -196,7 +273,6 @@ const getRequiredNamespacesForPath = (relativePath: string) => {
196273

197274
if (path.endsWith("/wallets/find-wallet/")) {
198275
primaryNamespace = "page-wallets-find-wallet"
199-
requiredNamespaces = [...requiredNamespaces, "page-wallets", "table"]
200276
}
201277

202278
// TODO: Remove this when the page is translated
@@ -210,7 +286,6 @@ const getRequiredNamespacesForPath = (relativePath: string) => {
210286

211287
if (path.startsWith("/layer-2/networks/")) {
212288
primaryNamespace = "page-layer-2-networks"
213-
requiredNamespaces = [...requiredNamespaces, "table"]
214289
}
215290

216291
if (path.startsWith("/roadmap/")) {
@@ -219,62 +294,13 @@ const getRequiredNamespacesForPath = (relativePath: string) => {
219294

220295
if (path.startsWith("/start/")) {
221296
primaryNamespace = "page-start"
222-
requiredNamespaces = [...requiredNamespaces]
223297
}
224298

225299
if (path.startsWith("/contributing/translation-program/translatathon/")) {
226300
primaryNamespace = "page-translatathon"
227301
}
228302

229-
if (path.startsWith("/10years/")) {
230-
requiredNamespaces = [...requiredNamespaces, "page-10-year-anniversary"]
231-
}
232-
233-
// Glossary tooltips
234-
if (
235-
path.startsWith("/apps/") ||
236-
path.startsWith("/layer-2/") ||
237-
path.startsWith("/layer-2/learn/") ||
238-
path.startsWith("/get-eth/") ||
239-
path.startsWith("/stablecoins/") ||
240-
path.startsWith("/staking/") ||
241-
path.startsWith("/run-a-node/") ||
242-
path.startsWith("/what-is-ethereum/") ||
243-
path.startsWith("/eth/") ||
244-
path.startsWith("/wallets/") ||
245-
path.startsWith("/gas/")
246-
) {
247-
requiredNamespaces = [...requiredNamespaces, "glossary-tooltip"]
248-
}
249-
250-
// Quizzes
251-
// Note: Add any URL paths that have quizzes here
252-
if (
253-
path.startsWith("/defi/") ||
254-
path.startsWith("/eth/") ||
255-
path.startsWith("/gas/") ||
256-
path.startsWith("/layer-2/") ||
257-
path.startsWith("/layer-2/learn/") ||
258-
path.startsWith("/nft/") ||
259-
path.startsWith("/quizzes/") ||
260-
path.startsWith("/roadmap/merge/") ||
261-
path.startsWith("/roadmap/scaling/") ||
262-
path.startsWith("/run-a-node/") ||
263-
path.startsWith("/security/") ||
264-
path.startsWith("/smart-contracts/") ||
265-
path.startsWith("/stablecoins/") ||
266-
path.startsWith("/staking/solo/") ||
267-
path.startsWith("/wallets/") ||
268-
path.startsWith("/web3/") ||
269-
path.startsWith("/what-is-ethereum/")
270-
) {
271-
requiredNamespaces = [...requiredNamespaces, "learn-quizzes"]
272-
}
273-
274-
// Ensures that the primary namespace is always the first item in the array
275303
return primaryNamespace
276-
? [primaryNamespace, ...requiredNamespaces]
277-
: [...requiredNamespaces]
278304
}
279305

280306
const getRequiredNamespacesForLayout = (layout?: string) => {

0 commit comments

Comments
 (0)