diff --git a/.all-contributorsrc b/.all-contributorsrc index 8f3217bd281..99ba14dd7fd 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1501,7 +1501,8 @@ "avatar_url": "https://avatars1.githubusercontent.com/u/44020788?v=4", "profile": "https://twitter.com/AnettRolikova", "contributions": [ - "content" + "content", + "tool" ] }, { @@ -14149,6 +14150,51 @@ "contributions": [ "maintenance" ] + }, + { + "login": "Ale-Bv-Dev", + "name": "Ale-Bv-Dev", + "avatar_url": "https://avatars.githubusercontent.com/u/214505223?v=4", + "profile": "https://github.com/Ale-Bv-Dev", + "contributions": [ + "bug" + ] + }, + { + "login": "rayjun", + "name": "rayoo", + "avatar_url": "https://avatars.githubusercontent.com/u/7517993?v=4", + "profile": "https://github.com/rayjun", + "contributions": [ + "bug" + ] + }, + { + "login": "kangbaek324", + "name": "BaekHo Kang", + "avatar_url": "https://avatars.githubusercontent.com/u/162931494?v=4", + "profile": "https://github.com/kangbaek324", + "contributions": [ + "translation" + ] + }, + { + "login": "franrob-projects", + "name": "Fran Roberts", + "avatar_url": "https://avatars.githubusercontent.com/u/111994975?v=4", + "profile": "https://franrob-projects.github.io/portfolio/", + "contributions": [ + "content" + ] + }, + { + "login": "jzhishu", + "name": "JH", + "avatar_url": "https://avatars.githubusercontent.com/u/39545185?v=4", + "profile": "https://github.com/jzhishu", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/.claude/commands/review-translations.md b/.claude/commands/review-translations.md index 55d10e8fef6..09346228c78 100644 --- a/.claude/commands/review-translations.md +++ b/.claude/commands/review-translations.md @@ -224,38 +224,39 @@ Read `.claude/translation-review/known-patterns.md` — this contains all issue ### Translation Glossary (AUTHORITATIVE SOURCE) -The EthGlossary API (`https://ethereum.org/api/glossary`) is the **authoritative source** for all Ethereum term translations across the entire pipeline. Community-voted glossary terms are not suggestions — they are the required translations. +**ETHGlossary** is the authoritative source for Ethereum term translations. Deviations are critical issues, not warnings. -**Fetch live from the API first, fall back to cache only if the API is unreachable:** +Resolve the base URL from the pipeline config (env var wins; default lives in `src/scripts/intl-pipeline/config.ts` under `GLOSSARY_API_URL`): ```bash -# Fetch live glossary -GLOSSARY_CACHE="$HOME/.claude/translation-review/fetch-translation-glossary.json" -GLOSSARY_URL="https://ethereum.org/api/glossary" - -# Try live fetch first -if curl -sf "$GLOSSARY_URL" -o "$TMPDIR/glossary-live.json" 2>/dev/null; then - # Update cache with fresh data - cp "$TMPDIR/glossary-live.json" "$GLOSSARY_CACHE" - echo "Glossary fetched live from API and cache updated." -else - echo "WARNING: API unreachable, using cached glossary." -fi +GLOSSARY_API_URL="${GLOSSARY_API_URL:-$(grep -oE 'https://[^"]+/api/v[0-9]+' "$WORKTREE_PATH/src/scripts/intl-pipeline/config.ts" | head -1)}" +GLOSSARY_HOST="${GLOSSARY_API_URL%/api/*}" ``` -Schema: `Array<{ string_term, translation_text, language_code, total_votes }>`. +Fetch `llms.txt` first as the canonical reference for endpoints and languages; if examples below disagree, llms.txt wins: -For each language being reviewed, extract relevant glossary terms: +```bash +curl -sf "$GLOSSARY_HOST/llms.txt" \ + -o "$TMPDIR/ethglossary-llms.txt" \ + && cp "$TMPDIR/ethglossary-llms.txt" "$HOME/.claude/translation-review/ethglossary-llms.txt" ``` -Filter entries where language_code matches the target locale. -Sort by total_votes descending. -Include ALL terms for the language (not just top 50) — these are authoritative. + +**Preferred — per-file filter** (`POST /filter`): returns only the glossary terms that appear in the English source, with translations sorted by occurrence. Avoids pulling hundreds of irrelevant terms into agent context. + +```bash +ENGLISH_SOURCE=$(cat "$WORKTREE_PATH/public/content/{path}.md") +curl -sf -X POST "$GLOSSARY_API_URL/filter" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg text "$ENGLISH_SOURCE" --arg lang "{LANGUAGE_CODE}" '{text: $text, language: $lang}')" +``` + +**Fallback — full language** when filtering per file is impractical or the endpoint is unreachable: + +```bash +curl -sf "$GLOSSARY_API_URL/translations/{LANGUAGE_CODE}" ``` -**The glossary is used in every subsequent phase:** -- **Phase 3 (Review):** Agents treat glossary deviations as CRITICAL, not warnings -- **Phase 5 (Auto-Fix):** Glossary deviations are auto-corrected to the top-voted translation -- **Phase 8 (Knowledge Base):** New deviations discovered are logged for future reviews +Used in Phase 3 (review — deviations are CRITICAL), Phase 5 (auto-fix corrects to ETHGlossary translation), Phase 8 (new deviations logged). ### Per-Language Prior Findings Check if `.claude/translation-review/per-language/{LANGUAGE_CODE}.md` exists. If so, read it and inject relevant prior findings into the agent prompt. @@ -331,22 +332,11 @@ The community has voted on these translations for key Ethereum terms. Use these - Review the entire current content of each file - Compare against English source files from the worktree -## MANDATORY: Fetch Ethereum Glossary FIRST - -**Before reviewing ANY translation, you MUST fetch the official Ethereum glossary for the language(s) being reviewed.** This is non-negotiable. The glossary contains community-approved translations for key terms. - -```bash -# Fetch full glossary (all languages): -curl -s "https://ethereum.org/api/glossary/" - -# Fetch glossary for a specific language (optional lang param, one at a time): -curl -s "https://ethereum.org/api/glossary/?lang=fr" -curl -s "https://ethereum.org/api/glossary/?lang=ja" -``` +## MANDATORY: Use ETHGlossary for the target language -The glossary returns approved translations per language. Use these as the authority for how technical terms SHOULD be translated. Flag any deviations as warnings with "Glossary mismatch" in the issue column. +Use the ETHGlossary terms fetched in Phase 2 as the authority for technical term translations. Report deviations as **critical** issues (not warnings), with the current (wrong) translation and the expected (ETHGlossary) translation so Phase 5 can auto-fix them. -**If you skip the glossary, the entire review is invalid.** +**If you skip ETHGlossary, the entire review is invalid.** ## Review Checklist @@ -733,6 +723,6 @@ ETH, Wei, Gwei, Gas - Use `--model=sonnet` or `--model=haiku` for faster reviews - Build verification is opt-in: `--build-local` for local scoped builds, `--netlify-check` for Netlify deploy preview checks - If an agent exceeds context limits with Opus, fall back to Sonnet with Grep-based file inspection -- **EthGlossary API** (`https://ethereum.org/api/glossary`) is fetched live in Phase 2 and is the authoritative source for term translations across the entire pipeline — review (Phase 3), auto-fix (Phase 5), and knowledge base (Phase 8). The local cache at `~/.claude/translation-review/fetch-translation-glossary.json` is a fallback only. +- **ETHGlossary** is the authoritative source for term translations across review (Phase 3), auto-fix (Phase 5), and knowledge base (Phase 8). See Phase 2 for usage; `llms.txt` is the canonical endpoint reference. - Knowledge base at `.claude/translation-review/` accumulates findings across reviews (committed to repo) - `gh` CLI commands require `dangerouslyDisableSandbox: true` due to TLS certificate verification issues in sandbox mode diff --git a/.storybook/main.ts b/.storybook/main.ts index adb6f8a805d..1bfafd87248 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -13,6 +13,7 @@ const config: StorybookConfig = { "../src/components/**/*.stories.{ts,tsx}", "../src/layouts/stories/*.stories.tsx", "../src/styles/*.stories.tsx", + "../app/**/*.stories.{ts,tsx}", ], addons: [ @@ -68,6 +69,12 @@ const config: StorybookConfig = { use: ["@svgr/webpack"], }) + // .all-contributorsrc is JSON without a .json extension + config.module.rules.push({ + test: /\.all-contributorsrc$/, + type: "json", + }) + return config }, diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index ee8ad2d1f6e..b2b64b42f9e 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -10,9 +10,6 @@ import nextIntl, { baseLocales } from "./next-intl" import { withNextThemes } from "./withNextThemes" import "../src/styles/global.css" -import "../src/styles/docsearch.css" - -import "@docsearch/css" const inter = Inter({ subsets: ["latin"], diff --git a/AGENTS.md b/AGENTS.md index 170aa511c3d..f7d2ab37601 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ This is the official Ethereum.org website - a Next.js application that serves as - **Next.js 14.2+** - React framework with App Router - **React 18** - UI library - **TypeScript 5.5+** - Type safety and development experience -- **Tailwind CSS 3.4+** - Utility-first CSS framework +- **Tailwind CSS 4+** - Utility-first CSS framework (CSS-first config in `src/styles/global.css`) ### Key Dependencies @@ -119,10 +119,10 @@ pnpm events-import # Import community events ### Internationalization -- **25 languages** supported via Crowdin (canonical list: `i18n.config.json`) -- **RTL support** for Arabic, Urdu -- Translation files (JSON format) in `src/intl/[locale]/` -- Content translations managed through Crowdin platform +- **25 languages** supported (canonical list: `i18n.config.json`); **RTL support** for Arabic, Urdu +- JSON UI strings in `src/intl/[locale]/`; translated markdown content in `public/content/translations/[locale]/` +- Non-English markdown is propagated by the **intl-pipeline** (`src/scripts/intl-pipeline/`, entry `main.ts`). **Do not hand-propagate English changes into non-English files** -- let the pipeline run, or trigger `intl-pipeline.yml` with `stamp_only: true` if manifests must catch up urgently (e.g. unblocking a build). Hand-fixing a translation error is fine when the English side hasn't moved, since the manifest mapping stays valid. Spec: `tests/specs/PIPELINE-SPEC.md`. +- Glossary: base URL from `GLOSSARY_API_URL` env var; default in `src/scripts/intl-pipeline/config.ts`. ETHGlossary is authoritative for Ethereum term translations. ### Markdown Content diff --git a/README.md b/README.md index 09205a0967f..af814fd8771 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Ev
Ev

🤔 🐛 🖋 Ivan Martinez
Ivan Martinez

🖋 Sebastian T F
Sebastian T F

💻 - Anett Rolikova
Anett Rolikova

🖋 + Anett Rolikova
Anett Rolikova

🖋 🔧 Pooja Ranjan
Pooja Ranjan

🖋 @@ -2203,6 +2203,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Jadi
Jadi

🚧 + Ale-Bv-Dev
Ale-Bv-Dev

🐛 + rayoo
rayoo

🐛 + BaekHo Kang
BaekHo Kang

🌍 + Fran Roberts
Fran Roberts

🖋 + JH
JH

🐛 diff --git a/app/[locale]/10years/_components/AdoptionSwiper/loading.tsx b/app/[locale]/10years/_components/AdoptionSwiper/loading.tsx index 46bfe2f77eb..bbd2aa0f759 100644 --- a/app/[locale]/10years/_components/AdoptionSwiper/loading.tsx +++ b/app/[locale]/10years/_components/AdoptionSwiper/loading.tsx @@ -2,7 +2,7 @@ import { Skeleton } from "@/components/ui/skeleton" const Loading = () => (
-
+
diff --git a/app/[locale]/10years/_components/InnovationSwiper/index.tsx b/app/[locale]/10years/_components/InnovationSwiper/index.tsx index c1316c5bcfd..c70ccb395e1 100644 --- a/app/[locale]/10years/_components/InnovationSwiper/index.tsx +++ b/app/[locale]/10years/_components/InnovationSwiper/index.tsx @@ -24,7 +24,7 @@ const InnovationSwiper = ({ innovationCards }: InnovationSwiperProps) => ( ({ image, title, date, description1, description2 }, index) => ( (
-
+
diff --git a/app/[locale]/10years/_components/NFTMintCard/index.tsx b/app/[locale]/10years/_components/NFTMintCard/index.tsx index 835bf9d439c..5296b99c774 100644 --- a/app/[locale]/10years/_components/NFTMintCard/index.tsx +++ b/app/[locale]/10years/_components/NFTMintCard/index.tsx @@ -20,7 +20,7 @@ const NFTMintCard = ({ className }: NFTMintCardProps) => { return ( @@ -30,7 +30,7 @@ const NFTMintCard = ({ className }: NFTMintCardProps) => {
@@ -46,12 +46,12 @@ const TenYearHero = async () => { {t("page-10-year-celebrating")}{" "} {WORDS[0]} - + {/* CLIENT SIDE, lazy loaded */} diff --git a/app/[locale]/10years/_components/TorchHistoryCard.tsx b/app/[locale]/10years/_components/TorchHistoryCard.tsx index 50514ee69d6..f5d7d38ba41 100644 --- a/app/[locale]/10years/_components/TorchHistoryCard.tsx +++ b/app/[locale]/10years/_components/TorchHistoryCard.tsx @@ -41,10 +41,9 @@ const TorchHistoryCard: React.FC = ({ return ( diff --git a/app/[locale]/10years/_components/UserStories/index.tsx b/app/[locale]/10years/_components/UserStories/index.tsx index b5e13b22622..3f35d292968 100644 --- a/app/[locale]/10years/_components/UserStories/index.tsx +++ b/app/[locale]/10years/_components/UserStories/index.tsx @@ -52,7 +52,7 @@ const Stories = ({ stories }: StoriesProps) => {
@@ -69,14 +69,14 @@ const Stories = ({ stories }: StoriesProps) => {
-
+

{story.name?.slice(0, 1) || "?"}

{story.name}

-

+

{story.country}

@@ -89,7 +89,7 @@ const Stories = ({ stories }: StoriesProps) => { hideArrow className="text-sm" > - +
)} @@ -122,7 +122,7 @@ const Stories = ({ stories }: StoriesProps) => {
{story.storyOriginal && (
-

+

{t("page-10-year-stories-english-translation")}

)} -

+

{story.date}

@@ -148,14 +148,14 @@ const Stories = ({ stories }: StoriesProps) => {
-
+

{story.name?.slice(0, 1) || "?"}

{story.name}

-

+

{story.country}

@@ -168,7 +168,7 @@ const Stories = ({ stories }: StoriesProps) => { hideArrow className="text-sm" > - +
)} @@ -200,7 +200,7 @@ const Stories = ({ stories }: StoriesProps) => { )}
-

+

{t("page-10-year-stories-original-language")}

-

+

{story.date}

diff --git a/app/[locale]/10years/_components/UserStories/loading.tsx b/app/[locale]/10years/_components/UserStories/loading.tsx index 31b50c6581f..8ee094951c7 100644 --- a/app/[locale]/10years/_components/UserStories/loading.tsx +++ b/app/[locale]/10years/_components/UserStories/loading.tsx @@ -5,7 +5,7 @@ const Loading = () => ( {Array.from({ length: 3 }).map((_, idx) => (
diff --git a/app/[locale]/10years/_components/data.tsx b/app/[locale]/10years/_components/data.tsx index 84785db7fc6..50b748491ac 100644 --- a/app/[locale]/10years/_components/data.tsx +++ b/app/[locale]/10years/_components/data.tsx @@ -1,11 +1,11 @@ // duplicate 1 2 3, 1 2 3 to fix mobile slider bug where styles are not applied const adoptionStyles = [ - "bg-background bg-gradient-to-b from-20% to-60% from-accent-c/10 to-accent-c/5 dark:from-accent-c/20 dark:to-accent-c/10 border-accent-c/10", - "bg-background bg-gradient-to-b from-20% to-60% from-accent-b/10 to-accent-b/5 dark:from-accent-b/20 dark:to-accent-b/10 border-accent-b/10", - "bg-background bg-gradient-to-b from-20% to-60% from-accent-a/10 to-accent-a/5 dark:from-accent-a/20 dark:to-accent-a/10 border-accent-a/10", - "bg-background bg-gradient-to-b from-20% to-60% from-accent-c/10 to-accent-c/5 dark:from-accent-c/20 dark:to-accent-c/10 border-accent-c/10", - "bg-background bg-gradient-to-b from-20% to-60% from-accent-b/10 to-accent-b/5 dark:from-accent-b/20 dark:to-accent-b/10 border-accent-b/10", - "bg-background bg-gradient-to-b from-20% to-60% from-accent-a/10 to-accent-a/5 dark:from-accent-a/20 dark:to-accent-a/10 border-accent-a/10", + "bg-background bg-linear-to-b from-20% to-60% from-accent-c/10 to-accent-c/5 dark:from-accent-c/20 dark:to-accent-c/10 border-accent-c/10", + "bg-background bg-linear-to-b from-20% to-60% from-accent-b/10 to-accent-b/5 dark:from-accent-b/20 dark:to-accent-b/10 border-accent-b/10", + "bg-background bg-linear-to-b from-20% to-60% from-accent-a/10 to-accent-a/5 dark:from-accent-a/20 dark:to-accent-a/10 border-accent-a/10", + "bg-background bg-linear-to-b from-20% to-60% from-accent-c/10 to-accent-c/5 dark:from-accent-c/20 dark:to-accent-c/10 border-accent-c/10", + "bg-background bg-linear-to-b from-20% to-60% from-accent-b/10 to-accent-b/5 dark:from-accent-b/20 dark:to-accent-b/10 border-accent-b/10", + "bg-background bg-linear-to-b from-20% to-60% from-accent-a/10 to-accent-a/5 dark:from-accent-a/20 dark:to-accent-a/10 border-accent-a/10", ] export { adoptionStyles } diff --git a/app/[locale]/10years/page-jsonld.tsx b/app/[locale]/10years/page-jsonld.tsx index 0c5c703e1e2..5c0c81e2d86 100644 --- a/app/[locale]/10years/page-jsonld.tsx +++ b/app/[locale]/10years/page-jsonld.tsx @@ -4,14 +4,10 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function TenYearJsonLD({ locale, contributors, @@ -32,8 +28,7 @@ export default async function TenYearJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -41,13 +36,9 @@ export default async function TenYearJsonLD({ description: t("page-10-year-anniversary-meta-description"), url: url, inLanguage: locale, - author: [ethereumCommunityReference], + author: [REFERENCE.ETHEREUM_COMMUNITY], contributor: contributorList, - isPartOf: { - "@type": "WebSite", - name: "ethereum.org", - url: "https://ethereum.org", - }, + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -65,8 +56,8 @@ export default async function TenYearJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#video` }, }, { @@ -77,8 +68,7 @@ export default async function TenYearJsonLD({ uploadDate: "2024-07-30T00:00:00Z", duration: "PT5M30S", embedUrl: "https://www.youtube.com/embed/gjwr-7PgpTC", - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, }, ], } diff --git a/app/[locale]/10years/page.tsx b/app/[locale]/10years/page.tsx index 8dcf52628a7..ed612029c66 100644 --- a/app/[locale]/10years/page.tsx +++ b/app/[locale]/10years/page.tsx @@ -96,7 +96,7 @@ const Page = async (props: { params: Promise }) => {
-
+

{t("page-10-year-livestream-title")} @@ -117,12 +117,12 @@ const Page = async (props: { params: Promise }) => { defaultValue={Object.keys(tenYearEventRegions)[0]} className="w-full" > - + {Object.entries(tenYearEventRegions).map(([key, data]) => ( {data.label}  ({data.events.length}) @@ -157,8 +157,8 @@ const Page = async (props: { params: Promise }) => { key={country} className={cn("flex flex-col border-b px-4 py-6")} > -

- +

+ }) => { {countryEvents.map((event, index) => (
@@ -206,7 +206,7 @@ const Page = async (props: { params: Promise }) => {
@@ -224,13 +224,13 @@ const Page = async (props: { params: Promise }) => { disablePictureInPicture playsInline /> -
+
{/* Curved text */} @@ -239,7 +239,7 @@ const Page = async (props: { params: Promise }) => { -
+

}) => {

- + {t("page-10-year-innovation-title")} - + {t("page-10-year-innovation-subtitle")}

@@ -292,10 +292,10 @@ const Page = async (props: { params: Promise }) => {

- + {t("page-10-year-adoption-title")} - + {t("page-10-year-adoption-subtitle")}

@@ -348,10 +348,10 @@ const Page = async (props: { params: Promise }) => {

- + {t("page-10-year-stories-title")} - + {t("page-10-year-stories-subtitle")}

diff --git a/app/[locale]/[...slug]/page-jsonld.tsx b/app/[locale]/[...slug]/page-jsonld.tsx index a48a876b67e..32dfe10e803 100644 --- a/app/[locale]/[...slug]/page-jsonld.tsx +++ b/app/[locale]/[...slug]/page-jsonld.tsx @@ -2,14 +2,11 @@ import { FileContributor, Frontmatter } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" +import { resolveAuthorsFromFrontmatter } from "@/lib/jsonld/utils" + export default async function SlugJsonLD({ locale, slug, @@ -29,7 +26,7 @@ export default async function SlugJsonLD({ "@type": "ListItem", position: 1, name: "Home", - item: `https://ethereum.org/${locale}/`, + item: normalizeUrlForJsonLd(locale, "/"), }, ] @@ -54,11 +51,15 @@ export default async function SlugJsonLD({ url: contributor.html_url, })) + const { authorGraphNodes, authorIds } = resolveAuthorsFromFrontmatter( + frontmatter.authors ?? frontmatter.author + ) + const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, + ...authorGraphNodes, { "@type": "WebPage", "@id": url, @@ -66,20 +67,15 @@ export default async function SlugJsonLD({ description: frontmatter.description, url: url, inLanguage: locale, - author: [ethereumCommunityReference], + author: authorIds, contributor: contributorList, - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: breadcrumbItems, }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#article` }, }, { @@ -90,10 +86,9 @@ export default async function SlugJsonLD({ image: frontmatter.image ? `https://ethereum.org${frontmatter.image}` : undefined, - author: [ethereumCommunityReference], + author: authorIds, contributor: contributorList, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, dateModified: frontmatter.published, mainEntityOfPage: url, about: { diff --git a/app/[locale]/apps/[application]/page-jsonld.tsx b/app/[locale]/apps/[application]/page-jsonld.tsx index 9e7a33d39e2..bdb7ec1e395 100644 --- a/app/[locale]/apps/[application]/page-jsonld.tsx +++ b/app/[locale]/apps/[application]/page-jsonld.tsx @@ -2,14 +2,10 @@ import { AppCategory, AppData, FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd, slugify } from "@/lib/utils/url" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + // Map internal app categories to schema.org enumerated applicationCategory values // https://schema.org/applicationCategory const APPLICATION_CATEGORY_MAP: Record = { @@ -43,8 +39,7 @@ export default async function AppsAppJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -53,13 +48,8 @@ export default async function AppsAppJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -83,8 +73,8 @@ export default async function AppsAppJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#applications` }, }, { diff --git a/app/[locale]/apps/[application]/page.tsx b/app/[locale]/apps/[application]/page.tsx index dbdb19a37b5..dea091fb442 100644 --- a/app/[locale]/apps/[application]/page.tsx +++ b/app/[locale]/apps/[application]/page.tsx @@ -43,6 +43,8 @@ import { import { slugify } from "@/lib/utils/url" import { formatStringList } from "@/lib/utils/wallets" +import { DEFAULT_LOCALE } from "@/lib/constants" + import ScreenshotSwiper from "./_components/ScreenshotSwiper" import AppsAppJsonLD from "./page-jsonld" @@ -148,13 +150,13 @@ const Page = async (props: { Ethereum.org - + / ALL APPS - + / @@ -187,13 +189,13 @@ const Page = async (props: { chains={app.networks as ChainName[]} className="mt-2" /> -

+

by {app.parentCompany}

-

+

{formatStringList( formatLanguageNames(app.languages), 5 @@ -273,7 +275,7 @@ const Page = async (props: { )}

{nextApp && ( - +

{t("page-apps-see-next")} @@ -291,7 +293,7 @@ const Page = async (props: { />

- +
)} @@ -300,9 +302,9 @@ const Page = async (props: {
{nextApp && ( - +
-

+

{t("page-apps-see-next")}

@@ -318,14 +320,14 @@ const Page = async (props: { />

- +
)}
-
+

{getLocalizedDescription( appDescriptions, @@ -334,22 +336,22 @@ const Page = async (props: { app.description )}

-
+

{t("page-apps-info-title")}

-

+

{t("page-apps-info-founded")}

{getDisplayYear(app.dateOfLaunch)}

-

+

{t("page-apps-info-creator")}

{app.parentCompany}

-

+

{t("page-apps-info-last-updated")}

{getTimeAgo(app.lastUpdated)}

@@ -368,7 +370,7 @@ const Page = async (props: { {relatedApps.length > 0 && (
-
+

{t("page-apps-more-apps-like-this")}

{relatedApps.map((relatedApp) => ( @@ -422,38 +424,51 @@ export async function generateMetadata(props: { const params = await props.params const { locale, application } = params - // Fetch apps data using the new data-layer function (already cached) - const appsData = await getAppsData() + try { + // Fetch apps data using the new data-layer function (already cached) + const appsData = await getAppsData() - // Handle null case - throw error if required data is missing - if (!appsData) { - throw new Error("Failed to fetch apps data") - } + // Handle null case - throw error if required data is missing + if (!appsData) { + throw new Error("Failed to fetch apps data") + } - const app = Object.values(appsData) - .flat() - .find((app) => slugify(app.name) === application)! + const app = Object.values(appsData) + .flat() + .find((app) => slugify(app.name) === application) - if (!app) { - notFound() - } + if (!app) { + throw new Error(`App not found: ${application}`) + } - const appDescriptions = await getTranslations("page-app-descriptions") + const appDescriptions = await getTranslations("page-app-descriptions") - const title = `Ethereum Apps - ${app.name}` // TODO (i18n): Extract "Ethereum Apps" to namespace - const description = getLocalizedDescription( - appDescriptions, - "app", - app.name, - app.description - ) + const title = `Ethereum Apps - ${app.name}` // TODO (i18n): Extract "Ethereum Apps" to namespace + const description = getLocalizedDescription( + appDescriptions, + "app", + app.name, + app.description + ) - return await getMetadata({ - locale, - slug: ["apps", application], - title, - description, - }) + return await getMetadata({ + locale, + slug: ["apps", application], + title, + description, + }) + } catch (error) { + const t = await getTranslations({ + locale: DEFAULT_LOCALE, + namespace: "common", + }) + + // Return basic metadata for invalid paths + return { + title: t("page-not-found"), + description: t("page-not-found-description"), + } + } } export default Page diff --git a/app/[locale]/apps/_components/AppsHighlight.tsx b/app/[locale]/apps/_components/AppsHighlight.tsx index 40487d68c0a..86d95d366c0 100644 --- a/app/[locale]/apps/_components/AppsHighlight.tsx +++ b/app/[locale]/apps/_components/AppsHighlight.tsx @@ -19,7 +19,7 @@ const AppsHighlight = ({ apps, matomoCategory }: AppsHighlightProps) => { const cards = apps.slice(0, 3).map((app, index) => ( (
-
+
{`${pick.name} { const t = await getTranslations("page-apps") return ( -
+

{t("page-apps-suggest-an-app-title")}

{t("page-apps-suggest-an-app-description")}

{ {Object.keys(appsData).map((category) => (
- + { ) })()}
-

+

{t(appsCategories[category].name)}

diff --git a/app/[locale]/apps/categories/[catetgoryName]/page-jsonld.tsx b/app/[locale]/apps/categories/[catetgoryName]/page-jsonld.tsx index c2585aa8cd6..4c51a07988f 100644 --- a/app/[locale]/apps/categories/[catetgoryName]/page-jsonld.tsx +++ b/app/[locale]/apps/categories/[catetgoryName]/page-jsonld.tsx @@ -4,14 +4,10 @@ import { AppCategoryData, AppData, FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function AppsCategoryJsonLD({ locale, categoryName, @@ -40,8 +36,7 @@ export default async function AppsCategoryJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -50,13 +45,8 @@ export default async function AppsCategoryJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -80,8 +70,8 @@ export default async function AppsCategoryJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#categories` }, }, { diff --git a/app/[locale]/apps/categories/[catetgoryName]/page.tsx b/app/[locale]/apps/categories/[catetgoryName]/page.tsx index b3f72ad03b8..d45c67eae6f 100644 --- a/app/[locale]/apps/categories/[catetgoryName]/page.tsx +++ b/app/[locale]/apps/categories/[catetgoryName]/page.tsx @@ -149,7 +149,7 @@ const Page = async (props: { Ethereum.org - + / @@ -157,7 +157,7 @@ const Page = async (props: { {t("page-apps-all-apps")} - + / @@ -223,35 +223,45 @@ export async function generateMetadata(props: { }) { const params = await props.params const { locale, catetgoryName } = params - const t = await getTranslations("page-apps") - // Normalize slug to lowercase - const normalizedSlug = catetgoryName.toLowerCase() + try { + const t = await getTranslations("page-apps") - // Find category by matching the slug - const categoryEntry = Object.entries(appsCategories).find( - ([, categoryData]) => categoryData.slug === normalizedSlug - ) + // Normalize slug to lowercase + const normalizedSlug = catetgoryName.toLowerCase() - if (!categoryEntry) { - notFound() - } + // Find category by matching the slug + const categoryEntry = Object.entries(appsCategories).find( + ([, categoryData]) => categoryData.slug === normalizedSlug + ) - const [categoryEnum, category] = categoryEntry + if (!categoryEntry) { + throw new Error(`App category not found: ${catetgoryName}`) + } - if (!isValidCategory(categoryEnum)) { - notFound() - } + const [categoryEnum, category] = categoryEntry - const title = t(category.metaTitle) - const description = t(category.metaDescription) + if (!isValidCategory(categoryEnum)) { + throw new Error(`Invalid app category enum: ${categoryEnum}`) + } - return await getMetadata({ - locale, - slug: ["apps", "categories", normalizedSlug], - title, - description, - }) + const title = t(category.metaTitle) + const description = t(category.metaDescription) + + return await getMetadata({ + locale, + slug: ["apps", "categories", normalizedSlug], + title, + description, + }) + } catch { + const t = await getTranslations("common") + + return { + title: t("page-not-found"), + description: t("page-not-found-description"), + } + } } export default Page diff --git a/app/[locale]/apps/page-jsonld.tsx b/app/[locale]/apps/page-jsonld.tsx index 3056fc5acaf..dffac2f2a38 100644 --- a/app/[locale]/apps/page-jsonld.tsx +++ b/app/[locale]/apps/page-jsonld.tsx @@ -4,16 +4,12 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" import { appsCategories } from "@/data/apps/categories" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function AppsJsonLD({ locale, contributors, @@ -34,8 +30,7 @@ export default async function AppsJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -44,13 +39,8 @@ export default async function AppsJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -68,8 +58,8 @@ export default async function AppsJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#apps` }, }, { diff --git a/app/[locale]/assets/_components/assets.tsx b/app/[locale]/assets/_components/assets.tsx index 208e12ae169..5be7788fc5c 100644 --- a/app/[locale]/assets/_components/assets.tsx +++ b/app/[locale]/assets/_components/assets.tsx @@ -76,15 +76,15 @@ import wallet from "@/public/images/wallet.png" import whatIsEthereum from "@/public/images/what-is-ethereum.png" const Row = (props: ChildOnlyProp) => ( -
+
) const H2 = (props: HTMLAttributes) => ( -

+

) const H3 = (props: ChildOnlyProp) => ( -

+

) const AssetsPage = () => { diff --git a/app/[locale]/assets/page-jsonld.tsx b/app/[locale]/assets/page-jsonld.tsx index 3034ec452c5..0012c2321e8 100644 --- a/app/[locale]/assets/page-jsonld.tsx +++ b/app/[locale]/assets/page-jsonld.tsx @@ -4,14 +4,10 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function AssetsJsonLD({ locale, contributors, @@ -32,8 +28,7 @@ export default async function AssetsJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -42,13 +37,8 @@ export default async function AssetsJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -66,8 +56,8 @@ export default async function AssetsJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#assets` }, }, { @@ -101,8 +91,7 @@ export default async function AssetsJsonLD({ url: normalizeUrlForJsonLd(locale, "/assets/#brand"), }, ], - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, }, ], } diff --git a/app/[locale]/bug-bounty/page-jsonld.tsx b/app/[locale]/bug-bounty/page-jsonld.tsx index 8d1feb7b91a..99e30b86438 100644 --- a/app/[locale]/bug-bounty/page-jsonld.tsx +++ b/app/[locale]/bug-bounty/page-jsonld.tsx @@ -4,14 +4,12 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" +import { KNOWN_PERSONS } from "@/lib/jsonld/persons" +import { personReference } from "@/lib/jsonld/utils" + export default async function BugBountyJsonLD({ locale, contributors, @@ -32,8 +30,8 @@ export default async function BugBountyJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + KNOWN_PERSONS["fredrik-svantes"], + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -42,13 +40,11 @@ export default async function BugBountyJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [ + personReference("fredrik-svantes"), + REFERENCE.ETHEREUM_COMMUNITY, + ], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -66,8 +62,8 @@ export default async function BugBountyJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, }, ], } diff --git a/app/[locale]/bug-bounty/page.tsx b/app/[locale]/bug-bounty/page.tsx index 1ecbef7ad02..237d9c4c18d 100644 --- a/app/[locale]/bug-bounty/page.tsx +++ b/app/[locale]/bug-bounty/page.tsx @@ -51,11 +51,11 @@ const Content = (props: ChildOnlyProp) => ( ) const H2 = ({ className, ...props }: ComponentProps<"h2">) => ( -

+

) const H4 = (props: ChildOnlyProp) => ( -

+

) const Text = ({ className, ...props }: ComponentProps<"p">) => ( @@ -71,7 +71,7 @@ const ClientRow = (props: ChildOnlyProp) => ( ) const Client = (props: ChildOnlyProp) => ( -
+
) const Row = (props: ChildOnlyProp) => ( @@ -256,16 +256,16 @@ export default async function Page(props: { params: Promise }) { {/* */} -
+
-
{" "} - +
{" "} + {t("page-upgrades-bug-bounty-title")}
}) {

- + {t("page-upgrades-bug-bounty-subtitle")} @@ -288,7 +288,7 @@ export default async function Page(props: { params: Promise }) {
- + {t("page-upgrades-bug-bounty-leaderboard")} @@ -380,7 +380,7 @@ export default async function Page(props: { params: Promise }) { /> -
+

{t("page-upgrades-bug-bounty-validity")}

@@ -394,7 +394,7 @@ export default async function Page(props: { params: Promise }) { })}
- + }) { {/* Out of Scope */}

{t("page-upgrades-bug-bounty-not-included")}

-

+

{t.rich("page-upgrades-bug-bounty-not-included-desc", { a: (chunks) => {chunks}, })} @@ -564,7 +564,7 @@ export default async function Page(props: { params: Promise }) { ] as const ).map(({ key, footnote }) => (

  • - + {t(key)} {footnote && *} @@ -572,16 +572,16 @@ export default async function Page(props: { params: Promise }) {
  • ))} -

    +

    * {t("page-upgrades-bug-bounty-out-of-scope-footnote")}

    {/* Bug Hunting Rules */} -
    +

    {t("page-upgrades-bug-bounty-hunting")}

    - + {t("page-upgrades-bug-bounty-hunting-desc")}
      @@ -595,9 +595,9 @@ export default async function Page(props: { params: Promise }) { ).map((key, idx) => (
    1. - + {idx + 1} {t(key)} @@ -612,12 +612,12 @@ export default async function Page(props: { params: Promise }) {

      {t("page-upgrades-bug-bounty-severity-qualifications-title")}

      -

      +

      {t("page-upgrades-bug-bounty-severity-qualifications-desc")}

      {/* Low */} -
      +
      {t("page-upgrades-bug-bounty-severity-low-title")} @@ -640,7 +640,7 @@ export default async function Page(props: { params: Promise }) {
      {/* Medium */} -
      +
      {t("page-upgrades-bug-bounty-severity-medium-title")} @@ -663,7 +663,7 @@ export default async function Page(props: { params: Promise }) {
      {/* High */} -
      +
      {t("page-upgrades-bug-bounty-severity-high-title")} @@ -686,7 +686,7 @@ export default async function Page(props: { params: Promise }) {
      {/* Critical */} -
      +
      {t("page-upgrades-bug-bounty-severity-critical-title")} @@ -737,7 +737,7 @@ export default async function Page(props: { params: Promise }) {
      diff --git a/app/[locale]/collectibles/_components/CollectiblesContent/index.tsx b/app/[locale]/collectibles/_components/CollectiblesContent/index.tsx index 6cf2fea3595..5c6d6f10469 100644 --- a/app/[locale]/collectibles/_components/CollectiblesContent/index.tsx +++ b/app/[locale]/collectibles/_components/CollectiblesContent/index.tsx @@ -83,7 +83,7 @@ const CollectiblesContent = ({ badges }: CollectiblesPageProps) => { return (
      {/* Already a contributor? section */} -
      +
      {t("page-collectibles-contributor-img-alt")}) => ( (
      - + {children} @@ -112,7 +112,7 @@ const HighlightCardFooter = ({ const CheckList = ({ className, ...props }: ListProps) => ( ( {children} @@ -253,7 +253,7 @@ const CollectiblesCurrentYear = ({ {t("page-collectibles-instructions-label")} - + - + - + -
      +
      {socialBadges.map((badge) => (
      -
      +
      {grouped[year].map((badge: Badge) => { const sanitizedName = badge.name .replace(/\s?ethereum.org\s?/i, " ") // Remove "ethereum.org" from label @@ -97,7 +97,7 @@ const CollectiblesPreviousYears = ({ alt={badge.name} className="size-16 transition-transform group-hover:scale-105 group-hover:transition-transform md:size-20" /> -
      +
      {label}
      diff --git a/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx b/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx index 9ae2fcd1416..1d17732e3f5 100644 --- a/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx +++ b/app/[locale]/collectibles/_components/CollectiblesProgress/index.tsx @@ -61,7 +61,7 @@ const CollectiblesProgress = ({ badges }: CollectiblesProgressProps) => { value={(ownedCount / (currentYearBadges.length || 1)) * 100} />
      -

      +

      {t("page-collectibles-index-frequency")}

      diff --git a/app/[locale]/collectibles/page-jsonld.tsx b/app/[locale]/collectibles/page-jsonld.tsx index 83f9c7ed943..60354eb9c9e 100644 --- a/app/[locale]/collectibles/page-jsonld.tsx +++ b/app/[locale]/collectibles/page-jsonld.tsx @@ -4,17 +4,13 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" import { COLLECTIBLES_BASE_URL } from "./constants" import type { Badge, Stats } from "./types" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function CollectiblesJsonLD({ locale, badges, @@ -39,8 +35,7 @@ export default async function CollectiblesJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -49,13 +44,8 @@ export default async function CollectiblesJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -73,8 +63,8 @@ export default async function CollectiblesJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#collectibles` }, }, { @@ -92,8 +82,7 @@ export default async function CollectiblesJsonLD({ url: badge.link || `${COLLECTIBLES_BASE_URL}/badge/${badge.id}`, image: badge.image, })), - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, additionalProperty: [ { "@type": "PropertyValue", diff --git a/app/[locale]/collectibles/page.tsx b/app/[locale]/collectibles/page.tsx index c2d55b8f117..93e711cc0ea 100644 --- a/app/[locale]/collectibles/page.tsx +++ b/app/[locale]/collectibles/page.tsx @@ -83,7 +83,7 @@ export default async function Page(props: { params: Promise }) { id="stats" className="flex flex-col gap-x-6 gap-y-4 px-4 xl:flex-row xl:px-12" > -
      +

      {t("page-collectibles-improve-title")}

      @@ -110,7 +110,7 @@ export default async function Page(props: { params: Promise }) {
      {/* Minted */} -
      +
      {stats.collectorsCount ? numberFormat(locale).format(stats.collectorsCount) @@ -121,7 +121,7 @@ export default async function Page(props: { params: Promise }) {
      {/* Collectors */} -
      +
      {stats.uniqueAddressesCount ? numberFormat(locale).format(stats.uniqueAddressesCount) @@ -132,7 +132,7 @@ export default async function Page(props: { params: Promise }) {
      {/* Unique Badges */} -
      +
      {stats.collectiblesCount ? numberFormat(locale).format(stats.collectiblesCount) diff --git a/app/[locale]/community/_components/community.tsx b/app/[locale]/community/_components/community.tsx index 68ce6d2c402..f9a6fda8220 100644 --- a/app/[locale]/community/_components/community.tsx +++ b/app/[locale]/community/_components/community.tsx @@ -75,7 +75,7 @@ const ImageContainer = ({ children }: ChildOnlyProp) => { } const Subtitle = ({ children }: ChildOnlyProp) => { - return

      {children}

      + return

      {children}

      } const FeatureContent = ({ children }: ChildOnlyProp) => { @@ -92,7 +92,7 @@ const H2 = ({ ...props }: BaseHTMLAttributes) => { return ( -

      +

      {children}

      ) @@ -161,7 +161,7 @@ const CommunityPage = () => { - +

      @@ -171,7 +171,7 @@ const CommunityPage = () => { {whyGetInvolvedCards.map((card, idx) => ( {

      -
      +
      - +

      {t("page-community-get-involved-title")} @@ -203,7 +203,7 @@ const CommunityPage = () => {
      {cards.map((card, idx) => ( {

      - +

      {t("page-community-open-source")}

      @@ -243,7 +243,7 @@ const CommunityPage = () => {
      - + @@ -272,7 +272,7 @@ const CommunityPage = () => { - +

      {t("page-community-support")}

      @@ -295,7 +295,7 @@ const CommunityPage = () => {
      -

      +

      {t("page-community-try-ethereum")}

      diff --git a/app/[locale]/community/events/_components/ContinentTabs.tsx b/app/[locale]/community/events/_components/ContinentTabs.tsx index a63b383b471..dc3d549d85b 100644 --- a/app/[locale]/community/events/_components/ContinentTabs.tsx +++ b/app/[locale]/community/events/_components/ContinentTabs.tsx @@ -127,7 +127,7 @@ export default function ContinentTabs({ /> {filteredEvents.length === 0 ? ( -

      {noEventsMessage}

      +

      {noEventsMessage}

      ) : (
      {displayedEvents.map((event) => { @@ -149,7 +149,7 @@ export default function ContinentTabs({
      {/* Logo + Title + Location */} - +
      -

      +

      {event.title}

      diff --git a/app/[locale]/community/events/_components/EventCard.stories.tsx b/app/[locale]/community/events/_components/EventCard.stories.tsx new file mode 100644 index 00000000000..b8cb910e1f7 --- /dev/null +++ b/app/[locale]/community/events/_components/EventCard.stories.tsx @@ -0,0 +1,101 @@ +import type { Meta, StoryObj } from "@storybook/nextjs" + +import type { EventItem } from "@/lib/types" + +import { VStack } from "@/components/ui/flex" + +import EventCard from "./EventCard" + +const meta = { + title: "Molecules / Community / EventCard", + component: EventCard, + decorators: [ + (Story) => ( +
      + +
      + ), + ], +} satisfies Meta + +export default meta + +type Story = StoryObj + +const baseEvent: EventItem = { + id: "devconnect-buenos-aires", + title: "Devconnect Buenos Aires", + logoImage: "https://avatars.githubusercontent.com/u/6250754?v=4", + bannerImage: "https://avatars.githubusercontent.com/u/6250754?v=4", + startTime: "2026-11-17T09:00:00Z", + endTime: "2026-11-22T18:00:00Z", + location: "Buenos Aires, Argentina", + link: "#", + tags: ["conference"], + eventTypes: ["conference"], + eventTypesLabels: ["Conference"], + isOnline: false, + continent: "south-america", + discord: null, + telegram: null, + twitter: null, + farcaster: null, +} + +const brokenEvent: EventItem = { + ...baseEvent, + id: "broken-image-event", + logoImage: "https://placehold.co/404error", + bannerImage: "https://placehold.co/404error", +} + +export const Grid: Story = { + args: { + event: baseEvent, + variant: "grid", + showTypeTag: true, + locale: "en", + }, +} + +export const GridBrokenImageFallback: Story = { + args: { + event: brokenEvent, + variant: "grid", + showTypeTag: true, + locale: "en", + }, +} + +export const Highlight: Story = { + args: { + event: baseEvent, + variant: "highlight", + locale: "en", + }, +} + +export const HighlightBrokenImageFallback: Story = { + args: { + event: brokenEvent, + variant: "highlight", + locale: "en", + }, +} + +export const FallbackComparison: Story = { + args: { + event: brokenEvent, + variant: "grid", + showTypeTag: true, + locale: "en", + }, + render: () => ( + + + + + + + ), +} diff --git a/app/[locale]/community/events/_components/EventCard.tsx b/app/[locale]/community/events/_components/EventCard.tsx index e0d945b0f7a..a11b3ee0013 100644 --- a/app/[locale]/community/events/_components/EventCard.tsx +++ b/app/[locale]/community/events/_components/EventCard.tsx @@ -1,3 +1,6 @@ +"use client" + +import { useState } from "react" import { MapPin } from "lucide-react" import type { EventItem, MatomoEventOptions } from "@/lib/types" @@ -15,7 +18,7 @@ interface EventCardProps { event: EventItem variant?: "grid" | "highlight" className?: string - locale?: string + locale: string showTypeTag?: boolean customEventOptions?: MatomoEventOptions } @@ -26,6 +29,8 @@ function EventCardGrid({ locale, customEventOptions, }: EventCardProps) { + const [logoError, setLogoError] = useState(false) + const primaryType = event.eventTypes?.[0] const hasDate = Boolean(event.startTime) @@ -36,7 +41,7 @@ function EventCardGrid({ : null return ( - +
      -
      - {event.logoImage ? ( +
      + {event.logoImage && !logoError ? ( {event.title} setLogoError(true)} /> ) : ( - + )}
      @@ -68,11 +74,11 @@ function EventCardGrid({ {event.eventTypesLabels?.[0] || primaryType} )} -

      +

      {event.title}

      {formattedDate &&

      {formattedDate}

      } -

      {event.location}

      +

      {event.location}

      @@ -85,36 +91,53 @@ function EventCardHighlight({ locale, customEventOptions, }: EventCardProps) { + const [logoError, setLogoError] = useState(false) + const [bannerError, setBannerError] = useState(false) + + const bannerSrc = event.bannerImage || event.logoImage + return ( - + -
      - {`${event.title} -
      -
      -
      +
      + {bannerSrc && !bannerError ? ( {event.title} setBannerError(true)} /> + ) : ( +
      + +
      + )} +
      +
      +
      + {event.logoImage && !logoError ? ( + {event.title} setLogoError(true)} + /> + ) : ( + + )}

      {event.title}

      -

      {event.location}

      -

      +

      {event.location}

      +

      {formatDateRange(event.startTime, event.endTime, locale)}

      @@ -128,7 +151,7 @@ export default function EventCard({ event, variant, className, - locale = "en", + locale, showTypeTag, customEventOptions, }: EventCardProps) { diff --git a/app/[locale]/community/events/_components/FilterEvents.tsx b/app/[locale]/community/events/_components/FilterEvents.tsx index b83a0b8d5a8..fbcc1ce5ab5 100644 --- a/app/[locale]/community/events/_components/FilterEvents.tsx +++ b/app/[locale]/community/events/_components/FilterEvents.tsx @@ -67,7 +67,7 @@ export default function FilterEvents({ events }: FilterProps) { return ( <> -
      +
      {filteredEvents.slice(0, MAX_RESULTS).map((event) => ( ({ + "@type": "Person", + name: contributor.login, + url: contributor.html_url, + })) + + const datedConferences = conferences.filter((c) => c.startTime) + + const jsonLd = { + "@context": "https://schema.org", + "@graph": [ + ...BASE_GRAPH_NODES, + { + "@type": "CollectionPage", + "@id": url, + name: t("page-events-conferences-hero-title", { year }), + description: t("page-events-meta-description", { year }), + url: url, + inLanguage: locale, + contributor: contributorList, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, + breadcrumb: { + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + position: 1, + name: "Home", + item: normalizeUrlForJsonLd(locale, "/"), + }, + { + "@type": "ListItem", + position: 2, + name: "Community", + item: normalizeUrlForJsonLd(locale, "/community/"), + }, + { + "@type": "ListItem", + position: 3, + name: common("events"), + item: normalizeUrlForJsonLd(locale, "/community/events/"), + }, + { + "@type": "ListItem", + position: 4, + name: t("page-events-section-upcoming-conferences"), + item: url, + }, + ], + }, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, + mainEntity: { "@id": `${url}#conferences` }, + }, + { + "@type": "ItemList", + "@id": `${url}#conferences`, + name: t("page-events-section-upcoming-conferences"), + numberOfItems: datedConferences.length, + itemListElement: datedConferences.map((event, i) => ({ + "@type": "ListItem", + position: i + 1, + item: toEventNode(event), + })), + }, + ], + } + + return +} diff --git a/app/[locale]/community/events/conferences/page.tsx b/app/[locale]/community/events/conferences/page.tsx index 3ec263e3f29..34ccb5498ae 100644 --- a/app/[locale]/community/events/conferences/page.tsx +++ b/app/[locale]/community/events/conferences/page.tsx @@ -1,7 +1,7 @@ import { pick } from "lodash" import { getMessages, getTranslations } from "next-intl/server" -import type { PageParams } from "@/lib/types" +import type { Lang, PageParams } from "@/lib/types" import ContentHero from "@/components/Hero/ContentHero" import I18nProvider from "@/components/I18nProvider" @@ -10,8 +10,10 @@ import { EdgeScrollContainer, EdgeScrollItem, } from "@/components/ui/edge-scroll-container" +import Link from "@/components/ui/Link" import { Section } from "@/components/ui/section" +import { getAppPageContributorInfo } from "@/lib/utils/contributors" import { getLocaleYear } from "@/lib/utils/date" import { getMetadata } from "@/lib/utils/metadata" import { getRequiredNamespacesForPage } from "@/lib/utils/translations" @@ -21,6 +23,8 @@ import EventCard from "../_components/EventCard" import OrganizerCTA from "../_components/OrganizerCTA" import { mapEventTranslations } from "../utils" +import ConferencesJsonLD from "./page-jsonld" + import { getEventsData } from "@/lib/data" const Page = async (props: { params: Promise }) => { @@ -32,7 +36,7 @@ const Page = async (props: { params: Promise }) => { const t = await getTranslations("page-community-events") // Apply translations and compute eventTypes from tags if missing - const events = mapEventTranslations(_events, t) + const events = mapEventTranslations(_events, t, locale) // Filter to conferences only (includes hackathons as they're often conference-adjacent) const conferences = events.filter( @@ -52,6 +56,11 @@ const Page = async (props: { params: Promise }) => { const requiredNamespaces = getRequiredNamespacesForPage("/community/events") const messages = pick(allMessages, requiredNamespaces) + const { contributors } = await getAppPageContributorInfo( + "community/events/conferences", + locale as Lang + ) + // Continent labels for tabs const continentLabels = { all: t("page-events-filter-all"), @@ -66,6 +75,11 @@ const Page = async (props: { params: Promise }) => { return ( + }) => { eventCategory: "Events_conferences", }} /> +

      + {t.rich("page-events-data-source-callout", { + a: (chunks) => {chunks}, + })} +

      {/* Footer CTA */} diff --git a/app/[locale]/community/events/meetups/_components/FilterMeetups.tsx b/app/[locale]/community/events/meetups/_components/FilterMeetups.tsx index 54ac000bfc9..b2c8e200c10 100644 --- a/app/[locale]/community/events/meetups/_components/FilterMeetups.tsx +++ b/app/[locale]/community/events/meetups/_components/FilterMeetups.tsx @@ -49,7 +49,7 @@ export default function FilterMeetups({ events }: FilterMeetupsProps) { {t("page-events-search-sr-text")} {filteredEvents.length ? ( -
      +
      {filteredEvents.map((event) => ( ({ + "@type": "Person", + name: contributor.login, + url: contributor.html_url, + })) + + // Only dated events belong in the Event ItemList; ongoing meetup groups + // (no startTime) are rendered on the page but aren't schema.org Events. + const datedMeetups = meetups.filter((m) => m.startTime) + + const jsonLd = { + "@context": "https://schema.org", + "@graph": [ + ...BASE_GRAPH_NODES, + { + "@type": "CollectionPage", + "@id": url, + name: t("page-events-meetups-hero-title", { year }), + description: t("page-events-meta-description", { year }), + url: url, + inLanguage: locale, + contributor: contributorList, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, + breadcrumb: { + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + position: 1, + name: "Home", + item: normalizeUrlForJsonLd(locale, "/"), + }, + { + "@type": "ListItem", + position: 2, + name: "Community", + item: normalizeUrlForJsonLd(locale, "/community/"), + }, + { + "@type": "ListItem", + position: 3, + name: common("events"), + item: normalizeUrlForJsonLd(locale, "/community/events/"), + }, + { + "@type": "ListItem", + position: 4, + name: t("page-events-section-local-meetups"), + item: url, + }, + ], + }, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, + mainEntity: { "@id": `${url}#meetups` }, + }, + { + "@type": "ItemList", + "@id": `${url}#meetups`, + name: t("page-events-section-local-meetups"), + numberOfItems: datedMeetups.length, + itemListElement: datedMeetups.map((event, i) => ({ + "@type": "ListItem", + position: i + 1, + item: toEventNode(event), + })), + }, + ], + } + + return +} diff --git a/app/[locale]/community/events/meetups/page.tsx b/app/[locale]/community/events/meetups/page.tsx index b00fde43761..ce085ef9d57 100644 --- a/app/[locale]/community/events/meetups/page.tsx +++ b/app/[locale]/community/events/meetups/page.tsx @@ -1,13 +1,14 @@ import { pick } from "lodash" import { getMessages, getTranslations } from "next-intl/server" -import type { PageParams } from "@/lib/types" +import type { Lang, PageParams } from "@/lib/types" import ContentHero from "@/components/Hero/ContentHero" import I18nProvider from "@/components/I18nProvider" import MainArticle from "@/components/MainArticle" import { Section } from "@/components/ui/section" +import { getAppPageContributorInfo } from "@/lib/utils/contributors" import { getLocaleYear } from "@/lib/utils/date" import { getMetadata } from "@/lib/utils/metadata" import { getRequiredNamespacesForPage } from "@/lib/utils/translations" @@ -16,6 +17,7 @@ import OrganizerCTA from "../_components/OrganizerCTA" import { getMeetupGroups, mapEventTranslations } from "../utils" import FilterMeetups from "./_components/FilterMeetups" +import MeetupsJsonLD from "./page-jsonld" import { getEventsData } from "@/lib/data" @@ -28,7 +30,7 @@ const Page = async (props: { params: Promise }) => { const t = await getTranslations("page-community-events") // Apply translations and compute eventTypes from tags if missing - const events = mapEventTranslations(_events, t) + const events = mapEventTranslations(_events, t, locale) // Combine API meetup events with legacy meetup groups // Exclude conferences and hackathons - they have their own section @@ -37,7 +39,7 @@ const Page = async (props: { params: Promise }) => { !e.eventTypes?.includes("conference") && !e.eventTypes?.includes("hackathon") ) - const meetupGroups = getMeetupGroups() + const meetupGroups = getMeetupGroups(locale) // Show API meetups first (sorted by date), then groups (sorted alphabetically) const meetups = [...apiMeetups, ...meetupGroups] @@ -45,8 +47,18 @@ const Page = async (props: { params: Promise }) => { const requiredNamespaces = getRequiredNamespacesForPage("/community/events") const messages = pick(allMessages, requiredNamespaces) + const { contributors } = await getAppPageContributorInfo( + "community/events/meetups", + locale as Lang + ) + return ( <> + }) => { locale as Lang ) - const events = mapEventTranslations(_events, t) + const events = mapEventTranslations(_events, t, locale) // Get highlighted conferences (with highlight flag or first 3) const conferences = events.filter( @@ -82,7 +82,7 @@ const Page = async (props: { params: Promise }) => { !e.eventTypes?.includes("conference") && !e.eventTypes?.includes("hackathon") ) - const meetupGroups = getMeetupGroups() + const meetupGroups = getMeetupGroups(locale) const meetups = [...apiMeetups, ...meetupGroups] // Continent labels for tabs @@ -202,7 +202,7 @@ const Page = async (props: { params: Promise }) => { key={id} className={cn( "ms-6 w-[calc(100%-4rem)] max-w-96 md:w-96 lg:max-w-[30%] xl:max-w-[22%]", - "flex flex-col justify-between gap-4 rounded-4xl border p-8 shadow-lg", + "rounded-4xl flex flex-col justify-between gap-4 border p-8 shadow-lg", logoBgColor )} > @@ -259,14 +259,14 @@ const Page = async (props: { params: Promise }) => { -
      +
      {t("page-events-hub-apply-cta")} @@ -277,7 +277,7 @@ const Page = async (props: { params: Promise }) => { {/* Find events near you */}

    - {/* Card 1: Something went wrong */} - {t("page-community-support-something-went-wrong")}

    - } - svg={} - className="h-fit" - > -
    -

    - {t("page-community-support-something-went-wrong-description")} -

    - - {t("page-community-support-lost-funds-scam")} - - - {t("page-community-support-secure-remaining-funds")} - - - {t("page-community-support-report-scam")} - - - {t("page-community-support-trace-funds")} - - - {t("page-community-support-sent-wrong-address")} - - - {t("page-community-support-lost-wallet-access")} - - - {t("page-community-support-stuck-transaction")} - -
    - - - {/* Card 2: Protect yourself */} - {t("page-community-support-protect-yourself")}

    } - svg={} - className="h-fit" - > -
    -

    - {t("page-community-support-protect-yourself-description")} -

    - - {t("page-community-support-common-scam-tactics")} - - - {t("page-community-support-recovery-experts-scams")} - - - {t("page-community-support-identify-scam-tokens")} - - - {t("page-community-support-full-security-guide")} - - - {t("page-community-support-revoke-approvals")} - -
    - + {sections.getHelp.map( + ({ + titleKey, + Svg, + colorClass, + descriptionKey, + eventAction, + items, + }) => ( + {t(titleKey)}} + svg={} + className="h-fit" + > +
    +

    + {t(descriptionKey)} +

    + {items.map(({ labelKey, href, eventName }) => ( + + {t(labelKey)} + + ))} +
    +
    + ) + )}
    @@ -241,147 +130,56 @@ export default async function Page(props: { params: Promise }) { {t("page-community-support-learn")}
    - {/* Card 3: Using Ethereum */} - {t("page-community-support-using-ethereum")}} - svg={} - className="h-fit" - > -
    -

    - {t("page-community-support-using-ethereum-description")} -

    - - {t("page-community-support-create-account")} - - - {t("page-community-support-use-wallet")} - - - {t("page-community-support-swap-tokens")} - - - {t("page-community-support-bridge-tokens")} - - - {t("page-community-support-revoke-token-access")} - -
    -
    - - {/* Card 4: Common misconceptions */} - {t("page-community-support-common-misconceptions")} - } - svg={} - className="h-fit" - > -
    -

    - {t( - "page-community-support-common-misconceptions-description" - )} -

    - - {t("page-community-support-is-ethereum-company")} - - - {t("page-community-support-recover-freeze-funds")} - - - {t("page-community-support-mine-ethereum")} - - - {t("page-community-support-is-support-team")} - -
    -
    + {sections.learn.map( + ({ + titleKey, + Svg, + colorClass, + descriptionKey, + eventAction, + items, + }) => ( + {t(titleKey)}} + svg={} + className="h-fit" + > +
    +

    + {t(descriptionKey)} +

    + {items.map(({ labelKey, href, eventName }) => ( + + {t(labelKey)} + + ))} +
    +
    + ) + )}
    {/* Still need help? */}

    {t("page-community-support-still-need-help")}

    -

    +

    {t("page-community-support-still-need-help-description")}

    & RefAttributes + > + colorClass: string + descriptionKey: string + eventAction: string + items: { labelKey: string; href: string; eventName: string }[] +} diff --git a/app/[locale]/contributing/translation-program/acknowledgements/_components/acknowledgements.tsx b/app/[locale]/contributing/translation-program/acknowledgements/_components/acknowledgements.tsx index 5fe387a4371..15f8be562db 100644 --- a/app/[locale]/contributing/translation-program/acknowledgements/_components/acknowledgements.tsx +++ b/app/[locale]/contributing/translation-program/acknowledgements/_components/acknowledgements.tsx @@ -37,7 +37,7 @@ const H2 = ({ className, ...props }: BaseHTMLAttributes) => ( -

    +

    ) const Text = ({ @@ -62,14 +62,14 @@ const TranslatorAcknowledgements = () => { -

    +

    {t( "page-contributing-translation-program-acknowledgements-acknowledgement-page-title" )}

    - + {/* LEFT COLUMN */} -
    +
    {t( "page-contributing-translation-program-acknowledgements-acknowledgement-page-1" @@ -102,7 +102,7 @@ const TranslatorAcknowledgements = () => {

    */}
    {/* RIGHT COLUMN */} -
    +
    { {t("page-contributing-translation-program-acknowledgements-3")} -

    +

    {t( "page-contributing-translation-program-acknowledgements-how-to-claim-title" )} diff --git a/app/[locale]/contributing/translation-program/acknowledgements/page-jsonld.tsx b/app/[locale]/contributing/translation-program/acknowledgements/page-jsonld.tsx index 39859ad4e5d..00b324e8455 100644 --- a/app/[locale]/contributing/translation-program/acknowledgements/page-jsonld.tsx +++ b/app/[locale]/contributing/translation-program/acknowledgements/page-jsonld.tsx @@ -4,14 +4,10 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function AcknowledgementsJsonLD({ locale, contributors, @@ -37,8 +33,7 @@ export default async function AcknowledgementsJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -51,13 +46,8 @@ export default async function AcknowledgementsJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -92,8 +82,8 @@ export default async function AcknowledgementsJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, }, ], } diff --git a/app/[locale]/contributing/translation-program/contributors/_components/contributors.tsx b/app/[locale]/contributing/translation-program/contributors/_components/contributors.tsx index 1d379bf2c9d..4c15ed0df24 100644 --- a/app/[locale]/contributing/translation-program/contributors/_components/contributors.tsx +++ b/app/[locale]/contributing/translation-program/contributors/_components/contributors.tsx @@ -43,10 +43,10 @@ const Contributors = () => { -

    +

    {t("page-contributing-translation-program-contributors-title")}

    -

    +

    {t( "page-contributing-translation-program-contributors-number-of-contributors" )}{" "} @@ -74,7 +74,7 @@ const Contributors = () => { . -

    +

    {t("page-contributing-translation-program-contributors-thank-you")}

    diff --git a/app/[locale]/contributing/translation-program/contributors/page-jsonld.tsx b/app/[locale]/contributing/translation-program/contributors/page-jsonld.tsx index bbc9f2236d8..6fe48b3b382 100644 --- a/app/[locale]/contributing/translation-program/contributors/page-jsonld.tsx +++ b/app/[locale]/contributing/translation-program/contributors/page-jsonld.tsx @@ -4,14 +4,10 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function ContributorsJsonLD({ locale, contributors, @@ -37,8 +33,7 @@ export default async function ContributorsJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -51,13 +46,8 @@ export default async function ContributorsJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -92,8 +82,8 @@ export default async function ContributorsJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, }, ], } diff --git a/app/[locale]/contributing/translation-program/translatathon/leaderboard/_components/Leaderboard.tsx b/app/[locale]/contributing/translation-program/translatathon/leaderboard/_components/Leaderboard.tsx index 21cf5f2188a..2dfd4992561 100644 --- a/app/[locale]/contributing/translation-program/translatathon/leaderboard/_components/Leaderboard.tsx +++ b/app/[locale]/contributing/translation-program/translatathon/leaderboard/_components/Leaderboard.tsx @@ -57,7 +57,7 @@ export const Leaderboard = () => { } return ( -
    +
    #
    @@ -83,7 +83,7 @@ export const Leaderboard = () => { return (
    @@ -112,7 +112,7 @@ export const Leaderboard = () => { onClick={showMore} className="m-2 mx-0 flex h-full w-full items-center justify-center rounded-full px-6 py-4 lg:mx-2 lg:w-auto" > - + Show more diff --git a/app/[locale]/contributing/translation-program/translatathon/leaderboard/page-jsonld.tsx b/app/[locale]/contributing/translation-program/translatathon/leaderboard/page-jsonld.tsx index 789cde9736f..6afbee664fa 100644 --- a/app/[locale]/contributing/translation-program/translatathon/leaderboard/page-jsonld.tsx +++ b/app/[locale]/contributing/translation-program/translatathon/leaderboard/page-jsonld.tsx @@ -2,14 +2,10 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function TranslatathonLeaderboardJsonLD({ locale, contributors, @@ -31,8 +27,7 @@ export default async function TranslatathonLeaderboardJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -42,13 +37,8 @@ export default async function TranslatathonLeaderboardJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -90,8 +80,8 @@ export default async function TranslatathonLeaderboardJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, }, ], } diff --git a/app/[locale]/developers/_components/BuilderCard.tsx b/app/[locale]/developers/_components/BuilderCard.tsx index 40bb6b87616..5573c135b65 100644 --- a/app/[locale]/developers/_components/BuilderCard.tsx +++ b/app/[locale]/developers/_components/BuilderCard.tsx @@ -25,13 +25,13 @@ const BuilderCard = ({ path, className }: BuildCardProps) => ( {path.tag} )}

    {path.title}

    -

    {path.description}

    +

    {path.description}

    ( - + - - + + ) diff --git a/app/[locale]/developers/_components/SpeedRunCard.tsx b/app/[locale]/developers/_components/SpeedRunCard.tsx index 9218419cadb..2e72fdd4a67 100644 --- a/app/[locale]/developers/_components/SpeedRunCard.tsx +++ b/app/[locale]/developers/_components/SpeedRunCard.tsx @@ -27,12 +27,12 @@ const SpeedRunCard = ({ alt="SpeedRunEthereum banner" sizes="(max-width: 768px) 100vw, 50vw" /> -
    +

    {title}

    {description}

    ( {course.hours} -

    +

    {course.title}

    -

    {course.description}

    +

    {course.description}

    ) diff --git a/app/[locale]/developers/page-jsonld.tsx b/app/[locale]/developers/page-jsonld.tsx index 8d064c29c22..bff93c429aa 100644 --- a/app/[locale]/developers/page-jsonld.tsx +++ b/app/[locale]/developers/page-jsonld.tsx @@ -4,16 +4,12 @@ import { EventItem, FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" import { DevelopersPath, VideoCourse } from "./types" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function DevelopersPageJsonLD({ locale, paths, @@ -40,8 +36,7 @@ export default async function DevelopersPageJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -50,13 +45,8 @@ export default async function DevelopersPageJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -74,8 +64,8 @@ export default async function DevelopersPageJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#developers` }, }, { @@ -108,8 +98,7 @@ export default async function DevelopersPageJsonLD({ url: hackathon.link, })), ], - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, }, ], } diff --git a/app/[locale]/developers/page.tsx b/app/[locale]/developers/page.tsx index e0174d0f026..968a7acf2c5 100644 --- a/app/[locale]/developers/page.tsx +++ b/app/[locale]/developers/page.tsx @@ -48,17 +48,17 @@ import scaffoldDebugScreenshot from "@/public/images/developers/scaffold-debug-s import stackExchangeScreenshot from "@/public/images/developers/stack-exchange-screenshot.png" import tutorialTagsBanner from "@/public/images/developers/tutorial-tags-banner.png" import dogeImage from "@/public/images/doge-computer.png" -import EventFallback from "@/public/images/events/event-placeholder.png" +import fallbackThumbnail from "@/public/images/eth-glyph-thumbnail.png" import heroImage from "@/public/images/heroes/developers-hub-hero.png" -const H3 = (props: ChildOnlyProp) =>

    +const H3 = (props: ChildOnlyProp) =>

    const Text = (props: ChildOnlyProp) =>

    const Column = (props: ChildOnlyProp) => ( -

    +
    ) const RightColumn = (props: ChildOnlyProp) => ( -
    +
    ) const Scroller = ({ @@ -103,9 +103,9 @@ const WhyGrid = () => { return (
    {items.map(({ heading, description }) => ( @@ -186,7 +186,7 @@ const DevelopersPage = async (props: { params: Promise }) => { id="why" className={cn( "grid grid-cols-1 gap-6 md:gap-10 lg:grid-cols-2", - "-mx-8 w-screen max-w-screen-2xl items-center bg-background-highlight px-8 py-10 md:py-20" + "bg-background-highlight -mx-8 w-screen max-w-screen-2xl items-center px-8 py-10 md:py-20" )} >
    @@ -261,7 +261,7 @@ const DevelopersPage = async (props: { params: Promise }) => { id="resources" className={cn( "grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-8", - "-mx-8 w-screen max-w-screen-2xl bg-background-highlight px-8 py-10 md:py-20" + "bg-background-highlight -mx-8 w-screen max-w-screen-2xl px-8 py-10 md:py-20" )} >

    @@ -269,7 +269,7 @@ const DevelopersPage = async (props: { params: Promise }) => {

    {/* Quickstart your idea */} - + Scaffold-ETH 2 debug screenshot }) => { />

    {t("page-developers-jump-right-in-title")}

    -

    +

    {t("page-developers-quickstart-scaffold-subtext")}{" "} }) => {

    -
    +
    npx create-eth@latest @@ -324,7 +324,7 @@ const DevelopersPage = async (props: { params: Promise }) => { {/* Get help */} - + Ethereum Stack Exchange screenshot }) => { />

    {t("page-developers-get-help-title")}

    -

    +

    {t("page-developers-get-help-desc")}

    @@ -362,7 +362,7 @@ const DevelopersPage = async (props: { params: Promise }) => {
    {/* Resources */} - + Banner showing four resource app icons }) => { />

    {t("page-developers-resources-title")}

    -

    +

    {t("page-developers-resources-desc")}

    @@ -393,7 +393,7 @@ const DevelopersPage = async (props: { params: Promise }) => {
    {/* Tutorials */} - + Banner displaying multiple learning topics in a tag cloud }) => { />

    {t("page-developers-tutorials-title")}

    -

    +

    {t("page-developers-tutorials-desc")}

    @@ -449,7 +449,7 @@ const DevelopersPage = async (props: { params: Promise }) => { id="docs" className={cn( "shadow-table-item-box", - "-mx-8 w-screen max-w-screen-2xl bg-background-highlight px-8 py-10 md:py-20" + "bg-background-highlight -mx-8 w-screen max-w-screen-2xl px-8 py-10 md:py-20" )} >
    @@ -598,13 +598,13 @@ const DevelopersPage = async (props: { params: Promise }) => { eventAction: "click", eventName: title, }} - className="min-w-72 max-w-md flex-1" + className="max-w-md min-w-72 flex-1" > {bannerImage ? ( ) : ( - + )} @@ -642,12 +642,12 @@ const DevelopersPage = async (props: { params: Promise }) => {
    -
    +

    {t("page-developers-founders-title")}

    {t("page-developers-founders-desc")}

    diff --git a/app/[locale]/developers/tools/[category]/page-jsonld.tsx b/app/[locale]/developers/tools/[category]/page-jsonld.tsx index dab3551d411..11f72e69e92 100644 --- a/app/[locale]/developers/tools/[category]/page-jsonld.tsx +++ b/app/[locale]/developers/tools/[category]/page-jsonld.tsx @@ -4,16 +4,12 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" import type { DeveloperTool, DeveloperToolCategorySlug } from "../types" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function DevelopersToolsCategoryJsonLD({ locale, category, @@ -38,8 +34,7 @@ export default async function DevelopersToolsCategoryJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -50,13 +45,8 @@ export default async function DevelopersToolsCategoryJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -86,8 +76,8 @@ export default async function DevelopersToolsCategoryJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#category-tools` }, }, { diff --git a/app/[locale]/developers/tools/[category]/page.tsx b/app/[locale]/developers/tools/[category]/page.tsx index 6b662021e7b..2f023144784 100644 --- a/app/[locale]/developers/tools/[category]/page.tsx +++ b/app/[locale]/developers/tools/[category]/page.tsx @@ -117,7 +117,7 @@ const Page = async (props: {

    {t("page-developers-tools-categories-title-other")}

    -
    +
    {DEV_TOOL_CATEGORIES.filter(({ slug }) => slug !== category).map( ({ slug, Icon }) => ( -
    +
    {filteredTools.map((tool) => ( { return (
    -
    +
    {tool.banner_url && ( { className="lowercase" />
    -
    +
    {await renderSimpleMarkdown( translatedDescription, mdComponentOverrides )}
    -
    +

    {t("page-developers-tools-modal-links")}

    {tool.website && ( diff --git a/app/[locale]/developers/tools/page-jsonld.tsx b/app/[locale]/developers/tools/page-jsonld.tsx index d3f765fea58..f562ad57084 100644 --- a/app/[locale]/developers/tools/page-jsonld.tsx +++ b/app/[locale]/developers/tools/page-jsonld.tsx @@ -4,16 +4,12 @@ import { FileContributor } from "@/lib/types" import PageJsonLD from "@/components/PageJsonLD" -import { - ethereumCommunityOrganization, - ethereumCommunityReference, - ethereumFoundationOrganization, - ethereumFoundationReference, -} from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" import { DEV_TOOL_CATEGORIES } from "./constants" +import { BASE_GRAPH_NODES, REFERENCE } from "@/lib/jsonld/constants" + export default async function DevelopersToolsJsonLD({ locale, contributors, @@ -34,8 +30,7 @@ export default async function DevelopersToolsJsonLD({ const jsonLd = { "@context": "https://schema.org", "@graph": [ - ethereumFoundationOrganization, - ethereumCommunityOrganization, + ...BASE_GRAPH_NODES, { "@type": "WebPage", "@id": url, @@ -44,13 +39,8 @@ export default async function DevelopersToolsJsonLD({ url: url, inLanguage: locale, contributor: contributorList, - author: [ethereumCommunityReference], - isPartOf: { - "@type": "WebSite", - "@id": "https://ethereum.org/#website", - name: "ethereum.org", - url: "https://ethereum.org", - }, + author: [REFERENCE.ETHEREUM_COMMUNITY], + isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE, breadcrumb: { "@type": "BreadcrumbList", itemListElement: [ @@ -74,8 +64,8 @@ export default async function DevelopersToolsJsonLD({ }, ], }, - publisher: ethereumFoundationReference, - reviewedBy: ethereumFoundationReference, + publisher: REFERENCE.ETHEREUM_FOUNDATION, + reviewedBy: REFERENCE.ETHEREUM_FOUNDATION, mainEntity: { "@id": `${url}#developer-tools` }, }, { diff --git a/app/[locale]/developers/tools/page.tsx b/app/[locale]/developers/tools/page.tsx index 42eadb8e37f..87a50e3add4 100644 --- a/app/[locale]/developers/tools/page.tsx +++ b/app/[locale]/developers/tools/page.tsx @@ -95,7 +95,7 @@ const Page = async (props: { className="ms-6 w-[calc(100%-4rem)] max-w-md md:min-w-96 md:flex-1 lg:max-w-[33%]" > - +
    -

    +

    {t(`page-developers-tools-category-${slug}-title`)}

    @@ -295,10 +295,10 @@ const TutorialsList = ({ internalTutorials }: TutorialsListProps) => {
    {/* Filter controls */} -
    +
    {/* Row 2: Topic tags */}
    -

    +

    @@ -318,7 +318,7 @@ const TutorialsList = ({ internalTutorials }: TutorialsListProps) => { {nicheTags.length > 0 && (