Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .claude/skills/design-system/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ When the existing primitive doesn't quite fit, the answer is usually "add a vari
8. **`useRtlFlip()` for directional icons** (right-pointing arrows/chevrons). Or use `ChevronNext`/`ChevronPrev` from `@/components/Chevron`.
9. **Markdown content goes through `MdComponents`.** The legacy `@/components/Card` (default export) is reserved for markdown shortcodes -- never import it from app code; use `@/components/ui/card`.
10. **Storybook stories ship with new UI components.** No automated unit tests; Storybook + Chromatic + types are the verification layer.
11. **Don't add new layouts.** There are six canonical layouts (`TopicLayout`, `StaticLayout`, `DocsLayout`, `TutorialLayout`, `ContentLayout`, `BaseLayout`). New sectioned content goes in `src/data/topics/<key>.ts` as a `TopicLayout` config -- not a new layout component. See `references/layouts.md`.

## Highest-Value Gotchas (read these now)

Expand Down Expand Up @@ -105,6 +106,7 @@ Pull these in only when the trigger applies. Don't read them all upfront.
- **`references/a11y.md`** -- Load when adding interactive elements (modals, dropdowns, custom click targets), building forms, or working with images and headings.
- **`references/card-walkthrough.md`** -- Load when starting any card-shaped UI work; an end-to-end worked example.
- **`references/page-hero-walkthrough.md`** -- Load when starting a new page that needs a hero; an end-to-end worked example.
- **`references/layouts.md`** -- Load when you're tempted to create a new layout, when adding a new topic-hub section, or when refactoring a one-off `src/layouts/md/<Section>Layout` file. The canonical inventory plus the rule that new layouts are very rare.
- **`references/new-component-checklist.md`** -- Load before opening a PR for a new component. The pre-merge checklist.

## Other Project Skills That May Apply
Expand Down
43 changes: 43 additions & 0 deletions .claude/skills/design-system/references/cleanup-playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,46 @@ import { Stack } from "@/components/ui/flex"
```

(Stack already defaults to `flex-col gap-2`, so often you can drop the className entirely.)

## Per-section `src/layouts/md/<Section>Layout` -> `TopicLayout` config

```tsx
// Before -- a section gets its own layout file that duplicates 90% of every other section's layout:
// src/layouts/md/Staking.tsx
export const StakingLayout = ({ children, frontmatter, slug, ... }) => {
const { t } = useTranslation("page-staking")
const dropdownLinks = { text: t("..."), items: [ /* ... */ ] }
const heroProps = { ...frontmatter, breadcrumbs: { slug, startDepth: 1 }, heroImg: { ... } }
return <ContentLayout dropdownLinks={dropdownLinks} heroSection={<ContentHero {...heroProps} />}>{children}</ContentLayout>
}

// After -- the data lives in a config file, no layout component needed:
// src/data/topics/staking.ts
import type { TopicConfig } from "."
export const staking: TopicConfig = {
translationNs: "page-staking",
dropdown: {
textKey: "page-staking-dropdown-staking-options",
ariaLabelKey: "page-staking-dropdown-staking-options-alt",
matomoCategory: "Staking dropdown",
items: [
{ textKey: "page-staking-dropdown-home", href: "/staking/", matomoEvent: "clicked staking home" },
// ...
],
},
heroImage: { width: 800, height: 605 },
}

