A self-contained, offline-capable Magic: The Gathering research tool powered by the Scryfall API
Single HTML file · No build step · IndexedDB + Cache API persistence · Real-time search
This document describes every major subsystem, data flow, caching strategy, and internal logic of the application. It is intended for DevOps architects, maintainers, or anyone needing a precise, technical understanding of how the app works.
- Overview
- System Architecture
- Core Components & State Management
- User Input & Query Translation
- API Integration & Rate Limiting
- Caching & Offline Persistence
- Search Engine & Display
- Stop Button & Abort Control
- Card Detail Modal
- Developer & Debug Panels
- Offline Database Panel
- UI Interactions & Visual Feedback
- Initialization & Event Wiring
- Deployment & Hosting
- Dependencies & External Resources
The Scryfall Researcher is a single-page HTML application that lets users write free-form card queries (one per line) and instantly see matching Magic: The Gathering cards from the Scryfall database. It includes advanced features like:
- Intelligent translation of natural / shorthand notations into valid Scryfall search syntax.
- Full offline caching of card data and images using IndexedDB and the Cache API.
- Double‑faced card support, collector number pinning, set overrides.
- Grouped or flat results view, sticky stats bar, scroll‑to‑top, auto‑scroll.
- Developer panel for direct API endpoint testing.
- Built‑in offline database browser and management.
- Stop button to cancel long-running searches.
All logic, styling, and HTML are contained in a single file; only the Quill rich text editor is loaded from a CDN. No server‑side component is required — it can be served from any static web host.
- Quill Editor: Accepts user input (one query per line).
- Translation Engine: Converts each line into a Scryfall‑compatible query string.
- Search Controller: Orchestrates the search loop, respects stop signals, updates UI.
- API Client: Fetches data from Scryfall with caching, rate‑limiting, and error handling.
- Storage Layer: Two independent caches – IndexedDB for structured data, Cache API for binary images.
- UI Layer: Renders cards, modals, panels, and handles all user interactions.
The entire application lives inside an immediately‑invoked function expression ((function(){...})()). All DOM references are stored in the DOM object, keyed by element ID. Global mutable state includes:
| Variable | Purpose |
|---|---|
db |
Open IndexedDB instance. |
searching |
Boolean flag preventing concurrent searches. |
searchAbortController |
AbortController instance for the current search, enabling cancellation. |
rowMap |
Map storing DOM rows when results are grouped by query. |
globalCardIndex |
Counter for stagger animation delays. |
autoScroll |
Boolean that remembers whether the user was at the bottom of the page. |
modalLoadId |
Monotonically increasing ID to prevent stale modal updates. |
currentModalCardId |
ID of the card currently shown in the modal (null when closed). |
quill |
Quill editor instance. |
Important functions and their roles:
translateAll()– reads Quill text, splits lines, callstranslateLine()for each, returns{lines, queries}.doSearch()– main entry point for a search, callssearchAll().doCustomSearch()– uses text from the advanced panel (custom queries) instead of the editor.searchAll(queries, lines, onProgress)– iterates over queries, fetches results, builds DOM.fetchCached(query, addKey)– central API call with caching.showModal(card, source)– renders the detailed card modal.makeCardElement(card, sourceLine, staggerIndex)– creates a single card thumbnail DOM element.
The Quill editor (.ql-editor) is the primary input. Each line is treated as a separate search query. The text can contain:
- Natural card names (e.g.,
"Black Lotus") - Oracle text snippets (e.g.,
Search your library for a card...) - Shorthand notations like
add: RRRRR(mana production) - Explicit Scryfall syntax (
t:creature c<=WGB) - Double‑faced cards using
//or/(e.g.,Delver of Secrets // Insectile Aberration) - Parenthetical set and optional collector number:
1 Ancient Den (SLD)orLotus Petal (MPS) 15 *F* - Foil/Etched markers (
*F*,*E*,*S*) which are ignored during translation. - Leading numbers (deck quantities) are stripped.
Each non‑empty line is processed through a series of steps:
add:shorthand: If the line starts withadd:, it converts the following mana symbols into anoracle:"add {W}{U}..."query.- Trim and strip leading quantity:
1 Black Lotus→Black Lotus. - Extract set and collector number from parentheses: Looks for
(SET) numberat the end. Example:Lotus Petal (MPS) 15 *F*→ set=MPS, number=15, line reduced toLotus Petal.Ancient Den (SLD)→ set=SLD, line reduced toAncient Den.- The extracted set code is used later to append
set:XYZto the query.
- Remove foil/etched/showcase markers:
*F*,*E*,*S*are stripped. - Detect double‑faced cards: If the line contains
//or/, it’s treated as a double‑faced card name and wrapped in(name:"..." OR oracle:"..."). - Quoted exact name: If the line is entirely wrapped in double quotes, it becomes
name:"...". - Fallback for plain text without Scryfall operators: If no keyword (
t:,o:,c:, etc.) is detected, the line is wrapped as(name:"..." OR oracle:"..."). - Hybrid lines: If the line contains both keywords and free text, the free text is turned into a
(name:"..." OR oracle:"...")clause, and keyword tokens are appended as separate conditions. - Append global modifiers: The global set override (if any) and language selection are appended to every query.
- Append collector number: If a number was parsed from the line,
cn:XYZis appended.
The result is a valid Scryfall search query string ready for the API.
| Input (line) | Generated Query |
|---|---|
"Black Lotus" |
name:"Black Lotus" |
add: RRRRR |
oracle:"add {R}{R}{R}{R}{R}" |
1 Ancient Den (SLD) |
name:"Ancient Den" set:SLD |
Lotus Petal (MPS) 15 *F* |
name:"Lotus Petal" set:MPS cn:15 |
Birgi, God of Storytelling // Harnfel |
(name:"Birgi, God of Storytelling // Harnfel" OR oracle:"Birgi, God of Storytelling // Harnfel") |
t:cat c<=WGB pow>3 |
type:cat color<=WGB pow>3 |
Search your library for a card |
(name:"Search your library for a card" OR oracle:"Search your library for a card") |
The generated queries are also written into the advanced panel (#customQueries) where they can be manually edited and searched via the "search these" button.
GET /cards/search?q=...– main search, respectsunique=prints,order,dir.GET /cards/:code/:number– fetch a specific card by set and collector number.GET /cards/random– fetch a random card.GET /bulk-data– list of available bulk data files.
This is the core search function. It:
- Constructs a cache key:
search:<query>|<addKey>, whereaddKeyencodes the current “first printing” and sort order settings. - Checks IndexedDB for a previously stored result. If found, logs as
📦 CACHEand returns it immediately. - If not cached, builds the URL:
https://api.scryfall.com/cards/search?q=<encodedQuery>&unique=prints&order=released&dir=asc|desc
- Fetches the data with standard headers (
Accept,User-Agent). - On success:
- Applies first‑printing filter (
DOM.firstPrinting.checked) – groups byoracle_idornameand keeps only the oldest printing. - Stores each card object in IndexedDB (
card:<id>). - Pre‑caches card images (normal and small sizes) using the Cache API.
- Stores the entire result array in IndexedDB under the cache key.
- Logs the API call with detailed console output (
logApi).
- Applies first‑printing filter (
- Returns the array of cards.
Rate Limiting: The search loop (in searchAll) inserts a 1.2‑second delay between individual queries to stay well within Scryfall’s recommended 10 requests/second and hard 2/second limit for the search endpoint. The dev panel manual fetches are not rate‑limited (used sparingly).
For random or set/number lookups:
- First checks IndexedDB using
getCardBySetNum(which looks upsetnum:<set>/<number>to find the card ID, then retrievescard:<id>). - If not cached, fetches from the API, saves the card and the set‑number reference, pre‑caches images, and shows the modal.
Every API interaction (including cache hits) is logged to the browser console with:
- Full URL, status code, method, and a preview of the returned data.
- The last call is displayed in the developer panel (
#lastApiCall).
Two separate browser storage mechanisms are used, working together to provide a fast, offline‑capable experience.
Database: ScryfallDB, version 2
Object store: cards with keyPath: 'key' and indexes on timestamp and type.
| Key Pattern | Type | Value |
|---|---|---|
card:<scryfall_id> |
card |
Full card JSON object from Scryfall |
setnum:<set>/<collector_number> |
setnum |
{ cardId: <scryfall_id> } |
| `search: | ` | search |
Usage:
- Card data is stored permanently until explicitly deleted. This allows offline browsing of previously fetched cards.
- Search results are cached so that identical queries (with same settings) load instantly.
- The offline database panel provides full CRUD‑like inspection and deletion.
Functions: openDB, dbPut, dbGet, dbGetAll, dbClear, getCardBySetNum, saveCardAndRef.
Cache name: scryfall-images-v2
All card images (normal and small versions) are fetched and stored in this cache. When displaying a card image, the app first tries to retrieve it from the cache (as a blob URL) via getCachedImageUrl. If not present, the original URL is used and the browser’s native cache takes over.
Pre‑caching: After fetching card data, preCacheCardImages iterates over all available image URIs (including card faces) and stores them. This ensures images are available offline after the first view.
- User clicks search (or auto‑searches on first load).
doSearch()callstranslateAll()to getqueriesandlines.- If no queries, the grid is cleared and shows “write something”.
searchAllis invoked with the list of queries.- An
AbortControlleris created and stored insearchAbortController; a new controller replaces any previous one. - The card grid is reset via
resetGridForSearch(). - For each query:
fetchCachedis called.- If the group by query toggle is on, the cards are added to a dedicated row (
addOrUpdateQueryGroup). Otherwise they are appended to a flat grid (appendFlatCards). - Counters and error counts are updated in the stats bar.
- After each query (except the last), the loop waits 1200 ms before proceeding to the next.
- The loop checks
signal.abortedbefore every fetch and after the delay, allowing a stop to abort the whole process.
- Flat (grid‑mixed): All cards from all queries are mixed into one grid. Each card element is appended with a staggered fade‑in animation.
- Grouped by query (grid‑rows): Each query gets its own collapsible row with a header showing the original line, and a nested grid of cards. Rows are reused if the same line appears multiple times (via
rowMap).
Each card is represented by a div.card-thumb containing:
- An
imgwith the card art (lazy‑loaded). Initially the image has classloading‑imgand a shimmer skeleton placeholder is shown. Once loaded, the class switches toloadedand the skeleton is hidden. - The card name and set code.
- A preview of the oracle text (first 80 characters).
- A small source line label.
- Clicking the thumbnail opens the card detail modal.
The image URL is obtained asynchronously via getImageUrlCached, which selects the appropriate art style (normal/large/art_crop/png) and tries the Cache API first.
Stagger animation: Each card has an animation-delay proportional to globalCardIndex * 0.04s, creating a cascading reveal effect.
autoScrollis set totruewhen the user is near the bottom of the page (within 200px). When new cards are added, the last inserted element is scrolled into view withbehavior:'smooth'.- The sticky stats bar gains a shadow when the page is scrolled past 20px (
classList.toggle('scrolled')). - A fixed scroll‑to‑top button appears when the user has scrolled more than 400px; clicking it smoothly scrolls to the top.
The stop button (.stop-btn) is only visible while a search is in progress. It calls stopSearch(), which:
- Calls
searchAbortController.abort(), causing the search loop to exit gracefully. - Sets
searching = false, re‑enables the search button, hides the stop button, and updates the status message to “stopped”.
This mechanism ensures that long‑running searches (e.g., many queries) can be cancelled without leaving the UI in an inconsistent state.
The modal (#cardInfoModal) shows comprehensive card information and is reused for every card view.
- Triggered by clicking a card thumbnail, or via the dev panel / database image grid.
showModal(card, source)is called with a card object and a source label.- A new
modalLoadIdis generated to prevent race conditions if the user quickly opens another card.
The modal first shows a “Loading” spinner and clears all content fields.
- The card’s art style is taken from the current radio button selection.
- For double‑faced cards (except the
adventurelayout), each face is rendered in a separate container with its own image and oracle text. - For single‑faced cards or adventure cards, a single image is displayed.
- Images are loaded from the Cache API or directly.
- Oracle text: Combined from all faces with face names.
- Type line, mana cost (with symbols rendered as
<span class="mana-symbol">). - Metadata grid: set name, collector number, rarity, artist, border, frame, CMC, colors, color identity, keywords, language, release date, reserved status, EDHREC rank.
- Prices: USD, USD foil, EUR, EUR foil, TIX.
- Legalities: Displayed for a predefined order of formats, with color coding (legal/not_legal/restricted/banned).
- Raw JSON: Toggleable view of the card data (with image URIs removed to keep it readable).
- Scryfall link: Opens the card’s page on scryfall.com.
- Clicking the close button, pressing Escape, or clicking the dimmed overlay triggers a closing animation (
.closingclass) and then hides the modal. - After closing, all modal content is reset to avoid stale data.
- Accessible via “⚙️ advanced” toggle.
- Shows a preview of the detected lines and an editable textarea with the generated Scryfall queries.
- “search these” runs the custom queries, bypassing the main editor translation.
- “refresh” re‑translates the Quill content and updates the textarea.
- Rate limit info: Static display of Scryfall’s limits.
- Fetch by set/number: Two input fields and a fetch button that calls
fetchBySetNum. - Random card: Button that fetches a random card and opens the modal.
- Bulk data: Fetches
/bulk-dataand displays the list of available bulk files with sizes. - Endpoints list: A visual reference of all supported endpoints.
- Last API call: Dynamically updated with the most recent API interaction.
- Simple info dialog explaining the tool, triggered by the “info” button or the footer link.
Provides direct access to the IndexedDB store.
- Show all stored cards: Displays a list of all entries with key, type, timestamp, and a preview.
- Show all card images: Renders a grid of all stored cards (using their cached images) and allows clicking to open the modal.
- Delete all data: Clears the IndexedDB store and also purges the image cache (
caches.deleteon the image cache name).
- Dark theme with CSS custom properties.
- Subtle transitions, hover effects, and shadows.
- Animations:
fadeInUpfor cards and modal sections,shimmerfor skeleton placeholders,dotBouncefor loading dots.
- Media queries adjust grid columns, modal size, and layout on screens narrower than 700px.
- The scroll‑to‑top button resizes on small screens.
The init function runs immediately after the script is parsed:
- Open IndexedDB:
openDB()resolves thedbinstance. - Initialize Quill: Set up the editor with a sample text containing various query types.
- Auto‑translate:
refreshQueries()populates the advanced panel. - Auto‑search:
setTimeout(doSearch, 600)triggers an initial search to showcase the app. - Event binding: All buttons, toggles, and selectors are wired to their handlers.
- Keyboard shortcut: Escape key closes either modal.
- The entire application is a single static HTML file.
- No server‑side processing, no build tools, no npm dependencies.
- It can be hosted on any web server (Apache, Nginx, S3, GitHub Pages, etc.).
- All external resources (Quill CSS/JS) are loaded from Cloudflare’s CDN.
- The app works best over HTTPS because service workers and the Cache API are more reliable in secure contexts (though not strictly required for basic functionality).
- The app makes heavy use of the Cache API and IndexedDB; ensure browsers support these (all modern browsers do).
- Large numbers of images may consume significant cache storage. The “delete all data” button helps manage this.
- The 1.2s delay between search queries is intentional; reducing it may result in HTTP 429 errors from Scryfall.
| Resource | URL | Purpose |
|---|---|---|
| Quill CSS | https://cdn.quilljs.com/1.3.6/quill.snow.css |
Rich text editor styles |
| Quill JS | https://cdn.quilljs.com/1.3.6/quill.js |
Rich text editor library |
| Scryfall API | https://api.scryfall.com |
Card data and images |
| Google Fonts | (via CSS font-family fallback) |
Typography |
No other libraries or polyfills are used. The app is entirely vanilla JavaScript with modern ES6+ features (async/await, template literals, const/let, arrow functions, etc.).
The Scryfall Researcher is a polished, feature‑rich, offline‑capable Magic: The Gathering card search tool built with careful attention to data caching, user experience, and API rate‑limit compliance. Its internal logic prioritizes predictability: every query is translated deterministically, every API response is cached, and every UI state transition is animated to keep the user informed. The codebase is a single, well‑structured IIFE that can be understood by any developer familiar with vanilla JavaScript and browser storage APIs.
For any modifications or deployment, refer to the detailed comments within the HTML itself and this architectural document.