Thanks for the interest. This document covers the lesson-authoring style that keeps RethLab cohesive, plus the practical mechanics of contributing.
Every lesson — especially Advanced and Expert — should ground its concept in real, currently-shipping source code.
1. Real source excerpt — verbatim, with a GitHub deep-link
2. Line-by-line walkthrough — what each part actually does
3. Design intent — why it's written this way (perf, safety, modularity)
4. Drill — a concrete exercise the learner can do in 15–30 minutes
Bad: "The EVM has a stack."
Good: Show the real Stack struct from revm/crates/interpreter/src/interpreter/stack.rs, link it, walk through STACK_LIMIT = 1024 and the popn<const N: usize>() const-generic optimization, and ask the learner to grep for the underflow check in push.
- Beginner / Fundamentals: keep concept-first; cite real code only where it directly demystifies a Rust idiom (e.g., showing
sign_message.rsfromalloy-rs/examples). - Advanced: every lesson should anchor on at least one real source excerpt.
- Expert: every lesson should anchor on multiple real source excerpts and show how they fit together.
Lessons reference main branch of upstream repos. This is intentional — we want learners to see current code, not a fossilized snapshot. The trade-off is that excerpts may go stale; please flag drift in PRs.
If a lesson cites code that has been refactored upstream, update the lesson with the new code rather than reverting to the stale version.
Use a fenced ```rust block, paste the source verbatim. Do not paraphrase. If you trim for length, mark with // ... and explain what was removed in the walkthrough.
Add a sentence above the block citing the source:
From [`crates/interpreter/src/instructions/arithmetic.rs`](https://github.com/bluealloy/revm/blob/main/crates/interpreter/src/instructions/arithmetic.rs):
For architectural concepts that don't translate to prose alone — pipelines, sequence-of-calls, dependency trees, state machines — use a fenced ```mermaid block. The lesson renderer wires these to a Mermaid component with a dark theme matching the site palette.
Guidelines:
- One diagram per concept, placed adjacent to the prose explaining it.
- Avoid
{and}inside node labels — they're decision-node syntax in Mermaid and break parsing. If you must show code shapes, writesol! macrorather thansol! { ... }. - Wrap labels with special characters in double quotes:
Types[".with_types EthereumNode"]. - Prefer
flowchart LRfor pipelines (HeaderStage → BodyStage → ...),sequenceDiagramfor call flows (Opcode → Interpreter → Database trait),graph TDfor trees / dependency hierarchies.
Don't add diagrams to Rust-syntax lessons (ownership, lifetimes, async) or pure source walkthroughs — the code itself is the explanation.
For lessons where there's a strong public talk by the maintainer or a Paradigm/Frontiers/Devcon speaker, append a ## 📺 Further watching (EN) / ## 📺 関連動画 (JA) section at the end of the lesson body and use a youtube fenced block:
\```youtube
<video_id>[ | <optional title>][ @<start_seconds>]
\```
The lesson renderer converts the block into a lazy-loaded thumbnail; the actual iframe only mounts after the user clicks play, so a lesson with three embeds still costs zero requests to Google on first paint.
Guidelines:
- One video per concept, ideally the most current talk by the actual maintainer (e.g., Rakita for Revm, Georgios for Reth direction). Multiple videos are fine when they cover different angles (architecture intro + perf deep-dive).
- Place at the end of the lesson, never above the source-walking content. The course is source-first; videos are supplementary.
- Skip when there's no clean fit. A weak generic tutorial dilutes the section — better empty than diluted.
- Embed-disabled videos show a "Video unavailable" iframe; if you spot one in the wild, swap to an alternative or remove the block.
Each lesson exists in EN and JA. When you change one, change the other in the same PR. The two should explain the same code with the same depth — translation is not literal; idiomatic Japanese is preferred over word-for-word English transliteration.
Two workflows depending on the course's pedagogical format.
Lesson content lives in prisma/seed-reth-{tier}-{lang}.ts. Each lesson is a Prisma lesson.create entry whose content field is a Markdown string — edited inline in the seed file.
{
title: 'Reading the interpreter',
slug: 'revm-interpreter-en',
type: 'CONTENT', // CONTENT | CHALLENGE | QUIZ
sortOrder: 0,
duration: 15, // minutes
xpReward: 30,
content: `# Reading the interpreter
[Markdown body — supports tables, code blocks, links]
`,
}After editing, run npm run seed:upsert (preserves user data) or npx prisma db seed (full reseed).
The openhl courses (consensus, clob, precompiles, funding, liquidation, adl) use a different pattern. Lesson bodies live in markdown drafts under drafts/, and the seed files were originally auto-generated by builder scripts under .github/scripts/build-openhl-*-seed.ts. Today the seed files in prisma/seed-reth-openhl-*-{en,ja}.ts are the source of truth — edit them directly; the drafts and builders are kept as historical scaffolding. To edit:
- Edit the lesson body inline in
prisma/seed-reth-openhl-{module}-{en,ja}.ts(content: \...``). Same shape as the Reth stack seeds — just a Markdown template literal. npm run seed:upsertto apply.
Historical workflow (kept for reference): the openhl seeds were originally generated from drafts/openhl_*_{en,ja}.md via .github/scripts/build-openhl-*-seed.ts. Each builder used draftFile, h1Marker, and startSignature to pick the right fenced block out of a draft. If you ever need to re-run a builder (e.g. to bulk-import a refreshed draft), the scripts still work and emit the same seed file shape — but day-to-day edits go straight into the seed, since that's the source of truth the upsert pipeline reads.
Build-along lessons follow a 3-part Goal section convention introduced after extensive UX review:
## Goal
Concepts you'll grasp in this lesson:
- **[Concept 1]** — 1-line explanation
- **[Concept 2]** — 1-line explanation
Verification:
```bash
cargo test -p openhl-funding…passes 5 tests (4 from L4 + 1 new proptest).
Specific changes:
src/types.rs— addsMarkPrice,IndexPrice, ...
The Goal opens with **semantic learning outcomes**, not mechanical verification — the cargo command moves to a "Verification" subsection. This split was applied across all 4 openhl courses; new build-along lessons should follow the same structure.
### Slugs
`slug` follows `kebab-case-{en,ja}`. **Once a lesson is in production, do not change its slug** — lesson URLs are keyed on slug (`/courses/<course-slug>/lessons/<lesson-slug>`), so renaming breaks every external link, bookmark, and stored localStorage completion record. The CUID `id` regenerates on every full reseed; the slug is the only stable identifier.
### Sort order
`sortOrder` is per-module and starts at 0. When inserting in the middle, you must bump subsequent lessons' `sortOrder` to maintain the sequence.
### Lesson types
- `CONTENT` — Markdown body, optional code samples and Mermaid diagrams
- `CHALLENGE` — Markdown + `starterCode` + `solutionCode` + `hints`. Renders the Monaco editor.
- `QUIZ` — Markdown intro + `quizQuestions` (array of `{question, options, correctIndex, explanation}`). Multiple-choice; passes at 70% correct.
`xpReward` and `duration` exist on lesson rows for legacy reasons but the UI no longer surfaces XP. Set them to anything reasonable (15–30 minutes for `duration`, 20–40 for `xpReward`); they don't affect what learners see.
---
## Running locally
```bash
npm install
cp .env.example .env # fill in DATABASE_URL, AUTH_SECRET, OAuth secrets
npx prisma db push
npx prisma db seed
npm run dev
URL: http://localhost:3000/rethlab
npx prisma db seed # full reset, drops user dataTo preserve user data while iterating on content:
curl -X POST "http://localhost:3000/rethlab/api/admin/seed?key=$AUTH_SECRET&mode=add"- Lesson follows the four-part structure (where applicable)
- EN and JA versions both updated
- Real-code excerpts have GitHub deep-links
-
slugnot renamed (or, if renamed, justified in PR description) -
sortOrderis consistent within the module -
npm run lintpasses -
npm run type-checkpasses -
npm testpasses (prisma/,src/lib/services/) - Manual test: re-seeded database renders the lesson correctly in the browser
Helpful issues include:
- Stale code excerpt:
lesson <slug>shows an old version of<file>(link the upstream commit that diverged) - Translation gap: EN and JA say different things about the same code
- Broken link: a GitHub deep-link 404s after upstream restructured
Less helpful:
- "I don't understand X" without specifying which lesson and which paragraph
- Feature requests without a use case
If you're adding a new specialization (e.g., "Parallel EVM" or "Hyperliquid Internals"):
- Pick lessons that map to one real codebase or whitepaper each. Don't write content unanchored to a primary source.
- Stay at 8–10 lessons per course for source-reading tiers, or 10–15 lessons for build-along projects.
- Decide the format up front:
- Source-reading: inline markdown in
prisma/seed-reth-<tier>-{en,ja}.ts. Best for "read this code, understand it" content. - Build-along project: markdown drafts in
drafts/<project>_l<N>_{en,ja}.md, builder script under.github/scripts/build-<project>-seed.ts, byte-identical reference implementation pinned per lesson via a SHA. Best for "build this from scratch" content.
- Source-reading: inline markdown in
- Update
prisma/seed.tsandprisma/seed-upsert.tsto register your new seeder. - Update
src/app/api/admin/seed/route.tsto include it. - If the new tier is a project category (like DIY Perp) rather than a difficulty tier, add a track key to
src/lib/i18n/{en,ja}.ts(courses.categories.<yourTrack>andpage.tracks.<yourTrack>Desc) and add a card tosrc/app/page.tsx.
Open an RFC issue first if the new tier touches more than ~15 lessons — coordination saves rework.
Note: any new tier should also be free. RethLab does not gate content behind payment or sign-in.