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.
Install
The JavaScript package is named
quran-textin itspackage.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-textNot on the registry at the commit this page was written againstAt
quran-ws/quran-text@fea25cdnothing is published to npm, PyPI, Packagist, pub.dev or Maven Central, and the repository is private. The package name above is the one inlib/js/package.json, and publishing is on the launch list. Until it lands,npm install ./lib/jsfrom 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.
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 textis the words joined with spaces and nothing else.renderadds 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.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-facerule 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.jsonand 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 dataThree 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.lengthsays the string is intact. Ship the font each edition names in its ownfontblock — seven fonts for seven editions, not one.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 throwIt 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 readrelation; never carry a bare number across editions.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.jsonlists 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 manifestlib/js/data/hafs.json— the copyMushaf.hafs()loads — isdata/mushaf/hafs.jsonwith its whitespace removed. It parses to identical data, but its bytes hash to66430ff0…, and the manifest lists only the canonical file at5b104f7d…. So the manifest check above works for editions you download, not for the bundled one. Recorded as a finding indocs/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 liveThe 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 have | You do not have |
|---|---|
| Text by surah, ayah, page, juz and word, in seven editions | A picture of the printed page — Quran SVG |
| One word number shared across all seven editions | Tajwīd colouring — Quran Tajweed |
| Conversion between the seven editions’ ayah numbers | Conversion between the six scholarly counting systems in the abstract — Qiraat Ayah Map |
| A search that ignores vowel marks | A search that tolerates every typed spelling — see Search the text before you ship one |