Syed Ahmer ShahSyed Ahmer ShahSyed Ahmer ShahSyed Ahmer ShahSyed Ahmer Shah — home
  1. …
  2. Engineering Logs
  3. The Engineering Logs
  4. OfflineShelf: Choosing Which Wikipedia Goes Offline
Part 21 of 23

OfflineShelf: Choosing Which Wikipedia Goes Offline

A case study on the hard part of offline Wikipedia, which isn't copying the articles but deciding which ones, and keeping that decision honest month after month

Written by

Syed Ahmer Shah

Software Engineer

Nov 2, 202611 min read2,360 words—reading now▼
The OfflineShelf emblem beside the app showing a combined list's build history, under the title Choosing which Wikipedia goes offline
Read this articlevoice
Markdown

There are schools with a room full of computers and no reliable internet. There are clinics where the connection comes and goes with the weather, and libraries where it costs more per month than the books. For places like these, a free app called Kiwix can carry Wikipedia offline: you copy one file onto a laptop, a phone or a small server, and the articles open with no connection at all.

Copying is the easy part. The whole of English Wikipedia with pictures is a download of roughly a hundred gigabytes. That won't fit on an old phone, and most of it won't matter to a nursing course or a secondary-school science class. Someone has to choose. And once they've chosen, the next questions arrive: can we make the same list again next term? What changed since last time? Can we add the biology articles but leave out the biographies?

OfflineShelf is my answer to that choosing step. You describe a list, it builds it from Wikipedia, keeps every version, rebuilds it every month if you ask, and exports it in a form you can carry. I built it at the end of October 2026, across 26 pull requests, and every feature works end to end. It isn't public yet. The launch at offlineshelf.ahmershah.dev is planned for February 2027, after the testing I describe at the end.

This is how it fits together, what fought back, and the two rules I wrote down before anything else.


#Two rules before any code

Store the choice, not the content. OfflineShelf keeps article titles and their ID numbers, never the text. Wikipedia already keeps the text, updates it every minute and records who wrote what. If I copied it into my own database, I'd be running a stale mirror with none of that history. The text is only fetched at the very end, when someone asks for an export, and it goes straight into the file.

A failed build never replaces the last good list. Every time a list is built, the result is saved as a new version next to the old ones. If the build fails halfway, because Wikipedia was slow or a category was renamed, the previous version is still there, untouched and still downloadable. A teacher who prepared a USB stick last month should never find that this month's list is half empty because a server hiccupped.

Both rules made the code longer. Both rules are why I'd trust it with somebody else's classroom.

#Four ways to ask Wikipedia for a list

People already describe what they want in different ways, so there are four builders.

A category. Wikipedia already sorts its articles into categories, like "Vaccines" or "Rivers of Pakistan". Categories contain smaller categories, which contain smaller ones, and some of them loop back on themselves. The builder walks down to a depth you choose, from 0 to 5, remembers where it has been so it never goes round in circles, and stops once the list is big enough.

A Wikidata query. Wikidata is Wikipedia's sister project: a database of facts, like "this is a university" and "it's in Pakistan". You write a query in its language, SPARQL, and the builder turns each result into the matching article on your chosen Wikipedia. When a result has no article in that language, the report says so instead of quietly leaving it out.

A pasted list. Sometimes you already have the list: a syllabus, a teacher's reading list. You paste the titles, and the builder cleans them up the way Wikipedia does, follows redirects (so an old or alternative title lands on the article it points to), removes duplicates and reports anything that doesn't exist.

The most-read articles. The top articles by pageviews for a given month, straight from Wikimedia's statistics, with the main page and the special pages filtered out.

The Builders page: a table that matches what you already have to the builder that fits, from a category to a combination
The Builders page: a table that matches what you already have to the builder that fits, from a category to a combination

The first time I ran all four against the live English Wikipedia, the reports were the most useful part. Three pasted titles came back with two redirects followed, one missing page and one that wasn't an article at all. A Wikidata query for universities in Pakistan returned eight results, of which seven had English articles. The pageviews list for August 2026 had already dropped the main page and the special pages that always crowd the top. None of that was hidden from the person asking.

#Being a good guest on Wikimedia's servers

Wikipedia is run by a non-profit and its servers are paid for by donations. Its API has rules for tools like mine, and I decided early to follow every one of them, because a tool that gets blocked stops working for everybody using it.

Every request goes through one small client. It introduces itself with a name and a contact address, so Wikimedia can reach me if something goes wrong. It sends a setting called maxlag, which means "if your database replicas are falling behind, tell me to wait". When the servers say wait, it waits, and tries again later.

I saw this happen for real. While I was running the example lists in Docker, Wikidata was lagging. The client backed off, the job was put back in the queue, and a few minutes later the list built normally. Nobody had to do anything. That's the boring outcome I was hoping for.

