Your first page in 10 minutes

Install Quran Text, print an ayah, put it on a web page in the font it was set in, load a second reading, and check a digest so you know the text you shipped is the text that was published. For a picture of the printed page rather than its characters, see Choosing a block; for an image of one verse with no code at all, Crop an ayah as an image is one HTTP GET.

Blocks used
Platforms
Node 18+ · any browser
Time
About 10 minutes
  1. Install

    The JavaScript package is named quran-text in its package.json. It is ESM, has no dependencies, ships TypeScript declarations, and bundles Ḥafṣ — the riwayah most of the world prints — together with its font.

    npm install quran-text
    
    Not on the registry at the commit this page was written against

    At quran-ws/quran-text@fea25cd nothing is published to npm, PyPI, Packagist, pub.dev or Maven Central, and the repository is private. The package name above is the one in lib/js/package.json, and publishing is on the launch list. Until it lands, npm install ./lib/js from a checkout gives you the identical package.

    The same API exists in Python, PHP, Dart, Swift and Kotlin; see Platforms for the package name in each.

  2. Load Ḥafṣ and print an ayah

    Mushaf.hafs() needs no file and no configuration. Everything else in the library hangs off the object it returns.

    // quickstart.mjs
    import { Mushaf } from "quran-text";
    
    const m = await Mushaf.hafs();
    
    m.key;             // "hafs"
    m.ayahCount;       // 6236  — read from the file, not written down
    m.countingSystem;  // "kufi"
    
    m.ayah(1, 4).text;                                   // مَٰلِكِ يَوۡمِ ٱلدِّينِ
    m.ayah(1, 4).render({ marks: true, ayahMarks: true }); // مَٰلِكِ يَوۡمِ ٱلدِّينِ ۝٤
    m.ayah(2, 255).page.number;                           // 42
    
    مَٰلِكِ يَوۡمِ ٱلدِّينِ
    1:4 · Ḥafṣ · m.ayah(1, 4).text

    text is the words joined with spaces and nothing else. render adds what the printed muṣḥaf adds — the pause signs and the end-of-ayah marker with its number. They are different strings and different lengths, which matters the moment you index into them; the Quran Text reference explains why.

  3. Put it on a page, in its own font

    The text is set in a font published by the same body that published the text, and the muṣḥaf file names it. The library writes the @font-face rule for you, so the font family name never appears in your code.

    m.font.family;   // "KFGQPC HAFS Uthmanic Script"
    m.font.file;     // "UthmanicHafs-v-3.0.ttf"
    m.fontFace();    // @font-face { font-family: "KFGQPC HAFS Uthmanic Script"; src: url("…/data/UthmanicHafs-v-3.0.ttf") format("truetype"); }
    

    In a browser the loading call is the same; only the file is fetched rather than read. Serve data/hafs.json and the TTF from the package alongside your app and:

    import { Mushaf } from "quran-text";
    
    const m = Mushaf.fromJson(await (await fetch("/data/hafs.json")).json());
    
    const style = document.createElement("style");
    style.textContent = m.fontFace("/data/UthmanicHafs-v-3.0.ttf");
    document.head.append(style);
    
    const el = document.querySelector("#ayah");
    el.dir = "rtl";
    el.lang = "ar";
    el.style.fontFamily = `"${m.font.family}"`;
    el.textContent = m.ayah(1, 4).render({ marks: true, ayahMarks: true });
    
    Blank or boxed text is a font problem, not missing data

    Three of the seven riwayat use code points that Unicode only added in 2021, and almost no general font draws them. With the wrong font the words render as empty boxes or as nothing, while text.length says the string is intact. Ship the font each edition names in its own font block — seven fonts for seven editions, not one.

  4. Load a second riwayah, and do not reuse the number

    The other six editions are files under data/mushaf/ in the repository — warsh.json, qalun.json, shubah.json, duri.json, susi.json, bazzi.json. Load one and it is a muṣḥaf of its own, with its own ayah count.

    const warsh = await Mushaf.load("data/mushaf/warsh.json");   // Node
    // browser: Mushaf.fromJson(await (await fetch(url)).json())
    
    warsh.ayahCount;         // 6214  — not 6236
    warsh.countingSystem;    // "madani-last"
    warsh.basmalahCounted;   // false — the basmalah of al-Fātiḥa is printed but unnumbered
    
    m.ayah(2, 255).to(warsh).key;        // "2:253-254"   relation "split"
    warsh.ayah(2, 253).to(m).key;        // "2:255"       relation "merged"
    
    warsh.ayah(2, 255) does not throw

    It returns the ayah Warsh numbers 255, which opens لَآ إِكْرَاهَ, the passage Ḥafṣ numbers 2:256, not āyat al-Kursī. Nothing in the type system stops a Ḥafṣ reference being passed to a Warsh muṣḥaf; the only symptom is the wrong verse on screen. Convert with .to() and read relation; never carry a bare number across editions.

  5. Check the digest

    Two digests matter, and they answer different questions.

    Which source text is this? Every muṣḥaf file names the publisher’s package it was extracted from and that package’s SHA-256. Record it with anything you persist, so a review can answer “which text did this ship?” without re-deriving it.

    m.provenance.text;
    // { package: "UthmanicHafs-v-3.0.zip", member: "UthmanicHafs-v-3.0.docx",
    //   release_year: 2026, sha256: "cdec7341b7c684e7b8dd469c4b68988914d2784e924c7e6cbb9cb4b57c24f013" }
    

    Is the file I downloaded the file that was published? data/manifest.json lists the SHA-256 of every emitted file. Check any edition you download against it before you load it, and fail loudly on a mismatch:

    import { createHash } from "node:crypto";
    import { readFile } from "node:fs/promises";
    
    const manifest = JSON.parse(await readFile("data/manifest.json", "utf8"));
    const file = "data/mushaf/warsh.json";
    
    const expected = manifest.files.find((f) => f.path === file).sha256;
    const actual = createHash("sha256").update(await readFile(file)).digest("hex");
    if (actual !== expected) throw new Error(`${file}: digest mismatch`);
    
    // expected === actual === "2134d90016ecd26543ca6eaef21427227ec7641d667b0fc14fbb8e4c3f6dbd3a"
    

    In a browser, crypto.subtle.digest("SHA-256", bytes) gives you the same hex.

    The bundled Ḥafṣ file is not the one in the manifest

    lib/js/data/hafs.json — the copy Mushaf.hafs() loads — is data/mushaf/hafs.json with its whitespace removed. It parses to identical data, but its bytes hash to 66430ff0…, and the manifest lists only the canonical file at 5b104f7d…. So the manifest check above works for editions you download, not for the bundled one. Recorded as a finding in docs/dx/quran-text.md.