// src/layouts/index.ts
export const layoutMapping = {
// ...
staking: TopicLayout, // was: StakingLayout
}
```

After the move:
- Keep the section's MDX component bundle (`stakingComponents`) in `src/layouts/md/<key>.tsx`; only the layout export goes away.
- Delete any per-section heading overrides (`Heading1`/`Heading2`/etc. with extra className). The defaults in `MdComponents` are the baseline.
- If the section needs a swap-in component (HubHero on a specific slug, content after the markdown), use `config.hubHero` or the `afterContent` prop -- don't fork a new layout.

See `references/layouts.md` for the full inventory and `docs/topic-layout-refactor.md` for the worked migration.
127 changes: 127 additions & 0 deletions .claude/skills/design-system/references/layouts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Layouts

> **TL;DR**: Creating a new layout is a very rare exception. There are six canonical layouts and you almost never need a seventh. Reach for a `TopicLayout` config or a small slot prop before opening a new layout file.

## The Canonical Layouts

The site has **six** layouts. All live in `src/layouts/`. Each maps to a `template:` value in markdown frontmatter via `layoutMapping` (`src/layouts/index.ts`).

| Layout | File | When to use | `template:` values it serves |
|---|---|---|---|
| `TopicLayout` | `src/layouts/Topic.tsx` | A topic hub with a shared sub-nav dropdown linking sibling pages. The workhorse for sectioned educational content. | `staking`, `use-cases`, `roadmap`, `upgrade`, `ai-agents` |
| `StaticLayout` | `src/layouts/Static.tsx` | One-off markdown pages with no sub-nav. | `static` (default fallback) |
| `DocsLayout` | `src/layouts/Docs.tsx` | Developer docs with the docs sidebar. | `docs` |
| `TutorialLayout` | `src/layouts/Tutorial.tsx` | Long-form developer tutorials with author/date/skill metadata. | `tutorial` |
| `ContentLayout` | `src/layouts/ContentLayout.tsx` | **Not a top-level layout.** Underlying scaffold consumed by the four above plus a handful of app-router pages (e.g. `/learn/`). | n/a (composed, not selected) |
| `BaseLayout` | `src/layouts/BaseLayout.tsx` | Root document scaffold (`<html>`, providers). Applied automatically by the App Router. | n/a |

That's the whole inventory. If a UI need can be met by configuring one of these (especially `TopicLayout`), it should be.

## The Habit: Configure, Don't Add a New Layout

When you have a page or section that "needs its own layout," walk this list top-to-bottom and stop at the first match:

1. **Does the page render through `[...slug]` with a markdown source?** Pick the right `template:` value. `TopicLayout` for anything with a sub-nav across sibling pages, `static` otherwise. No new layout.
2. **Does the page have a sub-nav dropdown linking related sibling pages?** That's exactly what `TopicLayout` is for. Add a `src/data/topics/<key>.ts` config file. Route `layoutMapping[<key>] = TopicLayout`. No new layout.
3. **Is the variation just a swap-in component (hero, before-content, after-content)?** Use the slots `TopicLayout` already exposes (`afterContent`, `heroSection` override via config `hubHero`) or add a narrow new slot. No new layout.
4. **Is the page a one-off App Router page (`app/[locale]/<route>/page.tsx`) with non-markdown content?** Compose `ContentLayout` directly (see `/learn/`). No new layout.

If you can stop at any of those steps, you don't need a new layout file.

## `TopicLayout` in Practice

`TopicLayout` is the canonical "topic hub" layout. It renders a hero, a TOC, content, a contributors block, a feedback card, and a sub-nav dropdown linking sibling pages. Everything per-topic comes from data.

### Adding a new topic

To add a new topic-style section (e.g. a new `developer-platforms` hub), you need **two** changes — no React layout file required:

#### 1. Create the topic config

```ts
// src/data/topics/developer-platforms.ts
import type { TopicConfig } from "."

export const developerPlatforms: TopicConfig = {
translationNs: "page-developer-platforms",
dropdown: {
textKey: "page-developer-platforms-dropdown",
ariaLabelKey: "page-developer-platforms-dropdown-aria",
matomoCategory: "developer platforms menu",
items: [
{ textKey: "page-developer-platforms-dropdown-home", href: "/developer-platforms/", matomoEvent: "home" },
{ textKey: "page-developer-platforms-dropdown-tools", href: "/developer-platforms/tools/", matomoEvent: "tools" },
{ textKey: "page-developer-platforms-dropdown-frameworks", href: "/developer-platforms/frameworks/", matomoEvent: "frameworks" },
],
},
}
```

#### 2. Wire it into the map

```ts
// src/data/topics/index.ts
import { developerPlatforms } from "./developer-platforms"