There's also a rule for my own tests: none of them may call the live APIs. They replay real responses I recorded once, and a guard fails any test that tries to reach Wikimedia. CI runs about two hundred backend tests on every change, and none of them puts any load on a volunteer-funded server.

#Lists that combine

Real collections are rarely one simple query. A health worker might want everything about vaccines, minus the pharmaceutical companies. A science teacher might want physics and chemistry together. So lists can be combined: union (in any of the lists), intersection (in all of them) and difference (in the first, in none of the others), from two up to ten lists.

A combination is a list like any other. It has versions, a history and exports. And when one of its source lists gets a new version, the combination rebuilds itself.

Union, intersection and difference, drawn as overlapping circles on the Builders page
Union, intersection and difference, drawn as overlapping circles on the Builders page

Under the hood, every source list is read already sorted by page ID, a little at a time, and merged in a single pass. That sounds like a detail, but it means combining two lists of 100,000 articles uses about as much memory as combining two lists of ten.

#What changed since last month?

Wikipedia changes every day. Articles get merged, renamed, deleted and created. A list that was right in March can be quietly different by June, and the person who carries it into a classroom deserves to know how.

So whenever a new version is ready, OfflineShelf compares it with the last good one and stores what was added and what was removed. In the app, the build history shows those two numbers next to each build, and you can open either list and read the titles. The comparison runs inside the database, so even big lists are never loaded into memory to work it out.

Monthly rebuilds work the same way. A small scheduler wakes up every hour and asks: is there a monthly list that hasn't been built since the 1st? If the server was switched off on the 1st, the next hourly check catches up instead of waiting a whole month.

Also readAIT: Building a University Admissions Portal From Scratch
I Invented 14 People to Break My University Website

#Three ways to carry it

A list is only useful once it leaves the browser, and different places need different things.

  • A TSV file: one row per article with its title, ID and revision, which opens in any spreadsheet and feeds other tools, including Kiwix's own. The first lines carry the licence and where the list came from.
  • An HTML bundle: a zip with one web page per article, an index and an attribution page. Unzip it onto a USB stick, double-click the index, and it works with no server and no internet. Links between articles on the list open the local copy; links to anything else point back to Wikipedia.
  • A ZIM file: the same pages in the single searchable file that the Kiwix apps read on phones, computers and Kiwix hotspots.
What's inside an HTML bundle: an index, an attribution page and one file per article, and an article opened from a USB stick
What's inside an HTML bundle: an index, an attribution page and one file per article, and an article opened from a USB stick

Two honest limits. Bundles and ZIM files are capped at 1,000 articles and expire after seven days, to keep the server's disk small; you can make a fresh one from the same version and get the same articles. And images are linked rather than copied for now, so offline pages show text without pictures.

The files inside a bundle are named by page ID, not by title. My first instinct was titles, until I remembered that a title like "Batman: The Animated Series" contains a colon, which Windows refuses in a file name, and that macOS treats "Red Dwarf" (the TV series) and "Red dwarf" (the star) as the same file. Numbers never have either problem.

Every page also ends with where it came from: the article, the exact revision, the licence and a link to its history. Wikipedia's text is shared under a licence called CC BY-SA, which lets anyone reuse it as long as they credit the authors and share it the same way. An offline copy that drops the credit breaks the deal, so every export carries it.

#Big lists, small memory

Most lists will be a few hundred articles, but I wanted to be sure a very large one wouldn't take the server down. So I wrote a test that pushes a list of 100,000 articles through the real build job into the real database, then streams the whole export back, and measures memory the whole way.

The build peaked at about 1.6 MB and the export at about 3.3 MB. Holding all 100,000 articles in memory at once would take about 19 MB, so the test fails if either step ever goes above 8 MB. My first limit was 20 MB, and then I measured the baseline and realised that limit would have let the exact mistake I was guarding against slip through. I tightened it.

#What I got wrong

A restarted worker left lists stuck forever. While running the example lists, I restarted the background worker in the middle of a build. The build never finished, never failed, and sat in "building" forever, which blocked every new build of that list. The same thing would happen after a deploy or if the server ran out of memory. Now a recovery job runs every hour and marks builds whose worker has died as failed, with a readable reason, so the list can be built again. It's careful to leave alone the jobs that are only waiting for a retry.

Combinations failed while their sources were still building. The example set has two lists and their union. When the first source finished, the combination was queued straight away, found the second source still empty, failed, and would have emailed its owner about a problem that was going to fix itself a minute later. You can see that failure in the build history below. Combinations now wait until every source has a good version.

