When I shipped Tilawa's first public version, it was ready in the technical sense — the features worked, there were no console errors, and it loaded fast. But "ready" and "launch-ready" are different things. There's a gap between a working app and one that gets indexed by Google, renders correctly when shared on WhatsApp, and doesn't confuse search engines.

Here's everything I did before the final push to production.

ISR strategy for 114 Surah pages. Each Surah page at /surah/[id] is dynamically rendered — it fetches data from the Al-Quran Cloud API at request time. Without caching, every visit to any Surah page would make a real API request. I added next: { revalidate: 86400 } to every fetch call — this tells Next.js to cache the response for 24 hours on the edge, and regenerate it once per day. The Quran text doesn't change, so 24 hours is conservative; I could set it to a week or a month. But 24 hours feels right for a live app.

Dynamic metadata per Surah page. Every page has a unique <title> and <meta description>:

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const surah = await getSurah(Number(params.id));
  return {
    title: `Surah ${surah.englishName} (${surah.name}) — Tilawa`,
    description: `Read and listen to Surah ${surah.englishName} (${surah.englishNameTranslation}) — ${surah.numberOfAyahs} Ayahs. English and Bangla translations with audio recitation by world-class reciters.`,
  };
}

Open Graph image. I created a single 1200×630 OG image (/public/og-image.png) with the Tilawa logo, app name, and tagline. It's the same image for every page — for a v1, that's acceptable. A better version would generate dynamic OG images per Surah using next/og, but that adds complexity I didn't need at launch.

Sitemap. The dynamic XML sitemap at /sitemap.xml is generated by src/app/sitemap.ts:

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const surahs = await getSurahList();
  const surahUrls = surahs.map((s) => ({
    url: `https://tilawa-app.vercel.app/surah/${s.number}`,
    lastModified: new Date(),
    changeFrequency: 'monthly' as const,
    priority: 0.8,
  }));
  return [
    { url: 'https://tilawa-app.vercel.app', priority: 1 },
    { url: 'https://tilawa-app.vercel.app/dua-remembrance', priority: 0.7 },
    ...surahUrls,
  ];
}

This generates 116 URLs (homepage, du'a page, 114 Surahs) automatically.

robots.txt. A simple src/app/robots.ts that allows all crawlers and points to the sitemap.

Vercel deployment. Connect the GitHub repo to Vercel, set framework to Next.js, deploy. Took about 90 seconds. The custom domain wasn't set up for launch — tilawa-app.vercel.app was fine for a public beta.

What I'd do differently next time. One: generate dynamic OG images per Surah using @vercel/og so shares of individual Surahs on social media show the Surah name. Two: add PWA support (a manifest.json and a service worker) so it can be installed to the home screen and cache Surah data for offline reading. Three: add a reading statistics page — how many Ayahs read this week, streak tracking, Juz completion percentage. The infrastructure for this already exists in the last-read localStorage data; it just needs a UI.

The app is live. Go try it at tilawa-app.vercel.app. Read Surah Al-Mulk. Then come back here and tell me the audio doesn't feel right at 0.75× speed with Maher Al-Muaiqly.

Mustafa Kamal Hossain

Mustafa Kamal Hossain

Founder & Principal Engineer at Manfi. Passionate about Laravel, SaaS architecture, and high-performance engineering.