Build a Quran Page Viewer with Next.js
This post walks you through building a page-by-page Quran viewer using Next.js App Router and the free UmmahAPI. You get server-side rendering, clean Arabic typography, and working page navigation with very little code.
What You Are Building
The app will have a single dynamic route: /quran/page/[pageNum]. When a user visits that URL, Next.js fetches the ayahs for that mushaf page on the server and renders them. Users can click Next and Previous to move between pages. The Quran has 604 pages total, so the page number stays between 1 and 604.
The data comes from the UmmahAPI Quran endpoint. No client-side fetching, no loading spinners for the main content. The page is fully rendered before it reaches the browser.
Project Setup
Start a new Next.js project. Use the App Router, which is the default in Next.js 14 and later.
npx create-next-app@latest quran-viewer --app --ts --tailwind --eslint
cd quran-viewer
You do not need any extra packages for the API calls. Next.js has fetch built in with caching options. If you want to skip Tailwind and use plain CSS, that is fine too. The logic is the same either way.
Your folder structure will look like this:
app/
quran/
page/
[pageNum]/
page.tsx // server component, fetches data
loading.tsx // optional loading skeleton
layout.tsx
page.tsx // home page, redirect to page 1
Fetching a Quran Page
The UmmahAPI endpoint for a mushaf page is GET /api/quran/page/:page. It returns all the ayahs on that page. You can also pass ?script=uthmani for the Uthmani script, which looks better for display.
Here is what a raw fetch looks like. You can test this in your browser or with curl right now:
https://ummahapi.com/api/quran/page/1?script=uthmani
Now add a helper function inside your app. Create lib/quran.ts:
const BASE = 'https://ummahapi.com';
export async function getQuranPage(page: number) {
const res = await fetch(
`${BASE}/api/quran/page/${page}?script=uthmani`,
{ next: { revalidate: 86400 } } // cache for 24 hours
);
if (!res.ok) {
throw new Error(`Failed to fetch page ${page}`);
}
return res.json();
}
The revalidate: 86400 option tells Next.js to cache this response for 24 hours. Quran text does not change, so you could set this much higher or use force-cache to cache indefinitely. This saves API calls and makes your app faster.
Tip. Get a free API key at ummahapi.com/register and pass it as an x-api-key header. Without a key you get 5,000 requests per 15 minutes, which is fine for development. With a key the limit is removed.
The Page Component
Create app/quran/page/[pageNum]/page.tsx. This is a server component by default in the App Router, so the fetch runs on the server.
import { getQuranPage } from '@/lib/quran';
import Link from 'next/link';
import { notFound } from 'next/navigation';
interface Props {
params: { pageNum: string };
}
export default async function QuranPage({ params }: Props) {
const pageNum = parseInt(params.pageNum, 10);
if (isNaN(pageNum) || pageNum < 1 || pageNum > 604) {
notFound();
}
const data = await getQuranPage(pageNum);
const ayahs = data.ayahs ?? [];
const prevPage = pageNum > 1 ? pageNum - 1 : null;
const nextPage = pageNum < 604 ? pageNum + 1 : null;
return (
<main className="quran-page">
<div className="page-header">
<h1>Page {pageNum} of 604</h1>
<nav className="page-nav" aria-label="Quran page navigation">
{prevPage ? (
<Link href={`/quran/page/${prevPage}`}>← Previous</Link>
) : (
<span className="disabled">← Previous</span>
)}
<span>Page {pageNum}</span>
{nextPage ? (
<Link href={`/quran/page/${nextPage}`}>Next →</Link>
) : (
<span className="disabled">Next →</span>
)}
</nav>
</div>
<div className="ayahs" dir="rtl" lang="ar">
{ayahs.map((ayah: any) => (
<p key={ayah.number} className="ayah">
{ayah.text} <span className="ayah-num">({ayah.numberInSurah})</span>
</p>
))}
</div>
</main>
);
}
Notice dir="rtl" and lang="ar" on the ayahs container. This is important for correct text direction. Arabic is right-to-left, so without these attributes the text can display incorrectly in some browsers.
Generating Static Pages with generateStaticParams
If you want to pre-render all 604 pages at build time, add generateStaticParams to the same file. This is optional but makes navigation instant because each page is already built as static HTML.
export async function generateStaticParams() {
return Array.from({ length: 604 }, (_, i) => ({
pageNum: String(i + 1),
}));
}
export async function generateMetadata({ params }: Props) {
const pageNum = parseInt(params.pageNum, 10);
return {
title: `Quran Page ${pageNum} | Quran Viewer`,
description: `Read Quran mushaf page ${pageNum} of 604.`,
};
}
The generateMetadata function adds a unique title and description to each page. Search engines see different meta tags for page 1, page 2, and so on. This is one of the main reasons to use Next.js for this kind of app.
Tip. If 604 static pages is too many for your build budget, remove generateStaticParams and set the dynamic export to 'force-dynamic' instead. Pages will be server-rendered on demand and cached after the first request.
Adding Arabic Typography
Good Arabic typography makes the viewer actually usable. The Amiri font from Google Fonts works well for Quran text. Add it to your app/layout.tsx:
import { Amiri } from 'next/font/google';
const amiri = Amiri({
subsets: ['arabic', 'latin'],
weight: ['400', '700'],
display: 'swap',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={amiri.variable}>{children}</body>
</html>
);
}
Then in your CSS, apply the font to the Arabic text container:
.ayahs {
font-family: var(--font-amiri), 'Amiri', serif;
font-size: 1.5rem;
line-height: 2.6;
text-align: right;
direction: rtl;
color: #1a1a1a;
}
.ayah {
margin-bottom: 0.5rem;
}
.ayah-num {
font-size: 0.85rem;
opacity: 0.5;
font-family: sans-serif;
direction: ltr;
display: inline-block;
}
Keep the line height around 2.4 to 2.8. Arabic text with diacritics (tashkeel) needs more vertical space than Latin text. Too little line height makes it look crowded and hard to read.
Adding a Jump-to-Page Form
Users should be able to jump to any page directly. Since navigation is a user action, this part needs a client component. Create components/PageJumper.tsx:
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export default function PageJumper({ current }: { current: number }) {
const router = useRouter();
const [value, setValue] = useState(String(current));
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const page = parseInt(value, 10);
if (page >= 1 && page <= 604) {
router.push(`/quran/page/${page}`);
}
}
return (
<form onSubmit={handleSubmit} className="page-jumper">
<label htmlFor="page-input">Go to page</label>
<input
id="page-input"
type="number"
min="1"
max="604"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button type="submit">Go</button>
</form>
);
}
Import this into your server component and pass the current page number. The form uses router.push to navigate, which triggers the server component to fetch the new page data.
Showing Surah Names at the Top of Each Page
The API response includes surah information with each ayah. A page can span more than one surah, so you need to find the unique surahs on the page and display them as a header. Here is how to pull that out of the ayahs array:
// inside your server component, after fetching data
const surahsOnPage = Array.from(
new Map(
ayahs.map((a: any) => [a.surah.number, a.surah.englishName])
).entries()
);
// then in JSX
<div className="surah-labels">
{surahsOnPage.map(([num, name]) => (
<span key={num} className="surah-label">
{name} ({num})
</span>
))}
</div>
This gives you a clean header that shows which surahs appear on the current page. It is especially useful when a page starts in the middle of a surah.
Adding a Translation Toggle
The UmmahAPI supports multiple translations via the ?translations=en query parameter. You can fetch the same page with an English translation alongside the Arabic.
Update your fetch function to accept a translations option:
export async function getQuranPage(page: number, translations = 'en') {
const params = new URLSearchParams({
script: 'uthmani',
translations,
});
const res = await fetch(
`https://ummahapi.com/api/quran/page/${page}?${params}`,
{ next: { revalidate: 86400 } }
);
if (!res.ok) throw new Error('Fetch failed');
return res.json();
}
You can pass translations=en,ur to get both English and Urdu translations in one request. Each ayah in the response will have a translations array with the text for each language you requested.
To let users switch languages without a full page reload, use a search param in the URL. Next.js App Router reads searchParams as a prop in server components, so you can pass the language preference through the URL cleanly.
// In your page component props
interface Props {
params: { pageNum: string };
searchParams: { lang?: string };
}
export default async function QuranPage({ params, searchParams }: Props) {
const lang = searchParams.lang ?? 'en';
const data = await getQuranPage(pageNum, lang);
// rest of the component...
}
A link like /quran/page/1?lang=ur will now fetch and display the Urdu translation. You can build a simple dropdown that generates these links. Because it changes the URL, the browser back button works correctly.
For more on what the Quran API returns, check the full API docs or the Quran API landing page.
FAQ
Which UmmahAPI endpoint gives Quran page data?
Use GET /api/quran/page/:page where the page number is between 1 and 604. The response includes all ayahs on that mushaf page along with surah info and the text in your chosen script.
Does Next.js App Router work well for a Quran page viewer?
Yes. You can use dynamic route segments like /quran/page/[pageNum] and fetch data in a server component. Every page is server-rendered, which means fast load times and good SEO without extra configuration. Pair it with generateStaticParams to pre-build all 604 pages at deploy time.
How do I display Arabic text correctly in a Next.js app?
Set dir="rtl" and lang="ar" on the Arabic text container. Load a proper Arabic font like Amiri from Google Fonts using next/font/google. The UmmahAPI supports the uthmani and indopak scripts via the ?script query parameter, so you can choose the one that fits your design.
Can I add audio recitation to the Quran page viewer?
The UmmahAPI supports a ?reciter query parameter. Available reciters include alafasy, sudais, abdul_basit, husary, and others. Pass the reciter name in your fetch URL and the API returns audio URLs you can attach to an HTML audio element or a custom audio player component.
Ready to build your own Quran app?
UmmahAPI is free and gives you Quran pages, translations, tafsir, prayer times, and more in one API.
Read the docs