The app's build history for a combination: a failed build saying a source list has no ready version yet, then a ready build of 475 articles
The app's build history for a combination: a failed build saying a source list has no ready version yet, then a ready build of 475 articles

Titles are messier than they look. Wikipedia capitalises the first letter of a title, except on Wiktionary, and except for letters like the German "ß", which turns into two letters when capitalised. Special pages have no page ID, which crashed my first lookup code, and the most-read list is full of them. And on the German Wikipedia, subcategories come back as "Kategorie:", which my code would have turned into "Category:Kategorie:Something". Each of these is now a test.

I had an option to send email without encryption. OfflineShelf emails the owner when a build fails. I'd left a setting that let a self-hoster turn encryption off for their mail server. A code-quality scanner flagged it, and it was right: there's no good reason to keep that door open. Mail now always goes out encrypted.

#By the numbers

  • 4 ways to build a list, plus combinations of up to 10 lists
  • 3 export formats, every one with attribution
  • 8 Wikimedia projects in any language: Wikipedia, Wiktionary, Wikibooks, Wikinews, Wikiquote, Wikisource, Wikiversity and Wikivoyage
  • 235 automated tests: 197 on the backend, 38 on the frontend
  • 100% branch coverage on the list logic, enforced by CI
  • 100,000 articles built and exported in a test, with memory staying under 4 MB
  • 26 pull requests, each checked by CI and CodeQL before merging

#What's next

The features are done; what's left is proving them under pressure. Before launch I want end-to-end browser tests of every path, from signing up to downloading a ZIM file, smoke tests on every release, and a load test with 15 to 20 simulated users building, combining and exporting at the same time while I watch the queue, memory and how hard we're leaning on Wikimedia. Then it goes live in February 2027.

After that, the work moves outside the code: the first school or library that actually uses it, and the things they tell me I didn't think of. Pictures in exports are near the top of that list already.

If I started again, I'd write the same two rules on day one. And I'd restart the worker in the middle of a build in the first week, on purpose, instead of finding out by accident.

The source is on GitHub, and I wrote about the idea itself, for people who have never thought about offline Wikipedia, in A library that fits on a USB stick.

Find me across the web.

Same person, every platform

Work & code

  • ASPortfolioahmershah.dev
  • LinkedInin/syedahmershah
  • GitHub@ahmershahdev
  • AWSAWS Builder Center@syedahmershah
  • Crunchbasesyed-ahmer-shah
  • LinkedIn · Companycompany/syedahmershah

Writing

  • Dev.tosyedahmershah
  • Medium@syedahmershah
  • Hashnode@syedahmershah
  • Substack@syedahmershah
  • HackerNoonu/syedahmershah
  • CLCoderLegionuser/syedahmershah

Reviews & listings

  • Google Knowledge PanelSyed Ahmer Shah
  • Google Business Profileg.page
  • ClClutch
  • TpTrustpilot
  • DRDesignRush
  • TBTechBehemoths

Social

  • YouTube@ahmershahdev
  • Instagram@ahmershahdev
  • TikTok@ahmershahdev
  • Facebookahmershahdev
  • X@ahmershahdev
#offlineshelf#casestudy#flask#react#wikipedia#apidesign#backgroundjobs#systemdesign
ShareXLinkedInRedditWhatsApp

← Previous in series

I Invented 14 People to Break My University Website

Next in series →

A Library That Fits on a USB Stick

On this page

  • Two rules before any code
  • Four ways to ask Wikipedia for a list
  • Being a good guest on Wikimedia's servers
  • Lists that combine
  • What changed since last month?
  • Three ways to carry it
  • Big lists, small memory
  • What I got wrong
  • By the numbers
  • What's next

The Engineering Logs

  1. 01The Edge Latency Lie: Solving Global Consistency
  2. 02Top 10 AI Tools Every Frontend Developer Should Know (2026 Guide)
  3. 03Change One Number. Take Everything.
  4. 04How Databases Work: From Tables to Query Execution
  5. 05The New Era of Software Engineering
  6. 06The Art of Object-Oriented Programming
  7. 07GleanGrid: Building a Pre-Order Marketplace for Farmers Markets
  8. 08The Last Mango Problem: Race Conditions in a Real Marketplace
  9. 09BookMyMovie: Building a Cinema Ticketing Platform
  10. 10One Seat, Two Buyers: Race Conditions, IDOR and Keys in a Cinema Booking System
  11. 11Zovita+: Building an Online Pharmacy That Adapts to Each Shopper
  12. 12The Discount That Knew You Were Hesitating: Rule-Based Personalisation in an Online Pharmacy
  13. 13RAG Retrieval Strategies on a 50k-Document Dataset: BM25 vs Dense vs Hybrid
  14. 14Inside the Database Stack
  15. 15HolidayOps: Building a Working Days API for 250 Countries
  16. 16The Weekend Is Not a Constant
  17. 17MarketClock: A Trading Calendar That Shows Its Work
  18. 17Cybersecurity Trends: Protecting Data in the Digital Age
  19. 18The Day the Money Moves
  20. 19AIT: Building a University Admissions Portal From Scratch
  21. 20I Invented 14 People to Break My University Website
  22. 21OfflineShelf: Choosing Which Wikipedia Goes Offline
  23. 22A Library That Fits on a USB Stick

