Read one card, then the next
Ravnary is a library where every page holds one short, checked idea. You read a card, then the next, and a quiz is there whenever you want to test yourself. You don't need an account, and there's nothing to sign in to.
The name comes from Odin's ravens. Huginn carries the thought out and Muninn brings it back, which is the loop the site is built around. It's a Next.js app on AWS, and most of the design work went into two things, publishing content atomically and keeping answers and reader data away from the browser.
Open the library →
The learning loop
Every topic is built for the same five steps. The reader decides when to switch from reading to answering, and the quiz is always optional.
- Learn a concept
- See an example or a gotcha
- Answer a quiz
- Read the explanation
- Continue or review
Eight kinds of card
Concept, example, walkthrough, comparison, gotcha, code, exercise, and recap. They're declared once, in reading order, and the import schema refuses any type it doesn't know before anything is written.
Seven kinds of question
Multiple choice, true or false, multiple select, ordered steps, fill in the blank, numeric, and matching. A quiz is its own model, separate from cards, and every answer comes with an explanation.
Static pages, one dynamic route, two tables
Content changes only when I import it, so almost everything is prerendered. The server does three jobs at request time. It serves the quiz, runs the server actions, and accepts analytics events. The CDK stack owns the tables and the roles.
Prerendered by default
Every content route is static. The build reads DynamoDB, so a content import asks Amplify to rebuild the site through an incoming webhook. Amplify Hosting doesn't support on-demand revalidation.
The quiz is the one exception
It draws a new seed and reshuffles its questions on every request. Prerendering it would freeze one question order for good, which would change the product.
Two tables that never join
Content is immutable between imports and has point-in-time recovery. Events are append-only, expire after 90 days, and have no backups, so the two live in separate tables.
Three fixed levels and a graph beside them
A subject owns its topics and a topic owns its cards and questions. The depth never changes, so no cycle can form and the URL spells the hierarchy as /learn/subject/topic/card. Paths, categories, and links between cards cross the hierarchy, so they're stored beside it.
A path is an order over existing places
A path puts subjects and topics in an order across the library. The ?path= parameter only records where the reader came from and never changes what a page contains. The browser carries the journey, so every page stays prerendered.
Categories are a graph, not a tree
A card tagged react shows up under Web Development and under Technology without moving. The importer precomputes every ancestor, so the database never walks the graph while someone waits.



Publishing a whole generation at once
An import builds a complete new generation of the read model next to the live one and flips a single pointer last. Readers see the old library until the flip and the new one after it.
Key design
Every page read is a GetItem or a prefix Query
The importer resolves joins and counts ahead of time, so the table needs no secondary index. A slug lookup is one GetItem. The whole category graph is a single 41 KB item.
CONTENT POINTER
CANONICAL TOPIC#id
Vn#SLUG TOPIC#subject#topic
Vn#TOPIC#id CARD#id
Vn#QUESTION#id PROMPTSafe to interrupt, safe to race
An import that dies halfway leaves an unreferenced generation and an unchanged site, and running it again repairs it. Two imports can't both win, because each one reserves its generation number with an atomic counter first.
Visibility is decided once
Drafts live only in the canonical records. The read model holds just what's published and reachable, so no page filters on status and the previous generation stays around for a rollback.
Cards that read without JavaScript


A card is a list of blocks
Markdown, diagrams, callouts, code, tables, steps, tabs, and reveals are each a typed block. Steps, tabs, and reveals can't nest, so every step and tab has one address, like #step-2. Raw HTML is never rendered.
No loading boundary anywhere
A loading boundary would stream the response. A missing card would then answer 200 instead of 404, and with JavaScript off a card would show only its skeleton. The skeleton is drawn in the browser instead, so the server always sends complete HTML.
The answer stays on the server
The quiz page reads prompts, which are questions with the correct options, accepted answers, pairings, and explanations taken out. Only the grading step reads the record that holds the answer, and the verdict comes back after you submit.
Options are shuffled with a seed derived from the request and the question id. Every evaluator matches on option id, so the order a reader sees never affects the grade. A numeric answer is parsed on the server, and a matching question sends both sides without the pairing.
The link to the card that teaches the question arrives with the verdict, because a card title can give the answer away.

Analytics that cannot name a reader
Ravnary wants to know which questions are too hard or badly written. It doesn't want to know who answered them. Raw events stay private and expire after 90 days, and only aggregates ever reach a public page.
Fields come from an allowlist
The event schema is a strict object, so email, IP address, user agent, and a client-sent correct flag are refused simply because the schema doesn't define them. The server stamps the id and the time.
The threshold applies at write time
Below 20 distinct sessions, aggregation produces no row at all. The raw figure never exists anywhere a later reader could find it, and a session is only a browser tab.
IAM draws the same line
The web role can only append to raw event partitions and can only read published ones. Even a bug in the site couldn't read another reader's events back.

Once a question clears the threshold, the page reports its accuracy, the wrong option readers pick most, and a discrimination index. A negative index means stronger readers get the question wrong. That points to a miskeyed answer, ambiguous wording, or a question about something the cards never taught.
Your progress stays in your browser
With no accounts, there's nowhere on the server to keep your progress. Reading progress, favourites, your last place, and the review schedule live in versioned browser storage keys.
They hold ids and scheduling state, never answers or card text. The site discards any stored value in a shape it doesn't recognize, and it reads an older version once before removing it.
The outcome
A library that prerenders its pages and grades every answer on the server.
Each rebuild renders the site from one published generation. Nobody needs an account, progress stays in the browser, and the reader data the site keeps is anonymous, expires after 90 days, and goes public only in aggregate.
Visit ravnary.com