An interactive block shows its full content at build time. The script adds immediate feedback, never information.
Content is composed, not written
12 blocks shared between the blog and the courses, 7 lesson blocks reserved to lessons. Every card gives the sentence that chooses it, the real rendering and the code to copy. The renderings below are the components themselves, not images.
A lesson imports from learn/blocks, an article from content/blocks. An author never looks for a path.
Renaming the id of a Quiz or a Checkpoint erases the learner’s answer. It is named after what it asks.
Stats refuses a figure without a source, Quiz refuses zero or two correct answers, Payload refuses a note outside the code.
Seven blocks that make a lesson
Reserved to courses: they know where the learner is. The arc of a lesson places them in this order, and only Objective and Recap are mandatory, because they give the lesson a start and an end.
- Objective
- prose + shared blocks
- Payload
- TryIt
- Troubleshoot
- Quiz · Checkpoint
- Recap
Objective
Opens the lesson: what the learner will be able to DO, how long it takes, what to have at hand.
Nota list of topics covered. An outcome starts with an action verb and can be checked.
- Add a BlitzAPI call to any Clay table with the HTTP API enrichment
- Set the rate limit that keeps a bulk run from failing halfway
You needA Clay account · Your BlitzAPI key
MDX
<Objective
outcomes={[
'Add a BlitzAPI call to any Clay table',
'Set the rate limit that keeps a bulk run alive',
]}
needs={['A Clay account', 'Your BlitzAPI key']}
time="3 min"
/>Payload
The code the learner will copy, with the lines to adapt annotated one by one.
Nota plain code block when nothing has to change. Three notes on fifteen lines is the right order of magnitude.
{
"person_linkedin_url": "https://www.linkedin.com/in/antoine-blitz-5581b7373"
1
}
- The only field of the request body: the LinkedIn profile URL. Send the x-api-key header with it.
MDX
<Payload
label="POST https://api.blitz-api.ai/v2/enrichment/person"
lang="json"
code={`{
"person_linkedin_url": "https://www.linkedin.com/in/antoine-blitz-5581b7373"
}`}
notes={[
{ line: 2, text: 'The only field of the request body: the LinkedIn profile URL.' },
]}
/>TryIt
Interrupts the reading to demand a gesture, then gives the criterion that says whether it worked.
Notforgetting the expected slot. An exercise without a success criterion invites silent failure.
Fire one row and read the response
/v2/enrichment/person and run it on a single row before touching the rest.found: true and a person object, and the run counter shows one result consumed.Show the solution
MDX
<TryIt task="Fire one row and read the response" minutes={2}>
The instruction, in markdown.
<span slot="expected">What should appear on screen.</span>
<span slot="solution">The way out, folded.</span>
</TryIt>Quiz
saves stateChecks ONE thing right after it was explained. Every option carries its why, the wrong ones included.
Notan absurd distractor. A plausible wrong answer is what teaches the most.
Show the answer key
- You ran out of creditsRunning out of credits does not return 429. It stops the run with a 402 Payment Required.
- Clay fired requests faster than your plan allowsThat is what 429 means. The first rows go through because the burst has not yet exceeded the window.
MDX
<Quiz
id="rate-limit"
question="Your first 200 rows go through, then 429. What happened?"
options={[
{ text: 'You ran out of credits', why: 'Running out of credits does not return 429.' },
{ text: 'Too many requests per second', correct: true, why: 'That is what 429 means.' },
]}
/>Checkpoint
saves stateThe gate before moving on: checkboxes that remember the learner.
Notconfusing it with Checklist, which is a list to READ, static and without memory.
Before you build the payload
All clear. Next section is safe to start.
MDX
<Checkpoint
id="enrichment-ready"
title="Before you build the payload"
items={[
'The method is POST and the URL points at a BlitzAPI endpoint',
'One test row returned JSON, not an error',
]}
/>Troubleshoot
The exact text the learner sees on screen, its cause, and the gesture that fixes it.
Notwriting the diagnosis in the symptom column. The learner searches for « 429 », not for « rate problem ».
When it does not work
429 Too Many Requests
CauseNo client-side rate limit on this enrichment, so Clay fires faster than your plan allows.
FixSet Max Requests to your plan rate, then re-run only the failed rows.
401 Unauthorized
CauseThe key is missing, misspelled, or pasted into the wrong header name.
FixCheck that the header is exactly x-api-key, lowercase, with no trailing space.
MDX
<Troubleshoot
items={[
{
symptom: '429 Too Many Requests',
cause: 'No client-side rate limit on this enrichment.',
fix: 'Set Max Requests, then re-run the failed rows.',
},
]}
/>Recap
Closes the lesson: what the learner did, in the past tense, then the reason to continue.
Notdescribing what the lesson covered. « You set the limit » can be checked, « the limit was presented » cannot.
What you just did
- You added a BlitzAPI call to a Clay table with the HTTP API enrichment
- You can tell a 429 apart from an auth error and from an empty input
The plumbing holds. Next you fill it: a real payload that turns a filter into your first page of accounts.
MDX
<Recap
points={[
'You added a BlitzAPI call to a Clay table',
'You can tell a 429 apart from an auth error',
]}
next="The plumbing holds. Next you fill it."
/>A 740px column, and the blocks inside it
The blog routes are dormant in the September build and come back in October; the collection, the components and the three articles are in the code. The article template is a reading column: header, takeaways, head figure, prose with the shared blocks, FAQ, then three cards.
- 01Header
category label, date, reading time, level (meta.ts), title, description
- 02KeyTakeaways
takeaways[] from the frontmatter, 3 to 5, also emitted as structured data
- 03Figure
the head visual: motion/specs/blog/<slug>.json, or a signature
- 04Prose column
740px, about 67 characters per line (prose.css): 18/28 body, section titles in Inter Display 28/36
- 05Shared blocks
Definition, Callout, Steps, Compare, Checklist, Endpoint, Stats, NextStep, Calculator, Diagnostic, inline in the MDX
- 06FAQ
faq[] from the frontmatter, FAQPage JSON-LD
- 07Keep reading
three BlogCard, latest posts, categories from CATEGORY_LABELS
KEYTAKEAWAYS · blog/KeyTakeaways.astro
PROSE · styles/prose.css
The metric hides in the denominator
Your reply rate is measured on the accounts you found. Discovery Rate measures the ones you did not, which is where most of the market lives. The column is 740 pixels wide, about 67 characters per line, body 18 on 28.
- Section titles are Inter Display 28/36, the alt heading of the system.
- Lists keep the same measure; code stays mono.
BLOGCARD · blog/BlogCard.astro · from the blog collection
Discovery Rate: The B2B GTM Metric Nobody Tracks
Your reply rate is measured on the accounts you found. Discovery Rate measures the ones you didn't, and how company-first targeting closes the gap.
The Three Stages of GTM Targeting Maturity Explained
Ad hoc lists, structured TAM, signal-driven targeting: a practical maturity model for GTM teams, with a self-diagnostic and steps to reach the next stage.
Waterfall ICP: Find the Right Decision-Maker Every Time
A practical guide to Waterfall ICP: build a prioritized cascade, read icp, ranking and what_matched, and route every matched contact to the right rep.
Cards link to #blog here: the /blog routes do not exist in the September build, and the audit refuses any link to them.
MDX · frontmatter contract
---
title: "Discovery Rate: The B2B GTM Metric Nobody Tracks"
description: "140 to 160 characters"
category: data-strategy # playbooks | product | data-strategy
publishedAt: 2026-08-08
readingMinutes: 8
level: beginner # beginner | intermediate | advanced
signature: scan # head visual, or scene: true with specs/blog/<slug>.json
takeaways: # 3 to 5, enforced by the schema
- "…"
faq:
- { q: "…", a: "…" }
tags: ["Discovery Rate", "TAM"]
---
import { Definition, Callout, Stats } from '../../components/content/blocks';An LMS without a login, built from the same column
Catalog, course landing, lesson page. The lesson page is the article column plus the seven lesson blocks and two pieces of chrome; progress lives in localStorage. Routes are dormant in September; the components and content are in the code.
- 01LessonChrome
reading bar, remaining time, keyboard arrows, resume where you left
- 02LessonSidebar
course plan, current lesson highlighted, ✓ on finished lessons, course progress bar
- 03Objective
the entry contract: outcomes, needs, time
- 04Prose + shared blocks
the same column as an article, the same 12 blocks
- 05Payload → TryIt → Troubleshoot
the gesture: copy, run, fix
- 06Quiz · Checkpoint
the stop: one question, one gate; state saved without a login (ProgressScript, localStorage)
- 07Recap
the exit: what was done, the thread to the next lesson
COURSECOVER · learn/CourseCover.astro · generative, image-free
LESSON PAGE · the pieces not rendered here
- CourseCard builds its own
/learn/courses/<slug>link and CurriculumList its lesson links: rendering them here would put dormant routes on the page, which the audit refuses. - LessonChrome and LessonSidebar read the headings and the progress of a real lesson; they have no standalone state.
- LeadMagnet posts to the LEADS endpoint of config/site.ts, still empty: the form would have nowhere to go.
The full process, written to be delegated to an AI, is in docs/04-contenu/ecrire-une-lecon.md and creer-un-cours.md.
MDX · a lesson, in order
import { Objective, Payload, TryIt, Troubleshoot, Quiz, Checkpoint, Recap, Callout } from '../../components/learn/blocks';
<Objective outcomes={['…']} needs={['…']} time="3 min" />
Prose, with the shared blocks where they help.
<Payload label="POST https://api.blitz-api.ai/v2/…" lang="json" code={`…`} notes={[{ line: 2, text: '…' }]} />
<TryIt task="…" minutes={2}>…<span slot="expected">…</span></TryIt>
<Troubleshoot items={[{ symptom: '429 Too Many Requests', cause: '…', fix: '…' }]} />
<Quiz id="rate-limit" question="…" options={[…]} />
<Checkpoint id="enrichment-ready" title="…" items={['…']} />
<Recap points={['…']} next="…" />