kreftjs 0.0.19-beta → 0.0.20-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,250 @@
1
+ # الفصل الثامن: العمل في الخلفية: الطوابير والبريد والذاكرة المؤقتة والجدولة
2
+
3
+ لكل أداة مجلدها في `backend/app/` وأمر `kreft make:` يولّد ملفها:
4
+
5
+ ```js
6
+ await dispatch(new SendWelcome(user)) // app/Jobs: يعمل في الخلفية (محاولات، وانتظار، وfailed())
7
+ await event(new OrderShipped(order)) // app/Listeners
8
+ await Mail.to(user).send(new WelcomeMail(user)) // app/Mail: موضوع، نص، html أو markdown({ … })
9
+ await user.notify(new InvoicePaid(invoice)) // app/Notifications: قنوات 'mail' و 'database'
10
+ await Cache.remember('stats', 600, () => loadStats()) // قاعدة بيانات أو ملف أو ذاكرة أو redis (CACHE_STORE)
11
+ const path = await req.file('avatar').store('avatars') // الرفع؛ Storage.url(path) هو /storage/avatars/…
12
+ ```
13
+
14
+ ## المهام (Jobs)
15
+
16
+ ```bash
17
+ kreft make:job SendReport
18
+ ```
19
+
20
+ ```js
21
+ // app/Jobs/SendReport.js
22
+ const { Job } = require('kreftjs/server')
23
+
24
+ class SendReport extends Job {
25
+ static tries = 3 // أعد المحاولة بعد خطأ، حتى 3 مرات
26
+ static backoff = 10 // ثوانٍ بين المحاولات
27
+
28
+ constructor(data) {
29
+ super()
30
+ this.data = data // النماذج تُخزَّن كمعرّف وتُقرأ من جديد عند التشغيل
31
+ }
32
+
33
+ async handle() {
34
+ // العمل نفسه
35
+ }
36
+
37
+ failed(error) {
38
+ // بعد آخر محاولة
39
+ }
40
+ }
41
+
42
+ module.exports = SendReport
43
+ ```
44
+
45
+ `Job.dispatch(...)` يعيد كائنًا يمكن تسلسل دوال عليه (`delay(seconds)` و`onQueue(name)`) ثم `await`:
46
+
47
+ ```js
48
+ await SendReport.dispatch(user)
49
+ await SendReport.dispatch(user).delay(60).onQueue('mail')
50
+ ```
51
+
52
+ > **تحذير:** `dispatch` يرسل المهمة عندما تكتب `await` (أو `.then`). استدعاؤه ونسيان `await` لا يرسل شيئًا.
53
+
54
+ المهام والمستمعون والبريد والإشعارات المضافة للطابور تعمل في الخلفية. `kreft dev` و`kreft serve` يعالجان جدول `jobs` كل ثانية. وللتحكم بالعمّال: `kreft queue:work` و`queue:failed` و`queue:retry all` و`queue:flush`. اختر المشغّل بـ `QUEUE_CONNECTION` (`database` أو `redis` أو `sync`).
55
+
56
+ - **`sync`**: تشغيل فوري في نفس الطلب. ممتاز للاختبارات.
57
+ - **`database`**: جدول `jobs`. لا شيء إضافي.
58
+ - **`redis`**: الأسرع، ويدعم التأخير. يلزم `REDIS_URL`.
59
+
60
+ ## الأحداث والمستمعون
61
+
62
+ ```js
63
+ // app/Events/OrderPlaced.js
64
+ class OrderPlaced {
65
+ constructor(order) { this.order = order }
66
+ }
67
+ module.exports = OrderPlaced
68
+ ```
69
+
70
+ ```js
71
+ // app/Listeners/LogOrder.js
72
+ class LogOrder {
73
+ static event = 'OrderPlaced' // كل مستمع له static event
74
+ // static queue = true // شغّله لاحقًا على الطابور
75
+
76
+ async handle(event) {
77
+ console.log(`[order] #${event.order.id}`)
78
+ }
79
+ }
80
+ module.exports = LogOrder
81
+ ```
82
+
83
+ ```js
84
+ await event(new OrderPlaced(order)) // يعمل كل المستمعين لهذا الحدث
85
+ ```
86
+
87
+ الأحداث تفصل "ما حدث" عن "ماذا نفعل بعد ذلك". أضف مستمعًا جديدًا (إرسال رسالة، تحديث إحصاء) دون أن تلمس شيفرة الطلب.
88
+
89
+ ## البريد
90
+
91
+ ```bash
92
+ kreft make:mail OrderReceipt
93
+ ```
94
+
95
+ ```js
96
+ // app/Mail/OrderReceipt.js
97
+ const { Mailable } = require('kreftjs/server')
98
+
99
+ class OrderReceipt extends Mailable {
100
+ constructor(order) {
101
+ super()
102
+ this.order = order
103
+ }
104
+
105
+ build() {
106
+ return this.subject(`إيصال الطلب #${this.order.id}`).markdown({
107
+ greeting: 'شكرًا لطلبك',
108
+ lines: [`المجموع: ${(this.order.total_cents / 100).toFixed(2)} $`],
109
+ action: { text: 'افتح التطبيق', url: process.env.APP_URL || '/' },
110
+ })
111
+ }
112
+ }
113
+ module.exports = OrderReceipt
114
+ ```
115
+
116
+ ```js
117
+ await Mail.to(user.email).send(new OrderReceipt(order)) // الآن
118
+ await Mail.to(user.email).queue(new OrderReceipt(order)) // على الطابور
119
+ await Mail.to(user.email).later(600, new OrderReceipt(order)) // بعد 10 دقائق
120
+ ```
121
+
122
+ > **ملاحظة:** حتى تضبط `MAIL_URL` في `.env` تُطبع الرسائل في الطرفية بدل أن تُرسل. وهذا مناسب جدًا للتطوير: ترى الرسالة التي كانت ستصل.
123
+
124
+ ## الذاكرة المؤقتة (Cache)
125
+
126
+ ```js
127
+ const stats = await Cache.remember('stats', 600, () => loadStats()) // احسب مرة كل 10 دقائق
128
+ await Cache.put('k', 'v', 60)
129
+ await Cache.get('k')
130
+ await Cache.forget('k')
131
+ ```
132
+
133
+ متى تُبطل الذاكرة؟ عندما يتغير ما تعتمد عليه:
134
+
135
+ ```js
136
+ async store(req) {
137
+ const product = await Product.create(await req.validate({ name: 'required' }))
138
+ await Cache.forget('products:list') // أبطل القائمة المؤقتة
139
+ return product
140
+ }
141
+ ```
142
+
143
+ المخازن: `database` أو `file` أو `memory` أو `redis`، بحسب `CACHE_STORE`. `memory` تخص العملية الواحدة؛ لعدة خوادم استعمل `database` أو `redis`.
144
+
145
+ ### الأقفال (Locks)
146
+
147
+ القفل الذري يضمن أن عملية واحدة فقط تدخل المقطع الحرج:
148
+
149
+ ```js
150
+ // أسلوب الدالة: يُحرَّر القفل وحده
151
+ await Cache.lock('checkout:' + key, 15, async () => {
152
+ // مقطع لا يدخله إلا واحد في كل مرة (تنتهي صلاحية القفل بعد 15 ثانية)
153
+ })
154
+
155
+ // أسلوب الكائن
156
+ const lock = await Cache.lock('report', 30)
157
+ if (lock) {
158
+ try { /* العمل */ } finally { await lock.release() }
159
+ }
160
+
161
+ // انتظر حتى 5 ثوانٍ للحصول عليه
162
+ await Cache.lock('report', 30).block(5, async () => { /* … */ })
163
+ ```
164
+
165
+ استعمالها الكلاسيكي: منع النقر المزدوج على "ادفع" من إنشاء طلبين (انظر الفصل الثاني عشر).
166
+
167
+ ## الملفات المرفوعة
168
+
169
+ ```js
170
+ const path = await req.file('avatar').store('avatars')
171
+ ```
172
+
173
+ يحفظ باسم عشوائي ولا يثق بالاسم الأصلي أبدًا. فقط الامتدادات المعروفة (صور، PDF، نص، مكتب، صوت، فيديو، أرشيفات) تحتفظ بامتدادها؛ وغيرها يُخزَّن `.bin`. استعمل `file.isImage()` لتفحص ترويسة الملف الحقيقية لا اسمه فقط.
174
+
175
+ الملفات في القرص العام تُخدَّم من `/storage/…` كتنزيلات بيانات خام، فلا يمكن لـ `.html` أو `.svg` مرفوع أن يشغّل سكربتات في موقعك.
176
+
177
+ ```js
178
+ const url = Storage.url(path) // /storage/avatars/xxxx.png
179
+ ```
180
+
181
+ ## الجدولة (Scheduling)
182
+
183
+ ```js
184
+ // backend/routes/console.js
185
+ Schedule.call(async () => { await Post.onlyTrashed().forceDelete() }).daily().name('clean-trash')
186
+ Schedule.job(new SendReport()).weekdays().at('08:00')
187
+ Schedule.command('send:report Daily').hourly()
188
+ ```
189
+
190
+ `kreft dev` و`kreft serve` يفحصان الجدول كل دقيقة. اضبط `SCHEDULER=off` لتشغّله من cron بـ `kreft schedule:run`؛ و`kreft schedule:list` يعرض ما هو مخطط.
191
+
192
+ ## Redis
193
+
194
+ لدى Kreft عميل Redis خاص به (RESP2 عبر TCP أو TLS) بلا حزمة إضافية. يعمل مع Redis وValkey وخدمات Redis المستضافة.
195
+
196
+ ```env
197
+ CACHE_STORE=redis
198
+ QUEUE_CONNECTION=redis
199
+ REDIS_URL=rediss://default:secret@host:6379/0 # أو REDIS_HOST وREDIS_PORT وREDIS_USERNAME وREDIS_PASSWORD وREDIS_DB
200
+ REDIS_PREFIX=kreft_
201
+ ```
202
+
203
+ ```bash
204
+ kreft redis:ping # PONG (1ms) — 127.0.0.1:6379/0
205
+ ```
206
+
207
+ Redis يعطيك أيضًا أقفالًا ذرية (`Cache.lock('report', 30)`) والنشر والاشتراك (publish/subscribe).
208
+
209
+ ## الإعدادات
210
+
211
+ الإعدادات تأتي من `.env` و`backend/config/*.js` والقيم الافتراضية عبر مساعد واحد:
212
+
213
+ ```js
214
+ const { config } = require('kreftjs/server')
215
+
216
+ config('app.name', 'My app')
217
+ config('database.default', 'sqlite')
218
+ config.has('redis.connections.default')
219
+ config.set('app.debug', false)
220
+ ```
221
+
222
+ ## الجلسات وعدم النشاط
223
+
224
+ طبقتان تخرجان المستخدم الغائب:
225
+
226
+ ```env
227
+ SESSION_LIFETIME=120 # دقائق خمول قبل أن يُسقط الخادم الجلسة
228
+ PUBLIC_SESSION_TIMEOUT=15 # دقائق قبل أن ترسل الصفحة نفسها المستخدم إلى /login
229
+ PUBLIC_SESSION_WARNING=2 # دقائق تحذير قبل ذلك
230
+ AUTH_TOKEN_DAYS=30 # رمز لم يُستعمل هذه الأيام يتوقف
231
+ ```
232
+
233
+ قيم `PUBLIC_` تُنسخ إلى المتصفح (`Kreftjs.env`)؛ لا تضع فيها أسرارًا أبدًا.
234
+
235
+ ## خلف وسيط (Proxy)
236
+
237
+ عنوان الزائر يأتي من الاتصال. خلف nginx أو Cloudflare أو موزّع أحمال اذكر وسطاءك:
238
+
239
+ ```env
240
+ TRUSTED_PROXIES=10.0.0.1 # أو *
241
+ TRUSTED_PROXY_HOPS=1 # مع *: كم وسيطًا أمامك
242
+ ```
243
+
244
+ `X-Forwarded-For` يُقرأ من اليمين، فلا يستطيع الزائر اختيار عنوانه بنفسه.
245
+
246
+ ## تمارين
247
+
248
+ 1. اجعل تسجيل مستخدم جديد يطلق حدثًا `UserRegistered` له مستمعان: واحد يرسل بريد ترحيب على الطابور، وآخر يزيد عدّادًا في الذاكرة المؤقتة.
249
+ 2. اكتب مهمة تحذف الملفات المؤقتة وجدولها كل ليلة الساعة 03:00.
250
+ 3. استعمل `Cache.lock` لمنع تشغيل تقرير ثقيل مرتين في آن واحد.
@@ -0,0 +1,234 @@
1
+ # الفصل التاسع: الاختبار والبناء والنشر
2
+
3
+ ## اختبار الواجهة الخلفية
4
+
5
+ تُختبر شيفرة الخادم بمشغّل الاختبارات المدمج في Node وقاعدة SQLite في الذاكرة، فالاختبارات سريعة ولا تمس بياناتك الحقيقية.
6
+
7
+ ```json
8
+ {
9
+ "scripts": { "test": "node --test \"tests/**/*.test.js\"" }
10
+ }
11
+ ```
12
+
13
+ ```js
14
+ // tests/models/post.test.js
15
+ const test = require('node:test')
16
+ const assert = require('node:assert/strict')
17
+ const { createSchema, setConnection } = require('kreftjs/server')
18
+ const { connect } = require('kreft/bin/db.js')
19
+ const Post = require('../../backend/app/Models/Post')
20
+
21
+ test.before(async () => {
22
+ const db = await connect('sqlite::memory:') // قاعدة فارغة جديدة لهذا الملف
23
+ setConnection(db)
24
+ await createSchema(db).create('posts', (table) => {
25
+ table.id()
26
+ table.string('title')
27
+ table.timestamps()
28
+ })
29
+ })
30
+
31
+ test('a post can be created and found', async () => {
32
+ const post = await Post.create({ title: 'Hello' })
33
+ assert.equal((await Post.find(post.id)).title, 'Hello')
34
+ })
35
+ ```
36
+
37
+ لاختبار جداول مشروع كامل شغّل ترحيلاته الحقيقية بدل بناء المخطط يدويًا: `migrator.migrate(db, 'backend/database/migrations')`.
38
+
39
+ اختبارات المكوّنات للمتصفح تعيش في `frontend/tests/` وتُفتح بـ `kreft dev`.
40
+
41
+ ## الأدوات المرافقة للتطوير
42
+
43
+ كل ما هنا يأتي مع Kreft:
44
+
45
+ **إعادة التحميل الفورية.** أثناء `kreft dev` احفظ ملف `.kreft` فيُبدَّل ذلك المكوّن وحده وتبقى حالته. وملف `.css` يُعاد تحميله بلا تحديث للصفحة، والخطأ يعرض طبقة فيها الملف والسطر.
46
+
47
+ **أدوات Kreft (DevTools).** زر **S** في الزاوية (أو <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>D</kbd>) يفتح لوحة:
48
+
49
+ | التبويب | يعرض |
50
+ |---|---|
51
+ | Components | الشجرة الحية. اختر مكوّنًا لترى الخصائص والحالة والقيم المشتقة، وغيّر الحالة هناك. |
52
+ | Stores | قيم كل مخزن (قابلة للتغيير) و`$reset()`. |
53
+ | Route | المسار الحالي وكل المسارات، مع زر للذهاب إليها. |
54
+ | Timeline | أفعال المخازن ورسائل الـ bus حال وقوعها. |
55
+
56
+ الزر واللوحة موجودان تحت `kreft dev` فقط؛ التطبيقات المبنية لا تحتويهما أبدًا.
57
+
58
+ ### مراقب الأخطاء
59
+
60
+ في التطوير، خطأ غير ملتقط على الخادم يعرض صفحة تفاعلية بدل رسالة مجردة: مستكشف مكدّس يخفي إطارات المكتبات ويبرز إطاراتك، ومعاينة للشيفرة حول السطر الفاشل، ومحرك اقتراحات للأخطاء الشائعة (خطأ إملائي في دالة نموذج، أو قراءة خاصية من `undefined`، أو جدول مفقود يحتاج `kreft migrate`)، والطلب كاملًا (مع إخفاء كلمات المرور والرموز والكوكيز). وتظهر الصفحة فقط حين `APP_ENV` ليس `production` و`APP_DEBUG` صحيح. **في الإنتاج الخطأ نفسه يجيب `Server error.` ببساطة**، بلا مكدّس أو أسماء ملفات.
61
+
62
+ ## البناء (Build)
63
+
64
+ ```bash
65
+ npm run build # أو: kreft build
66
+ npm run preview # افحص النتيجة: kreft dev dist
67
+ ```
68
+
69
+ | الملف | ما هو |
70
+ |---|---|
71
+ | `index.html` | صفحتك، تحمّل الآن الملفات أدناه. |
72
+ | `kreftjs.js` | الإطار. |
73
+ | `app.js` | كل ملفات `.kreft`، مصرّفة. |
74
+ | `app.css` | كل كتل `<style>`؛ المحصورة محصورة فعلًا. |
75
+ | `main.js` | شيفرة `<script>` المضمنة في `index.html`. |
76
+ | `deps/` و`vendor/` | حزم npm وملفات CSS التي تستوردها ملفاتك، مصغّرة. |
77
+
78
+ ماذا تكسب:
79
+
80
+ - **أول تحميل أسرع:** يحمّل المتصفح `app.js` واحدًا ولا يصرّف شيئًا.
81
+ - **سياسة CSP صارمة تعمل:** كل تعبير يصير دالة حقيقية، فلا `eval` ولا `new Function` في التطبيق المبني.
82
+ - **أخطاء قبل النشر:** أي خطأ في أي ملف `.kreft` يوقف البناء ويسمي الملف والسطر.
83
+
84
+ خيارات: `kreft build ./site --out public` يبني مجلدًا آخر، و`--base /` يغيّر `<base href>`. و`--no-htaccess` يتخطى ملف `.htaccess`.
85
+
86
+ ### القاعدة (base) الصحيحة: خطأ شائع جدًا
87
+
88
+ يحتفظ البناء بوسم `<base href="…">` المكتوب في `index.html` المصدر، ويضبط قواعد `.htaccess` عليه. **إن كان `index.html` المصدر يحمل قاعدة مجلد فرعي** (مثل `<base href="/demo/">` في مشروع `demo`) وأنت ترفع الموقع إلى الجذر، فالنتيجة **صفحة بيضاء**: المتصفح يطلب `/demo/app.js` فلا يجد شيئًا.
89
+
90
+ الحل: اذكر القاعدة صراحة عند البناء للنشر في الجذر:
91
+
92
+ ```bash
93
+ kreft build --out dist/site --base /
94
+ ```
95
+
96
+ وافحص `index.html` المبني: يجب أن ترى `<base href="/">`. وإن كان موقعك في مجلد فرعي `/blog/` فابنِ بـ `--base /blog/`.
97
+
98
+ ### الرفع إلى استضافة
99
+
100
+ **استضافة ثابتة: Hostinger وcPanel وأي استضافة Apache.** ارفع ما **داخل** `dist/` (لا المجلد نفسه) إلى المجلد الذي يخدم الموقع، وهو عادة `public_html`. في hPanel من Hostinger: **Files ← File Manager ← public_html ← Upload**، أو استعمل FTP. لا تستعمل مستورِدات "framework" أو "Node.js app" لهذا: فهي تبحث عن `package.json` وتقول *Unsupported framework or invalid project structure* لأن `dist/` فيه ملفات منتهية فقط.
101
+
102
+ ويكتب `kreft build` ملف `dist/.htaccess`:
103
+
104
+ ```apache
105
+ RewriteEngine On
106
+ RewriteBase /
107
+ RewriteRule ^index\.html$ - [L]
108
+ RewriteCond %{REQUEST_FILENAME} !-f
109
+ RewriteCond %{REQUEST_FILENAME}/index.html !-f
110
+ RewriteCond %{REQUEST_URI} !^/api/
111
+ RewriteCond %{REQUEST_URI} !\.[A-Za-z0-9]+$
112
+ RewriteRule . /index.html [L]
113
+ ```
114
+
115
+ Apache لا يعرف إلا الملفات الموجودة على القرص، فبدون هذه القواعد يعطي `/docs` أو أي عنوان نظيف خطأ 404 عند فتحه أو إعادة تحميله. ومعها، كل عنوان صفحة (بلا امتداد وليس تحت `/api/`) ليس ملفًا حقيقيًا يفتح `index.html` ويتولى الموجِّه الباقي. ارفع الملف أيضًا (يبدأ بنقطة فبعض مديري الملفات يخفونه؛ فعّل "show hidden files").
116
+
117
+ ويضع `kreft build` أيضًا بصمة قصيرة في عنوان كل سكربت وملف أنماط (`app.js?v=28a965f6`)، ويطلب `.htaccess` من الاستضافة ألا تحتفظ بـ `index.html`. الاستضافات تخزّن `.js` و`.css` أيامًا، فبدون البصمة يظل الزوار يشغّلون الإطار والصفحات القديمة بعد رفع بناء جديد.
118
+
119
+ > **ملاحظة:** هذا يستضيف الواجهة الأمامية فقط. تسجيل الدخول وقاعدة البيانات وكل ما تحت `/api` يحتاج خادم Node.js الذي لا يشغّله Apache.
120
+
121
+ **خطة Node.js أو VPS.** ارفع المشروع كله (بلا `node_modules`)، لا `dist/`. أضف Kreft إلى اعتماداتك بـ `npm i kreftjs`، وابنِ بـ `npm run build`، وشغّل الخادم بـ `npm run serve` (يقرأ `PORT` الذي تضبطه استضافتك). سكربت `start` المولَّد يشغّل خادم التطوير فلا تستعمله في الإنتاج.
122
+
123
+ على VPS أبقِه يعمل بـ PM2 وضع nginx أمامه للنطاق وHTTPS:
124
+
125
+ ```bash
126
+ npm install && npm run build
127
+ npm i -g pm2
128
+ pm2 start "npm run serve" --name my-app && pm2 save
129
+ ```
130
+
131
+ ```nginx
132
+ server {
133
+ server_name example.com;
134
+ location / {
135
+ proxy_pass http://127.0.0.1:8000;
136
+ proxy_set_header Host $host;
137
+ proxy_set_header X-Forwarded-Proto $scheme;
138
+ }
139
+ }
140
+ ```
141
+
142
+ ثم `sudo certbot --nginx -d example.com` يضيف HTTPS. اضبط `APP_ENV=production` و`APP_KEY` حقيقيًا في `.env` أولًا، وراجع [قائمة الفحص](07-auth-and-security.md).
143
+
144
+ ## تحسين محركات البحث (SEO)
145
+
146
+ ### صفحات HTML جاهزة: `--prerender`
147
+
148
+ ```bash
149
+ npm i -D jsdom # مرة لكل مشروع
150
+ kreft build --prerender # كل مسار بلا :params
151
+ kreft build --prerender /users/1,/users/2 # وهذه العناوين أيضًا
152
+ ```
153
+
154
+ كل صفحة تُفتح في متصفح محاكى، وتُحمَّل بياناتها، وتُحفظ النتيجة في `dist/<page>/index.html`، فيحصل الزوار ومحركات البحث على HTML منتهٍ.
155
+
156
+ الوسوم التي تضعها الصفحة بـ `.seo({...})` أو `meta` (الوصف، وcanonical، وOpen Graph، وTwitter، وhreflang، وJSON-LD، و`lang` و`dir`) تُحفظ في HTML الصفحة أيضًا، فتراها معاينات الروابط والزواحف التي لا تشغّل JavaScript.
157
+
158
+ ### SEO لصفحات تأتي من قاعدة بياناتك: `backend/routes/seo.js`
159
+
160
+ ```js
161
+ // backend/routes/seo.js
162
+ const Post = require('../app/Models/Post')
163
+
164
+ module.exports = {
165
+ '/': { title: 'متجري', description: 'كل ما يحتاجه البيت' },
166
+ '/blog/:slug': async ({ slug }) => {
167
+ const post = await Post.query().published().where('slug', slug).first()
168
+ if (!post) return null // 404 حقيقي مع noindex
169
+ return {
170
+ title: post.title,
171
+ description: String(post.body || '').slice(0, 150),
172
+ schema: { '@type': 'Article', headline: post.title },
173
+ }
174
+ },
175
+ // عناوين صفحات بياناتك، لـ `kreft sitemap`
176
+ urls: async () => (await Post.query().published().get()).map((p) => ({ path: '/blog/' + p.slug, lastmod: p.updated_at })),
177
+ }
178
+ ```
179
+
180
+ `kreft dev` و`kreft serve` يجيبان طلب الصفحة بـ `index.html` مع وسوم أول عنوان مطابق. واضبط `APP_URL` لتحصل كل صفحة على `canonical`.
181
+
182
+ ### صفحات كاملة للزواحف
183
+
184
+ الزاحف أو معاينة الرابط (Googlebot وBingbot وWhatsApp وFacebook وX وTelegram …) الذي لا يشغّل JavaScript سيرى `<div id="app">` فارغًا. لهؤلاء يفتح `kreft dev` و`kreft serve` الصفحة في المتصفح المحاكى نفسه، وينتظر تحميل بياناتها، ويجيب بالصفحة مرسومة.
185
+
186
+ ```bash
187
+ npm i -D jsdom # مرة لكل مشروع
188
+ KREFT_SSR=bots # الافتراضي: الزواحف ومعاينات الروابط فقط
189
+ KREFT_SSR=all # كل زائر
190
+ KREFT_SSR=off # أبدًا
191
+ KREFT_SSR_TTL=300 # ثوانٍ تحتفظ فيها بالصفحة المرسومة
192
+ ```
193
+
194
+ ### خريطة الموقع وrobots.txt
195
+
196
+ ```bash
197
+ kreft sitemap --url https://example.com # public/sitemap.xml و public/robots.txt
198
+ kreft sitemap --url https://example.com --urls urls.js # مع العناوين في ذلك الملف
199
+ ```
200
+
201
+ ## نشر الواجهة الخلفية
202
+
203
+ ```bash
204
+ NODE_ENV=production kreft serve dist
205
+ ```
206
+
207
+ وقبلها أنجز [قائمة الفحص قبل الإنتاج](07-auth-and-security.md).
208
+
209
+ ## TypeScript
210
+
211
+ يعمل TypeScript في المتصفح (ملفات `.kreft` ووحدات `.ts`) وعلى الخادم (`routes/api.ts` و`app/**/*.ts` والترحيلات والبذور).
212
+
213
+ ```bash
214
+ kreft new my-app --ts
215
+ ```
216
+
217
+ داخل ملف `.kreft` يمكنك كتابة `interface` و`type` و`enum` و`import type`، و`prop` و`state` و`derive` و`saved` بأنواعها. والتنفيذ لا يحتاج إعدادًا: يستعمل Kreft الأداة `esbuild` إن وُجدت، وإلا إزالة الأنواع المدمجة في Node (22.6+)، وإلا مزيله الخاص بلا اعتماديات.
218
+
219
+ ## تصحيح المشاكل الشائعة
220
+
221
+ | العَرَض | السبب المرجح | الحل |
222
+ |---|---|---|
223
+ | صفحة بيضاء بعد الرفع | `<base href>` خاطئ في `index.html` | ابنِ بـ `--base /` وأعد الرفع |
224
+ | `/docs` يعطي 404 عند التحديث | ملف `.htaccess` لم يُرفع | ارفعه (أظهر الملفات المخفية) |
225
+ | بقاء النسخة القديمة بعد الرفع | تخزين المتصفح أو الاستضافة | تأكد من رفع كل الملفات الجديدة بالبصمات |
226
+ | `no such table: …` | ترحيل جديد لم يُنفَّذ | `kreft migrate` |
227
+ | لا شيء يتغير في الصفحة | سطر `const` في السكربت | استعمل `state` أو `derive` |
228
+ | الإرسال لا يعمل مع `type="email"` | تحقق المتصفح الأصلي يسبق Kreft | أضف `novalidate` إلى `<form>` |
229
+
230
+ ## تمارين
231
+
232
+ 1. ابنِ تطبيقك بـ `--base /` وافتح `dist/index.html` وتحقق من وسم `<base>`.
233
+ 2. أضف `backend/routes/seo.js` لمدونتك وافتح صفحة مقال بمتصفح يحاكي زاحفًا: `curl -A Googlebot …`.
234
+ 3. اكتب اختبار نموذج لـ `Product` يتحقق من أن `scopeInStock` يستثني المنتجات التي مخزونها صفر.