Design Wikipedia
The reading and editing experience of an online encyclopedia that serves billions of page views a month, mostly to anonymous readers, many of them on slow networks and cheap phones. The hard parts are serving almost every read from an edge cache and purging it on edit, keeping the article page light enough for a 2G connection, a reading layout that works in hundreds of languages and both writing directions, and an editor that catches two people changing the same article without real-time collaboration.
The question
Design the web front end of Wikipedia. People read articles, search for them, switch to the same article in another language, and a small share of them edit. Most readers are not logged in, and a large share are on phones with slow or expensive data.
Walk me through how a page gets to the reader fast, what the article page looks like and why, how search suggestions work, and how editing works when two people change the same article at the same time.
Real-life examples: Wikipedia (Vector 2022 skin on desktop), Wikipedia mobile web (MobileFrontend), Fandom wikis, MDN Web Docs, Kiwix offline reader
Requirements: a reading site that also lets anyone edit
Wikipedia is two products sharing one URL. The first is a reading site: an article page that must load fast anywhere in the world, on any device, for people who never log in. The second is an editing tool: a text editor, a preview, a history of every change, and a way to undo bad ones. Readers outnumber editors by a huge margin, and that ratio drives almost every decision in this guide.
Before drawing anything, ask questions whose answers move the design. The table lists the ones that matter, the answer this guide assumes, and what changes if the interviewer answers differently.
| Question | Answer we assume | What it changes |
|---|---|---|
| Who reads, and are they logged in? | Billions of page views a month. The great majority are anonymous readers; only a small share of requests carry a login session. | Anonymous pages are the same for everyone, so they can be cached at the edge. Logged-in pages can't, so they need a different path. |
| Which devices and networks? | About two thirds of views come from phones. Many readers are on 2G or 3G, prepaid data, and phones with 1 to 2 GB of memory. | Page weight, JavaScript size, and lazy loading become first-class requirements, not polish. |
| How many languages? | More than 300 language editions, including right-to-left scripts such as Arabic, Hebrew, Persian, and Urdu. | Layout must flip by direction, fonts must cover many scripts, and the language switcher must handle hundreds of entries. |
| How often do articles change? | A popular article can change many times an hour during breaking news; most change rarely. | The cache must be purged on edit, and readers must see the new version within seconds, not hours. |
| How do people edit? | Two editors: a visual one that looks like the page, and a source one for wiki markup. One person edits at a time; there is no live co-editing. | We need conflict detection on save, not real-time collaboration. |
| Which features are in scope? | Reading, search, language switching, editing, history and diffs, and the basic tools patrollers use to revert vandalism. | Sets the chapters. Talk pages, uploads, and admin tools are out. |
Functional requirements
- Read an article: title, lead section, infobox, sections with headings, images, tables, references, and categories.
- Navigate inside a long article with a table of contents, and to other articles through links, with a preview on hover.
- Search with suggestions as you type, and see full search results on their own page.
- Switch to the same article in another language, in either reading direction.
- Edit an article in a visual editor or in wiki markup, with a preview, an edit summary, and protection against losing work.
- See the history of an article and the difference between any two versions.
- Let patrollers review recent changes and revert a bad edit in one or two clicks.
Non-functional requirements, with numbers
- Readable fast on slow networks: the lead text visible within about 3 seconds on a slow 3G connection (400 Kbps, 400 ms round trip, the numbers of WebPageTest's "3G Slow" profile) on a low-end phone. That is our budget, not a Wikimedia figure.
- Works without JavaScript: the article, links, search results page, and source editor must all work if scripts fail or never load. Scripts only improve the page.
- Cache friendly: an anonymous article view should almost never reach the application servers. We aim for well over 90% of requests answered by the edge cache.
- Fresh after edits: a saved edit is visible to anonymous readers within a few seconds.
- Accessible: WCAG 2.2 AA, keyboard-friendly, works with screen readers, readable text size and line length settings.
- Small JavaScript: our budget is under about 100 KB of compressed JavaScript for a reader on a first visit, loaded after the text is on screen.
Out of scope, and why
Say what you leave out so the interviewer can pull something back in. We leave out: the wiki markup parser itself (we treat it as a service that returns HTML), file uploads and the media repository, talk pages and discussion tools, user accounts and login, the native Android and iOS apps, and the server side of search ranking.
We use the database and the parser as black boxes and focus on the parts a frontend engineer owns: what HTML goes out, how it is cached, what runs in the browser, and how edits come back.
Check your understanding · Exercise 1
Which answers from the interviewer would change the design the most? Select all that apply.
What makes this hard
A Wikipedia article looks like the simplest page on the web: text, a few pictures, some links. The difficulty is not in any one screen. It comes from five traits that pull against each other, and every later chapter is an answer to one of them.
- Huge, skewed reads. Billions of views a month, and on any day a few thousand articles (the news, a death, a sports final) take a big share of them. If every view hit the application servers and the database, the site would need many times the hardware. The page must be served from a cache close to the reader.
- The same page for almost everyone. An anonymous reader in Lagos and one in Oslo get the same HTML for the same article and language. That is what makes edge caching possible, and it means we must keep anything personal out of the HTML: no username, no "you have new messages" badge baked into the page.
- Edits must show up quickly. A cache that serves an hour-old version during breaking news is a real problem: vandalism stays visible, and corrections don't land. So the cache can't just expire; it must be told to forget a page the moment it changes.
- Slow networks and cheap phones. On slow 3G, every 100 KB costs about 2 seconds, and a cheap phone can spend a second just parsing and running a few hundred KB of JavaScript. The text has to arrive and render before any script, and everything else has to earn its bytes.
- Anyone can edit, nobody locks. Editing is open, so two people can start from the same version and both press save. There is no live co-editing, so the system must detect the clash at save time, merge what it safely can, and show a clear choice when it can't. Bad edits must also be easy to spot and undo.
Add one more trait that touches everything: hundreds of languages. Text direction, fonts, word lengths, and even which script a reader wants (Serbian in Cyrillic or Latin, Chinese in simplified or traditional characters) vary by wiki. A layout tested only in English breaks somewhere.
Think first
A teammate proposes rebuilding Wikipedia as a React single-page app: one JavaScript bundle, article content fetched as JSON. What goes wrong for the reader on slow 3G?
The reader sees nothing useful until the HTML shell, the bundle, and then the JSON have all arrived and the bundle has run. That is at least three sequential downloads on a 400 ms round-trip network, plus parse time on a weak CPU. Text that the server could have sent in the first response now waits on a few hundred KB of script.
Search engines and link previews also get less from a page that needs scripts, and if the bundle fails to load, the page is blank. A server-rendered page with scripts as an extra layer avoids all three problems.
Serving reads: render once, cache at the edge, purge on edit
Start with the most important path: an anonymous reader opens /wiki/Mount_Everest. Wikimedia's public statistics count about 15 billion page views a month from people (not bots) across all its wikis, most of them on Wikipedia. Spread evenly that is about 6,000 views per second, and peaks during big news are several times higher.
Each view needs HTML that came from wiki markup (wikitext) through a parser, which expands templates, builds the infobox, numbers the references, and resolves hundreds of links. Parsing a long article can take seconds of server time.
The naive design renders the page on every request. At 6,000 requests per second and even 300 ms of server time each, that is about 1,800 CPU-seconds every second, before peaks, for pages that almost never change between two views. It also puts every reader in Jakarta or Lima a full ocean round trip away from the data center.
Decision How should the article HTML reach an anonymous reader?
What we pick: Pick server rendering with edge caching and purge on edit. Wikipedia's reads are the textbook case: the same bytes for everyone, read far more than written, and freshness defined by an event (an edit) rather than a clock. The ssr and cdn-caching lessons cover the general mechanics; here we focus on how this site uses them.
Two caches, two jobs
There are really two caches. The parser cache sits next to the application servers and stores the rendered article body per revision, so a cache miss at the edge doesn't mean re-parsing. Wikimedia keeps parser cache entries for 30 days.
The edge cache (the CDN, short for content delivery network) sits in data centers around the world and stores the complete HTTP response for a URL. Wikimedia runs its own CDN: Varnish as an in-memory frontend cache, and Apache Traffic Server (ATS) as a larger disk-backed cache behind it, in regional sites such as Amsterdam, Marseille, Singapore, San Francisco, and São Paulo.
Wikimedia's published settings are worth knowing because they show the design intent. MediaWiki gives page views a CDN lifetime of 14 days, but each edge layer caps its own copy at 1 day and then revalidates with the origin, which can answer 304 Not Modified cheaply. So freshness never depends on those timers. The timers only bound how long a non-edit change (a new skin version, a config change) takes to reach every page.
GET /wiki/Mount_Everest HTTP/2
Host: en.wikipedia.org
HTTP/2 200
content-type: text/html; charset=UTF-8
content-encoding: gzip
cache-control: private, s-maxage=0, max-age=0, must-revalidate, no-transform
vary: Accept-Encoding,X-Subdomain,Cookie,Authorization,User-Agent
age: 50117
x-cache: cp5024 miss, cp5024 hit/58
x-cache-status: hit-front
Notice the split. The browser is told max-age=0, must-revalidate, so a reader never sees a stale copy from their own disk cache after an edit. The CDN gets its long lifetime through its own configuration, and the private here keeps any other shared cache from storing the page.
The age header shows the copy has sat in the edge for about 14 hours, and the cache status says the in-memory frontend (Varnish) answered, for the 58th time. The vary list tells other caches the response depends on the device class (X-Subdomain, which the edge sets for phones), the session cookie, and login headers. Exact values change over time; the pattern of a short browser lifetime and a long, purgeable edge lifetime is the point.
Purging on edit
When an edit is saved, MediaWiki sends a purge for the page's URLs to every edge server. Until 2020 these went out as HTCP (a UDP protocol) over multicast, where a lost packet meant a lost purge.
Today purges go through Kafka, a durable message queue, to a small daemon called purged on each cache host, which turns each message into an HTTP PURGE request against its local Varnish and Traffic Server. A queue matters here: if a cache host is down or slow for a minute, its purges wait rather than vanish, so it can't come back serving the vandalised version.
- Template fan-out. An edit to a template such as the country infobox changes every page that uses it, possibly millions. Those purges go through a slow background job queue, so they can take minutes to hours. The parser cache handles it the other way: each entry records a page-touched timestamp, and an entry older than that timestamp is ignored.
- Purge before render. If the purge reaches the edge before the new revision is committed and readable, the next request could re-cache the old version. The save must finish, then purge.
- Lost purges. The 1-day edge cap is the safety net: even a purge that never arrives can't leave a page stale for longer than a day.
Logged-in readers take another path
A logged-in reader's page differs: their username, their notifications, their preferences, their watchlist star. A request that carries a MediaWiki session cookie can't be answered from the shared copy, so it goes to the application servers, and the personalised response is never stored at the edge. Those requests still benefit from the parser cache, so only the skin around the article is built per user. The linkedin guide covers the same split for logged-out pages: keep personal data out of the shared HTML, or the cache stops working for everyone.
Check your understanding · Exercise 2
A news event sends 50,000 views per second to one article while it is being edited every minute. Which statements are true for this design? Select all that apply.
Fast on slow networks: text first, everything else earns its bytes
Caching gets the bytes close to the reader. It doesn't make them fewer. Take our budget: lead text readable within about 3 seconds on slow 3G (400 Kbps, which is 50 KB per second, and 400 ms per round trip).
A new HTTPS connection needs about three round trips before the request can even go out: DNS, TCP, and TLS 1.3. That is 1.2 seconds gone. Add one more round trip for the request itself and the reader has 1.4 seconds left, enough for roughly 70 KB of data. Every extra file that blocks rendering costs at least another 400 ms.
So the rule is simple: the first response must be readable on its own. The HTML carries the article text, and the CSS needed to lay it out arrives in one small render-blocking stylesheet request. Wikimedia tested inlining critical CSS instead in 2021 and declined it: it helped first visits a little but slowed the far more common repeat views. Fonts, images, and every script must not stand between the reader and the text.
Decision How should JavaScript be delivered to readers on browsers of very different ages?
What we pick: Pick the feature test. It keeps one cached HTML per page and makes failure harmless. MediaWiki does exactly this with ResourceLoader: the basic layer is HTML and CSS for every browser, and the startup module's feature test (currently requiring ES2019 support) decides whether the modern JavaScript layer loads at all. There is no user agent list: Grade A browsers are the ones Wikimedia actively tests, Grade C are older ones that must still get a readable, working page, and any other browser simply gets whatever the test allows.
How the startup module works
The HTML starts with class="client-nojs" on the root element and an inline script that swaps it to client-js, so CSS can hide no-JS fallbacks before first paint. Then an async script loads the startup module. It holds the feature test and a manifest: every module name with a short version hash and its dependencies.
Because the manifest changes whenever any module changes, the startup response is cached for only 5 minutes, and a deployment reaches every reader within about 10 minutes. Every other module URL carries the version hash, so it can be cached for a long time and is reused across pages.
// startup.js (simplified). It must parse even in very old browsers,
// so it avoids modern syntax and tests for it inside a string.
function isCompatible() {
return "querySelector" in document &&
"localStorage" in window &&
"fromEntries" in Object && // an ES2019 API
(function () {
try {
// ES2018 async generators, ES2019 optional catch binding.
new Function("try { async function* x() {} } catch {}");
return true;
} catch (e) {
return false;
}
})();
}
if (!isCompatible()) {
// Undo the inline script's swap: the page stays plain HTML.
document.documentElement.className =
document.documentElement.className.replace("client-js", "client-nojs");
} else {
register(MANIFEST); // { name: [versionHash, deps] }
// Modules this page needs, listed by the server in RLPAGEMODULES.
load(window.RLPAGEMODULES || []);
}
Batching matters on high-latency networks: ten modules become one request like load.php?modules=a|b|c&version=x7q2. ResourceLoader also keeps loaded modules in localStorage keyed by version, so a repeat visit can skip the network for unchanged modules. Our budget for a first-time reader is under 100 KB of compressed JavaScript, all of it after the text is painted.
async, so it never delays the first paint, and the modules it loads come in one batched request.Cutting bytes on phones
Wikipedia serves phones a separate mobile view (the MobileFrontend extension with the Minerva skin) rather than the desktop page squeezed by CSS. From 2011 that view lived on an m. subdomain behind a redirect. In late 2025 Wikimedia moved it onto the normal URL: the edge reduces the user agent to a device class (the X-Subdomain header) and keeps two cached copies per page, mobile and desktop.
Dropping the redirect made mobile responses about 20% faster, and dropping the duplicate purges for mobile URLs halved the purge rate MediaWiki sends (a baseline of about 40,000 purges per second before the change). The mobile HTML is also lighter: navigation boxes at the bottom of articles are removed, and the lead paragraph is moved above the infobox, so the first screen shows the article's opening sentence, not a table. Three more techniques do most of the work:
- Lazy images. Older versions of MobileFrontend wrote a placeholder element with the image's URL and size in data attributes, and a script swapped in the real
imgnear the viewport, with anoscriptcopy for readers without scripts. Today the HTML carries ordinaryimgelements with the browser's nativeloading="lazy", so no script is needed. Thewidthandheightattributes still matter so the layout doesn't jump. - Collapsed sections. On mobile, sections after the lead start collapsed behind tappable headings. The text is in the HTML (so find-in-page and search engines still see it), but lazy images inside a collapsed section aren't fetched, because the browser only loads them when they are rendered near the viewport.
- Right-sized thumbnails. The image server makes thumbnails at a limited set of standard widths, and
srcsetlets the browser pick one for the screen density, so a phone doesn't download a 1,200-pixel image for a 300-pixel slot. The image-optimization lesson covers formats andsrcsetin depth.
Fonts are the last trap. A web font costs a request plus a download and can hide text until it arrives. Wikipedia's skins use the reader's system fonts for the interface and content. The multilingual chapter returns to fonts for scripts a phone may not have.
Check your understanding · Exercise 3
On slow 3G, which changes move the time to readable text the most for a long article? Select all that apply.
The article page: layout, navigation, and previews
Now the screen itself. On desktop, Wikipedia's default skin is Vector 2022, the default on English Wikipedia since January 2023. Its layout answers one question: how do you keep a 10,000-word article easy to read and easy to move around in? The wireframe names the parts.
- Header: logo, search box, and user links. When the article title scrolls out of view, a sticky header appears with the title, search, and the key page tools, so the reader never has to scroll back up to search or switch language.
- Table of contents: a sidebar that lists the sections and highlights the one being read. It can be pinned in the sidebar or tucked into a menu next to the title.
- Title bar: the title, the language button ("58 languages"), and tabs for Article, Talk, Read, Edit, and View history.
- Content: a limited reading width for comfortable line length, with the infobox floated to the side and references at the end.
- Appearance menu: text size, width, and color theme. The menu and a larger default text size reached logged-out readers in June 2024, and dark mode followed in July 2024.
A table of contents that follows the reader
The naive way to highlight the current section listens to scroll and calls getBoundingClientRect() on every heading. On a long article with 60 headings that forces layout up to 60 times per scroll event, which on a cheap phone means dropped frames. An IntersectionObserver lets the browser compute intersections as part of its normal rendering work, and calls us only when a heading crosses a line we choose.
const headings = [...document.querySelectorAll("h2[id], h3[id]")];
const links = new Map(
headings.map((h) => [h.id, document.querySelector(`#toc a[href="#${CSS.escape(h.id)}"]`)])
);
const started = new Set<string>(); // headings at or above the line
const observer = new IntersectionObserver(
(entries) => {
for (const e of entries) {
// Inside the top band, or already scrolled above it: this section has started.
if (e.isIntersecting || e.boundingClientRect.top < 0) started.add(e.target.id);
else started.delete(e.target.id); // back below the line after scrolling up
}
// The current section is the last started one, in document order.
setActive(headings.filter((h) => started.has(h.id)).at(-1)?.id);
},
{ rootMargin: "0px 0px -80% 0px" } // only the top 20% of the viewport counts
);
headings.forEach((h) => observer.observe(h));
function setActive(id: string | undefined) {
document.querySelector("#toc [aria-current]")?.removeAttribute("aria-current");
if (id) links.get(id)?.setAttribute("aria-current", "true");
}
Two details make it feel right. The TOC itself is plain HTML links in the cached page, so it works with JavaScript off; the script only adds highlighting and collapsing. And aria-current marks the active entry for screen readers as well as for styling. The same trick, an observer on the article title, decides when the sticky header appears.
Appearance settings without breaking the cache
Here is a puzzle: an anonymous reader picks dark mode and large text. The HTML is shared with everyone, so the server can't bake those choices in. Fetching the settings with JavaScript after load means a flash of white page and a jump in text size.
The answer is a tiny inline script at the top of the page. It reads a small client preference value (Wikimedia keeps it in a cookie for logged-out readers) and switches classes on the root element before the first paint, such as skin-theme-clientpref-night. The cached HTML stays identical, and the CSS does the rest. The edge ignores this cookie for cache lookup, which is safe precisely because the server never reads it to build the page.
The "automatic" theme (the class skin-theme-clientpref-os) maps to prefers-color-scheme. Dark mode on a wiki is harder than on most sites: thousands of templates hard-code colors such as a light grey infobox background. So the dark theme needs CSS variables for the interface, plus fixes by editors for content that templates paint themselves, which is why it reached logged-out readers wiki by wiki.
Page previews on hover
Hovering a link to another article shows a card with the first sentences and a thumbnail. This is the Page Previews feature, on by default for logged-out readers on every Wikipedia since 2018. Its data comes from a summary endpoint, /api/rest_v1/page/summary/{title}, which returns a small JSON object (title, plain and HTML extract, thumbnail, language, and direction). It is cached at the edge for up to 14 days and refreshed after edits, so a popular link's preview costs the origin almost nothing.
- Wait before fetching. A cursor crossing a paragraph touches many links. Page Previews starts the fetch only after the pointer has rested on a link for 150 ms, and shows the card about 500 ms after the hover began, even when the answer comes back sooner, so cards don't flicker.
- Cancel on leave. If the pointer leaves before the card shows, abort the request with an
AbortControllerand never show a stale card. - Keyboard too. Keyboard focus on a link triggers the same preview, and the card is marked up as a tooltip. Ours should also close with
Escape, as WCAG's rule for content on hover or focus asks. Touch devices skip previews: a tap means navigate. - Fail quietly. A failed or empty summary shows a generic card with a link, never an error dialog. Previews are a nice extra; reading continues.
abort() on the request, so a late answer can never pop up a card for a link the reader has left.Check your understanding · Exercise 4
Which choices keep the article page fast and cache-friendly? Select all that apply.
Search: suggestions as you type, results on the server
Search is the second way into an article, and on Wikipedia the most common goal is "take me to the article I already have in mind". So the box has two jobs: suggest titles while the reader types, and if they press Enter, either jump straight to an exact title match or show a results page. The autocomplete guide covers the component itself in depth (debounce, races, caching, the combobox pattern). Here we cover what is specific to this site.
The baseline works without JavaScript
The search box in the cached HTML is a plain form: form action="/w/index.php" with an input named search and a hidden title=Special:Search. Submitting it is a normal GET. The server checks for an exact title (or a redirect, such as "Everest" to "Mount Everest") and redirects there; otherwise it renders the full results page on the server. That page is the slow path and isn't cached for long, but it works on any browser.
Suggestions on top
When the modern layer loads, the box becomes a combobox. Vector 2022's search asks the MediaWiki REST API (/w/rest.php/v1/search/title) for title matches; the older action=opensearch API returns the same kind of list and still powers browser search integrations. Each suggestion carries a title, a short description, and a thumbnail, so "Mercury" can show "planet", "element", and "Roman god" apart.
GET /w/rest.php/v1/search/title?q=mount%20ev&limit=10
200 OK
{
"pages": [
{
"id": 42179,
"key": "Mount_Everest",
"title": "Mount Everest",
"description": "Earth's highest mountain",
"thumbnail": { "url": "//thumb.wikimedia.org/.../60px-Mt._Everest....jpg", "width": 60, "height": 40 }
}
]
}
- Debounce lightly. Titles are short and readers know what they want. Wait about 100 to 150 ms after the last keystroke; the debounce-throttle lesson explains the trade-off.
- Cancel and ignore stale answers. Abort the previous request when a new one starts, and drop any response whose query isn't the current text. On a 400 ms network, a slow answer for "mo" arriving after "mount ev" is common.
- Cache by prefix. Answers are the same for every reader of a wiki, so keep them in memory per normalised query. The REST response is also public and cacheable for a few hours (English Wikipedia sends
max-age=10800). Backspacing then costs nothing. - Small thumbnails. 60-pixel thumbnails, loaded lazily, keep ten suggestions to a few KB on top of the JSON.
- Keyboard and screen readers. Arrow keys move the active option with
aria-activedescendant, Enter on a suggestion navigates, and Enter on the plain text submits the form. The keyboard-navigation lesson covers the pattern.
The last row of the list is always "Search for pages containing ...", which submits the full search. That keeps the two jobs distinct: suggestions match titles, the results page matches article text. Behind the results page, Wikimedia runs CirrusSearch, its search extension built on an Elasticsearch-family cluster (Wikimedia has been moving it to OpenSearch). The frontend's only concerns there are the URL (so results can be shared and cached briefly) and a page that works without scripts.
Check your understanding · Exercise 5
A reader on slow 3G types "mount ev" quickly. Which behaviours are correct? Select all that apply.
Editing: two editors, loaded on demand, and conflicts caught at save
Only a small fraction of page views lead to an edit, so the first rule of the editor is that readers must never pay for it. The "Edit" tab in the cached HTML is a plain link to ?action=edit, which returns a server-rendered form with a textarea of wiki markup: the source editor, working with no JavaScript at all. Everything richer loads only after a click.
Decision Which editing surface should we offer?
What we pick: Offer both. That's what Wikipedia does: VisualEditor for those who want it, the wikitext editor for everyone else, and a toggle between them. VisualEditor depends on Parsoid, the parser that converts wikitext to HTML and back without changing untouched markup; switching editors mid-edit goes through Parsoid too. The rich-text-editor guide covers how a visual editor models a document; here we care about loading and saving.
Loading the editor only when needed
When the modern layer is present, a click on Edit is intercepted. The page doesn't navigate; instead three things start in parallel: the editor's modules, the article's editable HTML from Parsoid for the current revision, and any edit notices. A progress bar appears at the top of the article. A common refinement is to prefetch the modules when the pointer rests on the Edit tab, which saves a few hundred milliseconds without costing readers who never hover there.
The fetch of the article returns its revision id along with the content. That id, the base revision, is the most important piece of state in the editor, and everything about conflicts depends on it. The plain wikitext form carries it too, as hidden fields: wpEdittime (the base revision's timestamp) and editRevId (its id).
When two people edit the same article
Ana and Ben both open "Mount Everest" at revision 1205. Ana fixes the height in the infobox and saves: revision 1206. Ben, still editing, adds a sentence to the History section and presses save.
If the server simply stores Ben's full text, Ana's fix silently disappears. That is the lost update problem, and with breaking news an article can have several people editing at once.
Decision How should the system handle two edits from the same base revision?
What we pick: Detect at save and merge. It matches how Wikipedia edits work: rare overlaps, attributable revisions, no locks. MediaWiki does this with a line-based three-way merge, run by the GNU diff3 tool, in both the wikitext editor and VisualEditor. The google-docs guide covers the real-time alternative and why it costs so much more.
POST /w/api.php
action=edit&title=Mount_Everest&format=json
&text=...full wikitext...
&baserevid=1205
&starttimestamp=2026-10-20T09:00:11Z
&summary=Add 1953 expedition dates
&token=...csrf token...
200 OK
{ "error": { "code": "editconflict", "info": "Edit conflict." } }
The three-way merge compares lines. Ana changed the infobox lines; Ben changed a History line. Neither touched the other's lines, so the merge combines both and saves revision 1207 with no prompt. If both changed the same line, say both corrected the height to different values, the merge fails and Ben sees a conflict screen.
Because the merge works on lines and wikitext paragraphs are often one long line, two edits to different sentences of the same paragraph still conflict. Editing a single section reduces that risk: Ben's save only replaces his section.
The conflict screen matters as much as the detection. The classic MediaWiki view shows the current text in one box, your text in another, and a diff between them; you copy your changes across. Wikimedia Deutschland later built a paragraph-based, two-column view (the TwoColConflict extension) that lines up each clashing paragraph side by side and asks you to pick, and optionally edit, one version of each. It is the default on German, Arabic, and Persian Wikipedia and a beta feature elsewhere.
Either way the rule is the same: never throw the user's text away, and make it obvious which version is whose.
Never lose work: drafts, previews, summaries
- Local drafts. The wikitext editor's Edit Recovery feature saves the form to IndexedDB every 5 seconds and offers to restore it if the tab closes; drafts expire after 30 days. VisualEditor has its own auto-save mechanism. If the article changed since the draft was saved, the restore message says so, which sets up a conflict check on save.
- Preview. The source editor's preview sends the wikitext to the server to parse, because only the server knows every template. The visual editor shows the result as you type.
- Edit summary. A one-line explanation saved with the revision. It shows in history and in patrollers' feeds, so the save dialog asks for it.
- Server checks after save. Anti-abuse filters can warn or block a save, and some saves need a CAPTCHA. The client treats these as a state, not an error: show the message, keep the text, let the editor fix and resubmit.
1206, because Ben has now seen Ana's edit; if someone else saved in between, the same check runs again.Check your understanding · Exercise 6
Ana and Ben both start from revision 1205. Ana saves first. Which outcomes are correct when Ben saves? Select all that apply.
History and diffs: every version, any two compared
Every saved edit is a new revision, kept forever. A popular article has tens of thousands of them. The history page lists them newest first, and the diff page shows what changed between any two. Editors and patrollers live on these two pages, so they must be fast and must work without JavaScript like everything else.
The revision list
Each row shows the date (a link to that old version), the author, the size in bytes with the change ("+142" in green, "-3,005" in red, a quick vandalism signal), the edit summary, and tags such as "mobile edit" or "reverted". Two columns of radio buttons pick the old and new revision, and a "Compare selected revisions" button submits a plain GET form to ?diff=1207&oldid=1205.
Pagination uses a cursor, not page numbers. The "older 50" link carries an offset that is the timestamp of the last row shown, so the server reads the next 50 rows from an index instead of skipping thousands. New edits arriving at the top don't shift what "older" means, which page numbers would. The default page is 50 rows; links offer 20 to 500, and the URL accepts up to 5,000.
Decision Where should the diff between two revisions be computed?
What we pick: Compute on the server and cache it. Revisions never change, so a diff of two revision ids is immutable content, the easiest thing in the world to cache. Wikimedia uses a fast native diff engine (wikidiff2) for this. The code-review-diff guide covers rendering long diffs on the client, which matters when you can't send HTML.
The diff page offers two layouts: the classic two columns, old on the left and new on the right, and an inline view that suits narrow screens, with deletions and insertions marked in one column. Changed words are marked inside a changed line, so fixing "8,848 m" to "8,848.86 m" highlights just the number. Colour is never the only signal: removed and added text also carry markup and labels that screen readers announce, and the two sides have plus and minus markers.
Check your understanding · Exercise 7
Which choices fit the history and diff pages? Select all that apply.
Hundreds of languages, both directions, many scripts
Wikipedia isn't one site translated into many languages but more than 300 separate wikis, each with its own articles, written by its own community. "Mount Everest" in English and the Nepali article on the same mountain are different texts. That shapes three frontend problems: linking the same topic across languages, laying out right-to-left scripts, and showing scripts the reader's phone may lack. The translation-architecture and i18n-performance-seo lessons cover the general machinery.
The language switcher
Which articles are "the same topic" is stored centrally in Wikidata, as a list of site links per item. When a page is rendered, those links become the article's interlanguage links and go into the cached HTML as a plain list, so they work without scripts. A big article can have 250 or more of them.
A plain list of 250 language names is hard to use, so the Universal Language Selector (ULS) enhances it after load: a search field that matches language names written in any language ("German", "Deutsch", "Allemand"), and a short list of suggested languages first, based on the browser's navigator.languages, the reader's region, and languages they used before. Each entry is a link with lang and hreflang set, so screen readers pronounce "Deutsch" in German and search engines learn the relationships.
Right-to-left wikis
Arabic, Hebrew, Persian, and Urdu Wikipedia set dir="rtl" on the root element, and the whole layout mirrors: sidebar on the right, text aligned right, arrows reversed. ResourceLoader flips the skin's stylesheets automatically with a tool called CSSJanus, which swaps left and right, margins, paddings, and float directions when serving CSS to an RTL page. New CSS should prefer logical properties such as margin-inline-start, which flip without any tool. The rtl-layouts lesson covers both approaches.
Mixed direction is the subtle part. An Arabic article quoting an English title, or an English article citing a Hebrew book, mixes directions in one line. Wrap such runs in elements with their own dir or in bdi, so punctuation and numbers stay with the right words. The page previews card uses the dir and lang the summary endpoint returns for exactly this reason.
margin-inline-start and float: inline-end; older CSS is flipped by CSSJanus when served to an RTL page.Fonts and script variants
- Fonts. Most phones ship fonts for major scripts, but not for every script a wiki uses. ULS has a repository of web fonts for such scripts, but web fonts are off by default on Wikimedia wikis, so a font never adds weight for readers who don't need it; readers who do can turn them on in the language settings. Our design keeps system fonts by default and loads a script font only by choice, with
font-display: swapso text is never hidden. - Variants. Some wikis serve one article in several scripts: Chinese Wikipedia in simplified or traditional characters, Serbian in Cyrillic or Latin. The LanguageConverter stores one text and converts it at render time. A variant URL such as
/zh-hans/or/zh-tw/is its own cached copy, keyed by the URL alone. The plain/wiki/URL instead picks a variant from the reader'sAccept-Languageheader, so its response has to vary on that header, which splits the cache.
Check your understanding · Exercise 8
Which statements about the multilingual design are correct? Select all that apply.
Architecture, data model, and interface
Put the pieces together. The shape is unusual for a modern web app: there is no single client application. There is a server-rendered document, cached at the edge, and a set of small JavaScript features that attach to it. Some (TOC highlighting, previews, search suggestions) load for every modern browser; one (the visual editor) is a large app that loads only on demand.
| Part | Where it runs | Responsibility |
|---|---|---|
| Edge cache (Varnish, Traffic Server) | Regional data centers | Answers anonymous page views, summaries, and suggestions; receives purges; passes logged-in requests through. |
| Purge pipeline (Kafka, purged) | Between origin and edge | Delivers every purge to every cache server, durably, after each save. |
| MediaWiki app servers and parser cache | Origin | Builds pages from the stored revision and the cached parse; runs the edit API, history, diffs, and search pages. |
| Parsoid | Origin | Converts wikitext to HTML and back for the visual editor; since 2025 it is also replacing the legacy parser for page views, wiki by wiki. |
| Skin HTML and CSS (basic layer) | Browser, no scripts | Readable article, TOC links, language links, search form, source editor form. |
| Startup module and loader | Browser | Feature test, module manifest, batched loading, local module cache. |
| Reading features | Browser, modern layer | Sticky header, TOC tracking, appearance settings, previews, search suggestions, language search. |
| Editor (VisualEditor or wikitext tools) | Browser, on demand | Loads on Edit; holds the base revision, the draft, and the save state machine. |
Why are the boundaries here? The line between the cached document and the scripts is the line between "same for everyone" and "different per reader or per action". Anything that varies (theme, previews, a reader's suggestions, the editor) lives in the browser or behind an API that is cacheable by its own key. The line between reading features and the editor is a cost line: readers pay a few tens of KB for reading features and nothing for the editor until they click.
The data flow figure follows one round trip: a reader's request served from the edge, then an editor's save travelling back to the origin, creating a revision, and turning into purges that reach the same edge servers. The write path is short and careful; the read path is wide and cheap.
Data model: where each piece of state lives
Most of the reader's data never becomes JavaScript state: it is HTML. The client-side model is small and lives in a few distinct places, each chosen by how long the data lives and who shares it.
type PageMeta = { // inline config in the HTML (RLCONF)
title: string; pageId: number; revisionId: number;
contentLanguage: string; dir: "ltr" | "rtl"; variant?: string;
isAnon: boolean;
};
type Summary = { // previews; in-memory LRU per tab
title: string; extractHtml: string; lang: string; dir: "ltr" | "rtl";
thumbnail?: { url: string; width: number; height: number };
};
type ClientPrefs = { // cookie for anonymous readers
textSize: "small" | "standard" | "large";
width: "standard" | "wide";
theme: "os" | "day" | "night"; // "os" follows prefers-color-scheme
};
type EditSession = { // editor memory; draft mirrored to IndexedDB
title: string; baseRevisionId: number; startedAt: string;
section?: number; text: string; summary: string; minor: boolean;
status: "editing" | "previewing" | "saving" | "saved"
| "conflict" | "failed" | "warned";
};
| Data | Lives in | Why there |
|---|---|---|
| Article, TOC, language links | Cached HTML | Same for every anonymous reader; must work without scripts. |
| Page metadata (ids, language, direction) | Inline config in the HTML | Scripts need it immediately, without another request. |
| Preview summaries, suggestions | In-memory cache per tab, plus edge cache | Short-lived, shared by everyone, cheap to refetch. |
| Appearance preferences | Cookie (anonymous) or user options (logged in) | Read by an inline script before first paint. |
| Loaded JavaScript modules | localStorage keyed by version | Skips the network on repeat visits. |
| Edit draft | IndexedDB, expiring after 30 days | Survives a closed tab or a crash; private to the device. |
| Base revision id | Editor state, sent with the save | The only way the server can detect a conflict. |
The interface
The endpoints split the same way the data does. Read endpoints are GETs with URLs that work as cache keys and return the same answer for everyone; write endpoints are POSTs with a CSRF token and are never cached.
| Endpoint | Used for | Caching |
|---|---|---|
GET /wiki/{title} | The article page (HTML) | Edge cached, purged on edit |
GET /api/rest_v1/page/summary/{title} | Page previews (JSON) | Edge cached, refreshed after edits |
GET /w/rest.php/v1/search/title?q=&limit=10 | Search suggestions | Public, cached by query for a few hours |
GET /w/index.php?title={t}&action=history&offset=&limit=50 | Revision list (HTML) | Short or none |
GET /w/index.php?diff={new}&oldid={old} | Diff page (HTML) | Long: both revisions are immutable |
POST /w/api.php with action=edit | Save, with baserevid and a token | Never |
Errors on the write path are typed codes, not messages: editconflict, badtoken (the session expired; fetch a new token and retry once), protectedpage, and abuse filter or CAPTCHA responses that the editor turns into a dialog. The client maps each code to a state in the save machine, and none of them discards the draft.
Check your understanding · Exercise 9
Which pairings of data and storage are correct? Select all that apply.
Think first
Fundraising wants a banner on every article for readers in some countries only. Where does the decision happen, so the cache keeps working?
In the browser, after the cached page has arrived. A small script reads the reader's country (from a cookie the edge sets, or a tiny geolocation endpoint), asks a cacheable endpoint which campaign applies, and inserts the banner into a reserved slot.
Reserving space avoids a layout shift when the banner appears. Baking the banner into the HTML would make the page vary by country, multiplying cache copies and breaking the "one URL, one copy" rule.
Accessibility, offline reading, and fighting vandalism
Three topics an interviewer often picks as a follow-up. Each deserves a real answer, not a checklist line.
Accessibility for very long documents
Our target is WCAG 2.2 AA. An encyclopedia article is mostly text, so the server-rendered HTML does most of the work if it is semantic: one h1, a real heading outline (h2 for sections, h3 below), nav landmarks for the TOC and the menus, main for the content, tables with header cells and captions, and alt text on images (written by editors, so the edit tools should prompt for it).
- Skip link. The first focusable element is "Jump to content", so keyboard users don't tab through the header and sidebar on every page.
- Readable typography. In June 2024 Wikimedia raised the default desktop text size for logged-out readers from 14 to 16 pixels, and the Appearance menu lets readers choose small, standard, or large text and standard or wide width. A limited line length keeps lines readable; the wide option exists for those who prefer it.
- Collapsible parts stay usable. Collapsed mobile sections and the TOC menu use real
buttonelements witharia-expanded, andfind in pagestill works because the text is in the HTML. - Popups and previews. Page previews open on keyboard focus as well as hover and are marked up as tooltips; ours also close with
Escapeand never trap focus. Search suggestions use the combobox pattern. - Motion and contrast. Our sticky header appears without animation when
prefers-reduced-motionis set, and both themes must keep body text contrast at 4.5:1 or better, including in template-painted boxes like infoboxes.
Offline and low-data reading
Offline Wikipedia already exists, just not mainly through the website. Kiwix packs whole wikis into ZIM files, compressed archives that a reader can download once and browse with no connection, used in schools and prisons and on ships. In 2026 the English edition is about 50 GB without images, about 120 GB with them, and about 13 GB for lead sections only.
The official Android and iOS apps can save articles for offline reading. For the website, a service worker could cache recently read articles and the skin assets, so a reader who loses signal can still open what they read yesterday.
Wikimedia has experimented with service workers for the web, but treat a production service worker on Wikipedia as an idea, not a shipped feature. The risks are real at this scale: a buggy worker is installed in millions of browsers and can serve stale pages after edits, and the worker's own updates need careful versioning. If we built it, we would cache only what the reader opened, keep a small quota, use network-first for articles (fresh when online) and cache-first for versioned assets, and include a kill switch.
Patrolling and reverting
Open editing works because bad edits are cheap to undo. Patrollers watch the Recent Changes feed, which shows every edit with its byte change, its summary, and tags. Machine-learning scores (from Wikimedia's model service, Lift Wing, which replaced ORES) flag edits that are likely damaging, and filters let patrollers see only those.
From a diff, one click on rollback reverts all consecutive edits by the last editor, and undo reverts a single edit through the same three-way merge as a normal save. Purges then push the clean version to the edge within seconds, which is the whole point of purge-on-edit.
Check your understanding · Exercise 10
Which statements about accessibility, offline reading, and undo are sound? Select all that apply.
Running it in the interview
In 45 minutes you can't cover every chapter. Lead with the read path, because it is the insight that makes everything else fall into place, then let the interviewer choose between editing and the reading experience.
| Minutes | What to do | What to draw |
|---|---|---|
| 0 to 5 | Ask about logged-out share, devices and networks, languages, and edit volume. State numbers: about 15 billion views a month, mostly anonymous, many on 3G. | Nothing yet; write the numbers in a corner. |
| 5 to 15 | The read path: server rendering, edge cache, purge on edit, the logged-in bypass. | Reader, edge, origin, parser cache, purge queue. |
| 15 to 23 | Page weight: text first, the feature test, lazy images, collapsed sections, no web fonts. | A timeline of the first 3 seconds on slow 3G. |
| 23 to 35 | Editing: load on demand, base revision, three-way merge, the conflict screen, drafts. | The save state machine and the conflict sequence. |
| 35 to 42 | One deep dive the interviewer picks: search, RTL and languages, TOC and previews, or accessibility. | The wireframe, or the search sequence. |
| 42 to 45 | Trade-offs you made and what you would measure. | Nothing new. |
Say these sentences out loud, because they show seniority: "Cost should follow edits, not views." "Anything personal stays out of the shared HTML." "Every script is optional; the page must be readable if none of them load." "The base revision id is how the server knows what the editor saw."
Think first
Follow-up: "A celebrity dies. One article gets 100 times its normal traffic and 30 edits in 10 minutes, some of them vandalism. What happens?"
Reads stay on the edge: each edit causes one purge, and concurrent misses after a purge are coalesced into one origin request per edge site. The origin sees 30 renders, not millions.
Editors hit conflicts often, but most edits touch different lines and auto-merge. Vandalism is reverted with rollback within seconds, and the purge removes it from the edge. Administrators may also protect the page temporarily so only experienced accounts can edit, which is a policy lever, not a technical one.
Think first
Follow-up: "Why not make it a modern single-page app with client-side routing between articles?"
Because the first view is what matters and most readers arrive from a search engine on a single article. A single-page app makes that first view slower on our hardest networks, and it complicates edge caching and the no-JavaScript baseline.
Client-side navigation could help readers who click from article to article, but you could get much of that benefit by prefetching a linked page's HTML on hover, which keeps every page a cacheable document.
Check your understanding · Exercise 11
Which moves strengthen an answer to this question? Select all that apply.
Summary
- Wikipedia is a read problem first: render once per revision, cache whole pages at the edge, and let edits drive freshness through purges.
- Keep the shared HTML free of anything personal; apply themes and settings with an inline script and a cookie the server ignores.
- The first response must be readable alone; scripts load after a feature test and only for the features a page uses.
- On phones, lazy images, collapsed sections, and a lead paragraph above the infobox matter more than any framework choice.
- Load the editor only on demand, send the base revision id with every save, merge non-overlapping edits, and never discard the user's text on a conflict.
- Diffs of two revisions are immutable, so compute them on the server once and cache them.
- Hundreds of languages mean direction-aware CSS,
langanddiron mixed text, system fonts by default, and variants in the URL so the cache key stays simple.
Check your answer
Tick what your own design covered. Pick the level you're interviewing for: each level includes the ones before it.
References
- Wikitech: CDN (Wikimedia's caching layers, lifetimes, and purging)
- Wikitech: purged (Kafka-based CDN purging)
- Wikimedia Diff: Unifying our mobile and desktop domains (2025)
- MediaWiki: Compatibility (Modern and Basic layers, Grade A and C browsers)
- MediaWiki: ResourceLoader architecture
- MediaWiki: Client preferences (theme and text size before first paint)
- MediaWiki: Page Previews
- MediaWiki: Edit Recovery
- English Wikipedia: Help:Edit conflict
- MediaWiki: Paragraph-based Edit Conflict Interface (TwoColConflict)
- MediaWiki: Parsoid
- MDN: Intersection Observer API
- MDN: CSS logical properties and values
Concepts behind it
- Server-side renderingThe server builds the full HTML for each request, so users see content before any JavaScript runs, then the browser hydrates it: faster first paint and better SEO, paid for with server work, a later first byte, and a second render in the browser.
- CDN cachingKeeping shared copies of your responses on servers near every user, so most requests never travel to your origin: the biggest lever for global speed and for surviving traffic spikes.
- HTTP cachingThe browser's built-in cache, steered by headers on each response, which lets a repeat visit skip the network entirely or ask only "has this changed?": the cheapest speed-up on the web.
- The critical rendering pathEverything the browser must fetch and compute before the first pixels: HTML, render-blocking CSS, parser-blocking scripts, then style, layout, paint, and composite, and how to count, measure, and shorten it.
- Images and mediaSending each user an image of the right size and format, reserving its space, and loading it at the right moment: images are usually the largest share of a page's bytes and its LCP element.
- Right-to-left layoutsHow to build layouts that mirror correctly for Arabic, Hebrew, Persian, and Urdu readers: the
dirattribute, CSS logical properties, which icons to flip, text that mixes both directions, and how to test all of it. - Translating an appHow an app speaks many languages: text in per-locale message catalogs, ICU messages so plurals and gender work everywhere, catalogs loaded per locale and route, clear fallbacks, and a safe workflow with translators.
- Debounce and throttleTwo small timing tools that turn a flood of events (keystrokes, scrolls, resizes) into a few calls you can afford: debounce waits for a pause, throttle runs at a steady rate.