The whole thing

Everything above in one file. Comments are the values returned.

// quickstart.mjs — run with: node quickstart.mjs
import { Mushaf } from "quran-text";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";

const m = await Mushaf.hafs();
console.log(m.key, m.ayahCount, m.countingSystem);          // hafs 6236 kufi
console.log(m.ayah(1, 4).render({ marks: true, ayahMarks: true }));   // مَٰلِكِ يَوۡمِ ٱلدِّينِ ۝٤
console.log(m.font.family);                                  // KFGQPC HAFS Uthmanic Script
console.log(m.provenance.text.sha256);                       // cdec7341b7c684e7b8dd…

const file = "data/mushaf/warsh.json";
const manifest = JSON.parse(await readFile("data/manifest.json", "utf8"));
const expected = manifest.files.find((f) => f.path === file).sha256;
const actual = createHash("sha256").update(await readFile(file)).digest("hex");
if (actual !== expected) throw new Error(`${file}: digest mismatch`);

const warsh = await Mushaf.load(file);
console.log(warsh.ayahCount, m.ayah(2, 255).to(warsh).key);  // 6214 2:253-254
Try it live

The same ayah in all seven editions, with each one’s own boundaries and spelling, from the real dataset.

What you have, and what you do not

You now haveYou do not have
Text by surah, ayah, page, juz and word, in seven editionsA picture of the printed page — Quran SVG
One word number shared across all seven editionsTajwīd colouring — Quran Tajweed
Conversion between the seven editions’ ayah numbersConversion between the six scholarly counting systems in the abstract — Qiraat Ayah Map
A search that ignores vowel marksA search that tolerates every typed spelling — see Search the text before you ship one