export const topics: Partial<Record<Layout, TopicConfig>> = {
// ...
"developer-platforms": developerPlatforms,
}

// src/layouts/index.ts
export const layoutMapping = {
// ...
"developer-platforms": TopicLayout,
}
```

#### 3. Add translation keys

In `src/intl/en/page-developer-platforms.json`. The intl-pipeline propagates to other locales.

That's it. No new layout component. No new MDX wiring beyond a `componentsMapping` entry (only if the section needs custom MDX components — most don't).

### Slots on `TopicLayout`

When the topic genuinely needs something extra:

- **`config.hubHero`** — Swap `ContentHero` for `HubHero` on a specific slug (used by Roadmap on `/roadmap/`). Declarative; lives in the topic config.
- **`config.editBanner`** — Render the top-of-page "edit this page" banner on every page in the topic. Used by UseCases and AiAgents. Per-page opt-out via frontmatter `hideEditBanner: true` if a specific page needs to suppress.
- **`afterContent` prop** — Render arbitrary JSX after the markdown content. Used by Staking for its community callout. Passed by the slug router for the one or two topics that need it. If you find yourself wanting a *third* `afterContent` consumer, consider promoting it to `config.afterContent` (still keyed by topic data).

If your topic needs something none of these expose, the right move is usually a narrow new slot on `TopicLayout`, not a new layout file.

## When a New Layout IS the Answer

The bar is high. A new layout is justified only when:

- The page renders through a fundamentally different content shape (e.g. a JSON-API-backed page vs markdown-content)
- It has a different navigation chrome (e.g. the docs sidebar, the tutorial metadata block) that doesn't fit `ContentLayout`'s scaffolding
- It manages a different content lifecycle (e.g. live data, streamed responses)

Cosmetic variation, different copy, a different sub-nav list, or a different hero image is **never** justification for a new layout. Those are all configuration of an existing layout.

## When You Find a One-Off Layout File

If you encounter a `src/layouts/md/<Something>.tsx` that exports its own `<Something>Layout` component (this is the pattern the topic refactor cleaned up), it's a cleanup target:

1. Confirm it's a topic-hub-with-sub-nav pattern (it almost always is)
2. Extract the dropdown items + translation namespace into `src/data/topics/<key>.ts`
3. Route `layoutMapping[<key>] = TopicLayout`
4. Delete the layout export from the file; keep the MDX components export
5. Smoke the section's pages

See `docs/topic-layout-refactor.md` for the worked example.

## Pre-Merge Checklist for Layout Work

Before opening a PR that touches anything in `src/layouts/`:

- [ ] Am I sure this isn't a `TopicLayout` config addition?
- [ ] Am I sure this isn't a slot/prop addition to an existing layout?
- [ ] Have I checked the `layoutMapping` to confirm no existing layout fits?
- [ ] Have I read `docs/topic-layout-refactor.md` for context on why the topic layouts were consolidated?
- [ ] If introducing a new layout (very rare), do I have explicit signoff from a maintainer?
- [ ] If extending `ContentLayout`, is the new prop genuinely shared across multiple consumers — not a one-section special case?

If you can't say yes to all of these and you're about to add `src/layouts/<NewName>.tsx`, stop and re-read the top of this file.
40 changes: 38 additions & 2 deletions app/[locale]/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ import type { GHIssue, SlugPageParams } from "@/lib/types"

import I18nProvider from "@/components/I18nProvider"
import mdComponents from "@/components/MdComponents"
import StakingCommunityCallout from "@/components/Staking/StakingCommunityCallout"
import VideoWatch from "@/components/Videos/VideoWatch"

import { dateToString } from "@/lib/utils/date"
import { getLayoutFromSlug } from "@/lib/utils/layout"
import { checkPathValidity, getPostSlugs } from "@/lib/utils/md"
import { getRequiredNamespacesForPage } from "@/lib/utils/translations"

import { topics } from "@/data/topics"
import { getGFIs } from "@/data-layer"

import SlugJsonLD from "./page-jsonld"

import { componentsMapping, layoutMapping } from "@/layouts"
import { componentsMapping, layoutMapping, TopicLayout } from "@/layouts"
import { getPageData } from "@/lib/md/data"
import { getMdMetadata } from "@/lib/md/metadata"

Expand Down Expand Up @@ -67,7 +69,7 @@ export default async function Page(props: { params: Promise<SlugPageParams> }) {

// Determine the actual layout after we have the frontmatter
const layout = frontmatter.template || getLayoutFromSlug(slug)
const Layout = layoutMapping[layout]
const topicConfig = topics[layout]

// If the page has a published date, format it
if ("published" in frontmatter) {
Expand All @@ -79,6 +81,40 @@ export default async function Page(props: { params: Promise<SlugPageParams> }) {
const requiredNamespaces = getRequiredNamespacesForPage(slug, layout)
const messages = pick(allMessages, requiredNamespaces)

if (topicConfig) {
const afterContent =
layout === "staking" ? (
<StakingCommunityCallout className="my-16" />
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sidecomment, we should probably need to think about a global callout component (in base layout perhaps) to be used for any page that needs something like this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree -- can separate this task

) : undefined

return (
<>
<SlugJsonLD
locale={locale}
slug={slug}
frontmatter={frontmatter}
contributors={contributors}
/>
<I18nProvider locale={locale} messages={messages}>
<TopicLayout
slug={slug}
frontmatter={frontmatter}
tocItems={tocItems}
lastEditLocaleTimestamp={lastEditLocaleTimestamp}
contentNotTranslated={!isTranslated}
contributors={contributors}
config={topicConfig}
>
{content}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of adding a new prop afterContent we could just do

Suggested change
{content}
{content}
{afterContent}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree -- patched

{afterContent}
</TopicLayout>
</I18nProvider>
</>
)
}

const Layout = layoutMapping[layout]

return (
<>
<SlugJsonLD
Expand Down
8 changes: 4 additions & 4 deletions public/content/ai-agents/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ metaTitle: AI agents | AI agents on Ethereum
description: An overview of AI agents on Ethereum
lang: en
template: use-cases
emoji: ":robot:"
sidebarDepth: 2
image: /images/ai-agents/hero-image.png
alt: People gathered at terminal table
summaryPoint1: AI that interacts with blockchain and trades independently
summaryPoint2: Controls onchain wallets and funds
summaryPoint3: Hires humans or other agents for work
summaryPoints:
- "AI that interacts with blockchain and trades independently"
- "Controls onchain wallets and funds"
- "Hires humans or other agents for work"
buttons:
- content: What are AI agents?
toId: what-are-ai-agents
Expand Down
8 changes: 4 additions & 4 deletions public/content/dao/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ metaTitle: What is a DAO? | Decentralized Autonomous Organization
description: An overview of DAOs on Ethereum
lang: en
template: use-cases
emoji: ":handshake:"
sidebarDepth: 2
image: /images/use-cases/dao-2.png
alt: A representation of a DAO voting on a proposal.
summaryPoint1: Member-owned communities without centralized leadership.
summaryPoint2: A safe way to collaborate with internet strangers.
summaryPoint3: A safe place to commit funds to a specific cause.
summaryPoints:
- "Member-owned communities without centralized leadership."
- "A safe way to collaborate with internet strangers."
- "A safe place to commit funds to a specific cause."
---

## What are DAOs? {#what-are-daos}
Expand Down
8 changes: 4 additions & 4 deletions public/content/decentralized-identity/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ title: Decentralized identity
description: What is decentralized identity, and why does it matter?
lang: en
template: use-cases
emoji: ":id:"
sidebarDepth: 2
image: /images/eth-gif-cat.png
summaryPoint1: Traditional identity systems have centralized the issuance, maintenance and control of your identifiers.
summaryPoint2: Decentralized identity removes reliance on centralized third parties.
summaryPoint3: Thanks to crypto, users now have the tools to issue, hold and control their own identifiers and attestations once again.
summaryPoints:
- "Traditional identity systems have centralized the issuance, maintenance and control of your identifiers."
- "Decentralized identity removes reliance on centralized third parties."
- "Thanks to crypto, users now have the tools to issue, hold and control their own identifiers and attestations once again."
---

Identity underpins virtually every aspect of your life today. Using online services, opening a bank account, voting in elections, buying property, securing employment—all of these things require proving your identity.
Expand Down
8 changes: 4 additions & 4 deletions public/content/defi/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ metaTitle: What is DeFi? | Benefits and Use of Decentralised Finance
description: An overview of DeFi on Ethereum
lang: en
template: use-cases
emoji: ":money_with_wings:"
image: /images/use-cases/defi.png
alt: An Eth logo made of lego bricks.
sidebarDepth: 2
summaryPoint1: A global, open alternative to the current financial system.
summaryPoint2: Products that let you borrow, save, invest, trade, and more.
summaryPoint3: Based on open-source technology that anyone can program with.
summaryPoints:
- "A global, open alternative to the current financial system."
- "Products that let you borrow, save, invest, trade, and more."
- "Based on open-source technology that anyone can program with."
---

DeFi is an open and global financial system built for the internet age – an alternative to a system that's opaque, tightly controlled, and held together by decades-old infrastructure and processes. It gives you control and visibility over your money. It gives you exposure to global markets and alternatives to your local currency or banking options. DeFi products open up financial services to anyone with an internet connection and they're largely owned and maintained by their users. So far, tens of billions of dollars worth of crypto has flowed through DeFi applications and it's growing every day.
Expand Down
8 changes: 4 additions & 4 deletions public/content/desci/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ title: Decentralized science (DeSci)
description: An overview of decentralized science on Ethereum
lang: en
template: use-cases
emoji: ":microscope:"
sidebarDepth: 2
image: /images/future_transparent.png
alt: ""
summaryPoint1: A global, open alternative to the current scientific system.
summaryPoint2: Technology that enables scientists to raise funding, run experiments, share data, distribute insights, and more.
summaryPoint3: Builds on the open science movement.
summaryPoints:
- "A global, open alternative to the current scientific system."
- "Technology that enables scientists to raise funding, run experiments, share data, distribute insights, and more."
- "Builds on the open science movement."
---

## What is decentralized science (DeSci)? {#what-is-desci}
Expand Down
7 changes: 4 additions & 3 deletions public/content/gaming/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ lang: en
template: use-cases
image: /images/robot-help-bar.png
sidebarDepth: 2
summaryPoint1: Game rules and state can be enforced by the Ethereum blockchain, not a studio's servers, representing a key benefit of onchain games
summaryPoint2: Anyone can build mods, bots, or entirely new games that plug into the same open onchain data
summaryPoint3: Purpose-built L2s enable real-time gameplay with lower fees, while game development frameworks make building onchain games more accessible than ever
summaryPoints:
- "Game rules and state can be enforced by the Ethereum blockchain, not a studio's servers, representing a key benefit of onchain games"
- "Anyone can build mods, bots, or entirely new games that plug into the same open onchain data"
- "Purpose-built L2s enable real-time gameplay with lower fees, while game development frameworks make building onchain games more accessible than ever"
buttons:
- content: Learn more
toId: gaming-on-ethereum
Expand Down
8 changes: 4 additions & 4 deletions public/content/nft/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ metaTitle: What are NFTs? | Benefits and use
description: An overview of NFTs on Ethereum
lang: en
template: use-cases
emoji: ":frame_with_picture:"
sidebarDepth: 2
image: /images/infrastructure_transparent.png
alt: An Eth logo being displayed via hologram.
summaryPoint1: A way to represent anything unique as an Ethereum-based asset.
summaryPoint2: NFTs are giving more power to content creators than ever before.
summaryPoint3: Powered by smart contracts on the Ethereum blockchain.
summaryPoints:
- "A way to represent anything unique as an Ethereum-based asset."
- "NFTs are giving more power to content creators than ever before."
- "Powered by smart contracts on the Ethereum blockchain."
---

## What are NFTs? {#what-are-nfts}
Expand Down
Loading
Loading