Up next

Keep reading.

All 61 articles→
  1. 01

    MarketClock: A Trading Calendar That Shows Its Work

    How I built MarketClock on my own: a Flask and Next.js app that tells traders and back offices whether 30 stock exchanges are open, when they close early and when a trade settles, and that links every fact it stores to the page it came from.

    Oct 24, 20269 minThe Engineering Logs

  2. 02

    HolidayOps: Building a Working Days API for 250 Countries

    How I built HolidayOps on my own: a Django and React platform that tells payroll, HR and logistics teams whether any date is a working day in any country, with company calendars, iCal feeds, signed webhooks and a public site that needs no JavaScript.

    Oct 18, 20269 minThe Engineering Logs

  3. 03

    Zovita+: Building an Online Pharmacy That Adapts to Each Shopper

    How I rebuilt Zovita+, an online pharmacy with 1,186 real medicines, a 3D symptom body map, personal offers, drug-interaction warnings, refill reminders, pharmacist-reviewed prescriptions, Urdu, and card or cash payments, on Laravel, React and MySQL: stack, data model, checkout locking, payments, admin, security, SEO, testing, what broke and what I'd change.

    Oct 2, 202622 minThe Engineering Logs

  4. 04

    BookMyMovie: Building a Cinema Ticketing Platform

    The story of BookMyMovie, a cinema booking site I built alone: why buying a ticket in Pakistan is still a hassle, the one rule the whole system is built around, how two people can never end up with the same seat, the back office, the design, and the bugs that slipped through.

    Sep 29, 202620 minThe Engineering Logs

  5. 05

    GleanGrid: Building a Pre-Order Marketplace for Farmers Markets

    How I designed and built GleanGrid on my own, a Laravel and React marketplace where Hyderabad's farmers publish weekly stock and customers reserve it, pay online or at the stall, and collect it: architecture, data model, payments, security and UX.

    Sep 27, 202615 minThe Engineering Logs

  6. 06

    A Library That Fits on a USB Stick

    Wikipedia can work with no internet at all. A plain-language story of how offline Wikipedia reaches schools and clinics, why choosing the articles is the hard part, and what building OfflineShelf taught me about lists, change and giving credit.

    Nov 2, 20266 minThe Engineering Logs

Available for new work · --:--:-- PKT

Have an idea?

sales@ahmershah.dev
Book a call WhatsApp +92 370 4831994

On this site

  • 01About
  • 02Education
  • 03Certificates
  • 04Skills
  • 05Projects
  • 06Experience
  • 07Services
  • 08Testimonials
  • 09Contact

Pages

  • Projects
  • Blog
  • Saved articles
  • RSS
  • Privacy Policy
  • Terms of Service
  • llms.txt

Elsewhere · official profiles

26 links

Professional

Work history, code and credentials.

  • in/syedahmershah
  • company/syedahmershah
  • @ahmershahdev
  • syed-ahmer-shah
  • syed-ahmer-shah
  • u/syedahmershah
  • @syedahmershah
  • AWS@syedahmershah

Coding & credentials

Problem solving, verified badges.

  • u/syedahmershah
  • syedahmershah
  • users/syedahmershah
  • learner/syedahmershah

Google & business

The official listings.

  • Google Business Profileg.page
  • Syed Ahmer Shah
  • syedahmershah
  • Beaconssyedahmershah
  • syedahmershah

Social (@ahmershahdev)

Build logs, clips, threads.

  • @ahmershahdev
  • @ahmershahdev
  • @ahmershahdev
  • ahmershahdev
  • @ahmershahdev
  • @bluesky.ahmershah.dev
  • ahmershahdev

Design

Interfaces and visuals.

  • syedahmershah
  • syedahmershah

Direct

  • sales@ahmershah.dev

    New projects & quotes

  • support@ahmershah.dev

    Support & help

  • syedahmershahofficial@gmail.com

    Personal

  • +92 370 4831994

    Phone · WhatsApp

  • Hyderabad, Sindh, Pakistan

    Remote-first · Asia/Karachi (UTC+5)

Resume ↗
SYED AHMER SHAH

© 2026 Syed Ahmer Shah. All rights reserved.

PrivacyTermsRSS
ahmershah.dev