قصة بناء مكتبة عالية الأداء لعرض المصحف الإلكتروني العالمي
NPM Package ·
Live Demo ·
مثال الإنتاج
البداية: المشكلة الحقيقية
لم تكن المشكلة مجرد "عرض نص على الشاشة"، بل كانت أعمق بكثير. كنت أريد بناء واجهة عرض واحدة (View Layer) قادرة على التعامل مع بيئات وتقنيات مختلفة، وهذا يطرح تحديات هندسية حقيقية:
1. تعدد المصاحف والتخطيطات
لا يقتصر القرآن على شكل واحد؛ فهناك مصاحف مختلفة لكل منها تخطيطه الخاص:
| المصحف | الراوي | المعرف في API | نوع العرض |
| المدينة المنورة V2 | حفص | 1 | رموز رسومية (Glyphs) |
| المدينة المنورة V4 (تجويد) | حفص | 19 | رموز ملونة |
| KFGQPC | حفص | 5 | نص Unicode |
كل مصحف له ملفاته الخاصة، وأنظمة رموز مختلفة، ومتطلبات عرض متباينة.
2. الشمولية عبر المنصات
أردت أن يعمل المكون بنفس الشكل في:
السر كان في فصل "النواة" (Core) عن "العرض" (View).
3. دقة التفاعل "كلمة بكلمة"
الهدف النهائي كان بناء واجهة تفاعلية للمستخدم:
النقر على أي كلمة
الحصول على ترجمتها
إظهار تفسيرها
التنقل بينها بدقة
هذا يتطلب ربط كل كلمة بإحداثياتها، ورقم سطرها، وموقعها في الآية.
الحل: واجهة برمجة تطبيقات مؤسسة القرآن
بعد بحث طويل، وجدت المصدر الأمثل: Quran.com. لمزيد من التفاصيل حول الواجهة، يمكنك زيارة الموقع الرسمي.
لماذا هذه الواجهة تحديداً؟
import { QuranClient } from "@quranjs/api";
const client = new QuranClient({
clientId: process.env.QURAN_CLIENT_ID!,
clientSecret: process.env.QURAN_CLIENT_SECRET!,
});
// جلب بيانات السور
const surahs = await client.chapters.findAll();
// النتيجة: 114 سورة ببيانات كاملة
// جلب بيانات الأجزاء
const juzs = await client.juzs.findAll();
// النتيجة: 30 جزء
ثورة البيانات على مستوى الكلمة
هنا كانت المفاجأة الحقيقية. بدلاً من إرسال الآية كنص واحد، ترجع الواجهة مصفوفة كاملة لكل كلمة:
{
"verses": [
{
"id": 1,
"verseKey": "1:1",
"words": [
{
"id": 1,
"position": 1,
"lineNumber": 2,
"pageNumber": 1,
"text": "بِسْمِ",
"code_v2": "ﱁ",
"charType": "word",
"surah": 1,
"verse": 1
},
{
"id": 2,
"position": 2,
"lineNumber": 2,
"pageNumber": 1,
"text": "ٱللَّهِ",
"code_v2": "﷽",
"charType": "word",
"surah": 1,
"verse": 1
}
]
}
]
}
لاحظ القوة الكامنة:
lineNumber - نعرف أي سطر تظهر فيه الكلمة
code_v2 - الرمز الرسومي للخط العثماني
text - النص العربي كاحتياطي
surah و verse - موقع الكلمة في المصحف
خط أنابيب البيانات Data-pipline: استراتيجية "الجلب مرة واحدة"
بعد التأكد من جودة البيانات، جاء القرار المعماري الأهم:
هل نجعل المكتبة تتصل بالـ API في كل مرة؟
الإجابة: لا.
الاعتماد على الشبكة في وقت التشغيل يعني:
بطء في تحميل الصفحات
عدم العمل دون إنترنت
ضغط على خوادم الـ API
الحل كان: جلب البيانات مرة واحدة وقت البناء، واستخدامها للأبد.
Step 1: جلب البيانات الوصفية
pnpm run generate:metadata
// scripts/fetch-metadata.ts
import { QuranClient } from "@quranjs/api";
async function main() {
const client = new QuranClient({
clientId: process.env.QURAN_CLIENT_ID!,
clientSecret: process.env.QURAN_CLIENT_SECRET!,
});
// جلب السور
const chapters = await client.chapters.findAll();
// → 114 سورة
// جلب الأجزاء
const juzs = await client.juzs.findAll();
// → 30 جزء
}
الناتج:
src/data/metadata/
├── surahs.json # 114 سورة
└── juz.json # 30 جزء
Step 2: جلب بيانات الصفحات
pnpm run generate:pages
// scripts/fetch-pages.ts
const MUSHAF_CONFIGS = [
{
id: 1,
name: "Hafs QCF V2",
wordFields: "code_v2,text_qpc_hafs,line_number,page_number,position",
outputDir: "src/data/pages/hafs-v2",
},
{
id: 19,
name: "Hafs QCF V4 Tajweed",
wordFields: "code_v2,text_qpc_hafs,line_number,page_number,position",
outputDir: "src/data/pages/hafs-v4",
},
{
id: 5,
name: "Hafs Unicode",
wordFields: "text_qpc_hafs,line_number,page_number,position",
outputDir: "src/data/pages/hafs-unicode",
},
];
async function fetchPageData(pageNumber: number, mushafId: number) {
const url = `${API_BASE}/verses/by_page/${pageNumber}?words=true&mushaf=${mushafId}`;
// ...
}
الناتج:
src/data/pages/
├── hafs-v2/pages.json # 604 صفحة
├── hafs-v4/pages.json # 604 صفحة
└── hafs-unicode/pages.json # 604 صفحة
Step 3: تنسيق البيانات الداخلي
أعدت صياغة بيانات الواجهة لتتناسب مع احتياجاتي:
function transformPageData(apiResponse: any, pageNumber: number) {
const linesMap: Record<number, Line> = {};
apiResponse.verses.forEach((verse: any) => {
verse.words.forEach((word: any) => {
if (word.page_number !== pageNumber) return;
const lineNum = word.line_number;
if (!linesMap[lineNum]) {
linesMap[lineNum] = {
lineNumber: lineNum,
words: [],
metadata: {
verseId: verse.id,
verseKey: verse.verse_key,
},
};
}
linesMap[lineNum].words.push({
id: word.id,
position: word.position,
text: word.text_qpc_hafs,
code_v2: word.code_v2,
pageNumber: word.page_number,
charType: word.char_type_name,
surah: verse.chapter_id,
verse: parseInt(verse.verse_key.split(":")[1]),
});
});
});
return Object.values(linesMap).sort((a, b) => a.lineNumber - b.lineNumber);
}
الهيكل النهائي للصفحة:
{
"pageNumber": 1,
"lines": [
{
"lineNumber": 2,
"words": [
{
"id": 1,
"position": 1,
"text": "بِسْمِ",
"code_v2": "ﱁ",
"pageNumber": 1,
"charType": "word",
"surah": 1,
"verse": 1
}
],
"metadata": {
"verseId": 1,
"verseKey": "1:1",
"chapterId": 1
}
}
]
}
Step 4: تحميل الخطوط
pnpm run generate:fonts
// scripts/download-fonts.ts
const FONT_BASE = "https://verses.quran.foundation/fonts/quran/hafs";
async function downloadPageFonts(version: "v2" | "v4") {
const url = `${FONT_BASE}/${version}/woff2/p${page}.woff2`;
// تحميل 604 ملف لكل إصدار
}
مصادر الخطوط:
الناتج:
src/data/fonts/
├── hafs-v2/ # 604 ملف (~25 ميجابايت)
└── hafs-v4/ # 604 ملف (~30 ميجابايت)
مشكلة الخطوط: كابوس الإنتاج
المشكلة
أثناء التطوير، عملت الخطوط بشكل مثالي. لكن بعد النشر، حدث هذا:
GET https://app.com/node_modules/open-quran-view/dist/data/fonts/p1.woff2 404
المشكلة: أدوات البناء الحديثة (Vite، webpack) تعالج import.meta.url بطرق مختلفة حسب البيئة.
الحل: نظام الأصول الثابتة static assets
// src/core/static/fonts.ts (مولد تلقائياً)
export const staticFonts = {
"hafs-v2": {
1: new URL("../../data/fonts/hafs-v2/p1.woff2", import.meta.url).href,
2: new URL("../../data/fonts/hafs-v2/p2.woff2", import.meta.url).href,
// ... 604 إدخال
},
"hafs-v4": { /* ... */ },
} as const;
export function getFontUrl(layout: MushafLayout, page: number): string {
return staticFonts[layout][String(page)];
}
توليد الأصول الثابتة static assets
// scripts/generate-static-fonts.ts
function generateFonts(): string {
let output = `export const staticFonts = {\n`;
const layouts = ["hafs-v2", "hafs-v4", "hafs-unicode"];
for (const layout of layouts) {
const fonts = getAllFonts(join(FONTS_SRC, layout));
output += ` "${layout}": {\n`;
for (const font of fonts) {
const pageNum = font.replace(/^p|\.woff2$/gi, "");
output += ` ${pageNum}: new URL("../../data/fonts/${layout}/${font}", import.meta.url).href,\n`;
}
output += ` },\n`;
}
output += `} as const;\n`;
return output;
}
البنية المعيارية: النواة والعرض
flowchart TB
subgraph Core["النواة (Core)"]
DataLoader["📦 محمل البيانات"]
FontLoader["🎨 محمل الخطوط"]
Search["🔍 البحث"]
DataLoader --> |تخزين مؤقت| Cache["💾 التخزين المؤقت"]
DataLoader --> |جلب| Pages["📄 جلب الصفحات"]
FontLoader --> |روابط ثابتة| FontLinks["🔗 روابط ثابتة"]
FontLoader --> |تحميل| Fonts["⬇️ تحميل الخطوط"]
Search --> |بحث عن| Verse["آية"]
Search --> |الحصول على| Page["صفحة"]
end
subgraph View["العرض (View)"]
React["⚛️ React"]
WebComponent["🌐 عنصر الويب"]
end
Core --> View
classDef core fill:#e1f5fe,stroke:#01579b,stroke-width:2px;
classDef view fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
classDef module fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px;
class Core core
class View view
class DataLoader,FontLoader,Search,React,WebComponent module
النواة (Core)
// src/core/data-loader.ts
export async function loadPage(layout: MushafLayout, page: number): Promise<Page> {
const url = getPagesUrl(layout);
const pages = await fetch(url).then(r => r.json());
return pages[page - 1];
}
// src/core/font-loader.ts
export async function loadFont(layout: MushafLayout, page: number): Promise<void> {
const url = getFontUrl(layout, page);
const fontFace = new FontFace(`p${page}-${layout}`, `url(${url})`);
await fontFace.load();
document.fonts.add(fontFace);
}
العرض (View)
مثال حي: Live Demo
// src/view/react/index.tsx
import { OpenQuranView } from 'open-quran-view/view';
function App() {
return (
<OpenQuranView
page={1}
mushafLayout="hafs-v2"
onWordClick={(word) => {
console.log(`النقرة على: ${word.text}`);
console.log(`السورة: ${word.surah}, الآية: ${word.verse}`);
}}
/>
);
}
<!-- Web Component -->
<open-quran-view
page="1"
mushaf-layout="hafs-v2"
></open-quran-view>
<script type="module">
import { registerOpenQuranView } from 'open-quran-view/view/web';
registerOpenQuranView();
</script>
المصاحف المدعومة
hafs-v2 (الافتراضي)
<OpenQuranView mushafLayout="hafs-v2" />
hafs-v4 (التجويد)
<OpenQuranView mushafLayout="hafs-v4" />
الراوي: حفص عن عاصم
الميزة: ألوان التجويد
الاستخدام: للتعليم
hafs-unicode
<OpenQuranView mushafLayout="hafs-unicode" />
لماذا نحتاج إلى خطوط مختلفة لـ hafs-unicode؟
في مصحف Unicode، لا نستخدم رموز رسومية (glyphs) بل نص عربي عادي. هذا يتطلب استخدام خطوط خارجية لعرض النص بشكل صحيح.
خط DigitalKhat - للقرآن الكريم
الاستخدام: لعرض نص القرآن الكريم نفسه
المميزات:
الاستخدام:
const App = () => (
<OpenQuranView
mushafLayout="hafs-unicode"
textFontFamily="DigitalKhat"
/>
);
مشكلة علامات الآيات (Aya Markers)
التحدي: في نهاية كل آية، يوجد رمز خاص (۩) يشير إلى نهاية الآية. هذا الرمز لا يظهر بشكل صحيح مع الخطوط العربية العادية.
الحل: استخدام خط AyaMarker لعرض علامة الآية فقط.
ملاحظة: علامة الآية جزء من معيار Unicode (U+06D9) ويجب معاملتها كحرف عربي عادي.
خط AyaMarker - لعلامات الآيات فقط
الاستخدام: لعرض علامة نهاية الآية (۩) فقط، وليس لنص القرآن
المميزات:
يعرض علامة نهاية الآية (۩) بشكل واضح
متوافق مع خطوط Unicode العربية
حل مثالي للتطبيقات التي تحتاج دقة عالية
الاستخدام:
const App = () => (
<OpenQuranView
mushafLayout="hafs-unicode"
textFontFamily="DigitalKhat"
markerFontFamily="AyaMarker"
/>
);
أي خط تختار؟
| الوظيفة | الخط | الوصف |
| نص القرآن | DigitalKhat | للقراءة اليومية |
| علامة الآية | AyaMarker | لتوضيح حدود الآيات |
التوصية
للتطبيقات البسيطة: DigitalKhat فقط
للتطبيقات التعليمية: DigitalKhat + AyaMarker لتوضيح حدود الآيات
للتطبيقات المتقدمة: كلا الخطين مع إمكانية التبديل
قابلية التوسع: التفسيرات والترجمات
بناء دعم التفسير
<OpenQuranView
page={1}
onWordClick={async (word) => {
const tafsir = await fetch(
`/verses/${word.verseId}/tafsirs?mushaf=${mushafId}`
).then(r => r.json());
showTafsirModal(tafsir);
}}
/>
بناء دعم الترجمة
// الترجمات المتاحة
const translations = [
{ id: 20, name: "Sahih International" },
{ id: 161, name: "Muhammad Taqi Usmani" },
];
async function getVerseWithTranslation(verseKey: string, translationId: number) {
const response = await fetch(
`/verses/by_key/${verseKey}?translations=${translationId}`
);
return response.json();
}
النشر والإنتاج
التحدي: السكريبت التلقائي
حللت مشكلة "المطور يثبت الحزمة ويحتاج البيانات" باستخدام سكريبت prepare:
{
"scripts": {
"prepare": "pnpm run generate:all && pnpm run build"
}
}
عند تثبيت الحزمة:
npm install open-quran-view
تحميل الحزمة
تشغيل prepare
جلب البيانات (metadata + pages + fonts)
توليد الأصول الثابتة
بناء TypeScript
بنية الناتج النهائي
dist/
├── core/
│ ├── static/
│ │ ├── fonts.ts # 1200+ رابط ثابت
│ │ └── data.ts # روابط البيانات
│ ├── data-loader.ts
│ └── font-loader.ts
├── data/
│ ├── fonts/
│ │ ├── hafs-v2/ # 604 ملفات
│ │ └── hafs-v4/ # 604 ملفات
│ ├── pages/
│ │ ├── hafs-v2/pages.json
│ │ └── hafs-v4/pages.json
│ └── metadata/
│ ├── surahs.json
│ └── juz.json
└── view/
├── react/ # مكون React
└── web/ # عنصر الويب
الدروس المستفادة
1. اختر مصدر البيانات بحكمة
Quran Foundation API أعطاني كل ما أحتاج:
بيانات هيكلية دقيقة
دعم مصاحف متعددة
تحديثات مستمرة
2. افصل النواة عن العرض
جعل إضافة React و Web Components عملاً تافهاً:
// النواة - ليست للتصدير العام (للاستخدام الداخلي فقط)
// ./src/core/*
// العرض - هذا هو الجزء المُصدَّر فقط
import { OpenQuranView } from 'open-quran-view/view/react';
3. البيانات على مستوى الكلمة = ميزات لا نهائية
كل ميزة بنيت فوق هذه البيانات:
النقر على الكلمات
إظهار التفسير
عرض الترجمات
التنقل الدقيق
4. الأصول الثابتة static assets تحل مشاكل الإنتاج
import.meta.url يعمل في التطوير، لكنه يكسر في الإنتاج. الحل: توليد الروابط ثابتاً في وقت البناء.
الإحصائيات
| المقياس | القيمة |
| إجمالي صفحات المصحف | 604 |
| إجمالي السور | 114 |
| إجمالي الأجزاء | 30 |
| السطور في كل صفحة | 15 |
| المصاحف المدعومة | 3 |
| ملفات الخطوط | 1,208 |
| حجم البيانات الإجمالي | 120 ميجابايت |
الخلاصة
رحلة بناء Open Quran View علمتني أن عرض القرآن ليس مجرد "نص على الشاشة". إنه نظام معقد يتطلب:
مصدر بيانات موثوق - Quran Foundation API
بنية معيارية - فصل النواة عن العرض
تخطيط للمستقبل - دعم مصاحف متعددة
حلول إنتاجية - الأصول الثابتة للتوزيع
المكتبة الآن تدعم React و Web Components، وتعمل بدون إنترنت، وتعرض المصحف بدقة مطبوعة.
روابط مفيدة
التوثيق المستمر
قسم كبير من نجاح أي مشروع برمجي هو توثيقه بشكل مستمر ومنتظم. في Open Quran View، التزمت بتوثيق كل خطوة من خطوات التطوير:
فلسفة التوثيق
كل تغيير أو إضافة جديدة تأتي مصحوبة بتوثيقها في نفس الوقت. هذا يضمن أن الوثائق تبقى محدثة دائماً ومواكبة للرماز الفعلي.
هيكل الوثائق
docs/
├── index.md # الصفحة الرئيسية
├── api/
│ └── views.md # مرجع واجهة البرمجة
├── architecture/
│ ├── open-quran-view-journey.md # قصة المشروع
│ ├── data-structure.md # هياكل البيانات
│ └── static-assets.md # الأصول الثابتة
└── guides/
├── api-integration.md # دمج Quran Foundation API
├── data-generation.md # توليد البيانات
├── font-loading.md # استراتيجيات الخطوط
└── mushaf-comparison.md # مقارنة المصاحف
التزام التحديث
كل دالة أو مكون جديد يُكتب، يُوثَّق في نفس اللحظة. هذا يمنع حدوث فجوة بين ما يفعله الكود وما تصفه الوثائق.
المستخدمون والمطورون يستطيعون دائماً الاعتماد على أن أن الوثائق تعكس آخر إصدار من المشروع.
التوثيق كمرجع للذكاء الاصطناعي
الوثائق المكتوبة بوضوح تُعد مرجعاً قيّماً للذكاء الاصطناعي. عند العمل مع نماذج اللغة المختلفة، يمكن استخدامها كسياق أساسي لفهم بنية المشروع واتخاذ قرارات صحيحة.
فوائد الوثائق للذكاء الاصطناعي:
فهم بنية المشروع بسرعة
التعرف على المصطلحات المستخدمة
معرفة مكان كل ملف ووظيفته
تتبع التغييرات والتحديثات
إنتاج رماز متسق مع النمط الموجود
المطورون يمكنهم استخدام هذه الوثائق مع أي ذكاء اصطناعي لمساعدتهم في:
إضافة ميزات جديدة متسقة مع البنية القائمة
إصلاح الأخطاء بسرعة أكبر
فهم الكود المعقد دون قراءة كل سطر
إنشاء اختبارات ووحدات جديدة
توثيق أكوادهم الخاصة بنفس الأسلوب
هذه الوثائق تُشبه "الخرائط الذهنية" التي يرجع إليها الذكاء الاصطناعي عند الحاجة.
صور توضيحية من التطبيق الفعلي
واجهة العرض
المثال الحي: open-quran-view-react.netlify.app

واجهة Web Component
مثال الإنتاج: github.com/adelpro/open-quran-view-react
المقارنة بين المصاحف



الصور أعلاه توضح كيف يظهر التطبيق في الواقع.
الخاتمة
Open Quran View هو مشروع مفتوح المصدر يهدف إلى توفير مكتبة عرض Quran سهلة الاستخدام وقابلة للتوسع. من خلال التصميم المعياري، يمكن للمطورين إضافة ميزات جديدة دون تعديل النواة، ويمكن دعم منصات جديدة بسهولة.
المميزات الرئيسية
- واجهات متعددة: React، Web Components، React Native
- دعم المصاحف المختلفة: المدينة المنورة، KFGQPC، Unicode
- أداء عالي: تحميل كسول للصور، تخزين مؤقت للبيانات
- قابل للتخصيص: خطوط، ألوان، موضوعات
المساهمون والمختبرون
المختبرون 🧪
شكر خاص للمختبرين الذين ساهموا في تحسين جودة المشروع:
- [اسم المختبر](رابط الملف الشخصي)
إذا كنت تريد أن تُذكر كمختبر، يرجى فتح issue في المستودع.
المساهمون 🤝
نرحب بمساهمتكم! للمشاركة في المشروع:
- انسخ المستودع (
git clone)
- أنشئ فرعاً للميزة (
git checkout -b feature/amazing)
- commit (
git commit -m 'feat: ميزة جديدة')
- push (
git push origin feature/amazing)
- افتح Pull Request
المستودع: github.com/adelpro/open-quran-view