شرح Astro Actions: نماذج Type-Safe في
٢٣ مايو ٢٠٢٦
تعد "أسترو أكشنز" (Astro Actions) دوال خادم (server functions) آمنة النوع (type-safe) تُعرفها مرة واحدة وتستدعيها من نموذج HTML، أو عميل JavaScript، أو أكواد خادم أخرى. يبني هذا البرنامج التعليمي نموذج ملاحظات يتحقق من المدخلات باستخدام Zod، ويعيد أخطاء محددة لكل حقل، ويعمل حتى في حالة فشل تحميل JavaScript.
ملخص
يبني هذا الدليل العملي نموذج ملاحظات شغالاً باستخدام Astro Actions بدءاً من مشروع فارغ. ستقوم بإنشاء تطبيق Astro 6.3.7، وإضافة محول Node، وتعريف "أكشن" آمن النوع يتم التحقق من مدخلاته بواسطة مخطط Zod، وربطه بنموذج HTML يرسل البيانات بدون أي JavaScript من جانب العميل. بعد ذلك، ستقوم بعرض أخطاء التحقق لكل حقل، وإعادة التوجيه عند النجاح، ورفض المدخلات السيئة باستخدام ActionError محدد النوع، وتحسين النموذج تدريجياً باستدعاء عميل محدد النوع، والانتهاء بتحميل المشاركات الجديدة على صفحة ثابتة باستخدام "جزيرة خادم" (server island). كل أمر وكتلة كود قابلة للنسخ والتشغيل وتم التحقق منها عن طريق بناء وتشغيل التطبيق النهائي في 23 مايو 2026.
ما ستتعلمه
- إنشاء مشروع Astro وإضافة محول Node للرينددر عند الطلب (on-demand rendering)
- الاحتفاظ بالمشاركات في مخزن محدد النوع يمكنك استبداله لاحقاً بقاعدة بيانات حقيقية
- تعريف "أكشن" آمن النوع باستخدام
defineAction()ومخطط Zod - إرسال نموذج HTML إلى ذلك الـ "أكشن" بدون أي JavaScript من جانب العميل
- التعامل مع النتائج: إعادة التوجيه عند النجاح وعرض أخطاء التحقق لكل حقل
- رفض المدخلات السيئة من المعالج باستخدام
ActionErrorمحدد النوع - تحسين نفس النموذج تدريجياً باستدعاء من جانب العميل محدد النوع
- عرض المشاركات الجديدة على صفحة ثابتة باستخدام "جزيرة خادم" (server island)
لماذا Astro Actions بدلاً من مسار API
الـ Astro Action هي دالة خلفية (backend function) ذات مدخلات تم التحقق منها بواسطة Zod وعميل آمن النوع. مقارنة ببناء نقطة نهاية API يدوياً، يزيل الـ "أكشن" ثلاث فئات من الأكواد المتكررة: فهو يحلل جسم الطلب (request body) نيابة عنك، ويتحقق من ذلك الجسم مقابل مخطط قبل تشغيل المعالج الخاص بك، وينشئ دالة محددة النوع بحيث يتفق العميل والخادم على شكل كل استدعاء.1 كانت الـ "أكشنز" جزءاً من Astro منذ الإصدار 4.15 وهي مستقرة اليوم — إذا أخبرك برنامج تعليمي بضبط علامة experimental.actions، فهو قديم.1
الفرق ملموس. مسار API التقليدي لهذا النموذج سيكون معالج POST يستدعي request.formData()، ويستخرج كل حقل يدوياً، ويحول نص التقييم إلى رقم، ويجري التحقق، ويتفرع بناءً على النجاح أو الفشل، ويحول استجابة JSON إلى تسلسل — ومعظم ذلك غير محدد النوع، لذا فإن إعادة تسمية حقل تعني خطأ في وقت التشغيل. الـ "أكشن" يختصر ذلك في مخطط ومعالج. تستورد الصفحة الـ "أكشن" وتشير سمة action الخاصة بالنموذج إليه مباشرة؛ استدعِ نفس الـ "أكشن" من كود العميل أو الخادم وستصبح إعادة تسمية الحقل خطأ في وقت التجميع (compile error) بدلاً من مفاجأة في وقت التشغيل. تعريف واحد يغطي ثلاثة مواقع استدعاء — نموذج HTML، ودالة من جانب العميل، وأكواد خادم أخرى.1
المتطلبات الأساسية
- Node.js 22.12.0 أو أحدث. يضبط Astro 6 قيمة
engines.nodeعلى>=22.12.0.2 تحقق باستخدامnode --version. - مدير حزم — يستخدم هذا الدليل
npm، الذي يأتي مع Node. - مبنى أوامر (terminal) ومحرر أكواد. لا يلزم وجود قاعدة بيانات، ولا حساب، ولا مفتاح API.
يستخدم التطبيق النهائي astro@6.3.7 و @astrojs/node@10.1.1، وهما الإصداران الحاليان المنشوران اعتباراً من 23 مايو 2026.23 تثبيت الإصدارات الدقيقة يحافظ على قابلية إعادة إنتاج البرنامج التعليمي؛ تشغيل npm install بدون إصدار يسحب أحدث ما هو متاح في اليوم الذي تشغله فيه.
الخطوة 1 — إنشاء مشروع Astro
أنشئ مشروعاً جديداً باستخدام أداة الإنشاء الرسمية:4
npm create astro@latest astro-actions-form
سيسألك المعالج بضعة أسئلة. اختر قالب Empty (بداية minimal — ليس بها صفحات تجريبية لحذفها لاحقاً)، وقل Yes لتثبيت التبعيات، و Yes لتهيئة مستودع git، وعندما يسألك عن TypeScript اختر Strict. عندما ينتهي، سيكون لديك تطبيق Astro شغال:
cd astro-actions-form
npm run dev
افتح http://localhost:4321 وسترى صفحة البداية. أوقف خادم التطوير باستخدام Ctrl+C قبل الخطوة التالية. لضمان أنك تستخدم الإصدار الدقيق الذي تم اختبار هذا الدليل عليه، قم بتثبيت إصدار Astro الآن:
npm install astro@6.3.7
الخطوة 2 — إضافة محول Node للريندر عند الطلب
تعمل الـ "أكشنز" على الخادم، والصفحة التي ترسل البيانات إلى إحداها عبر نموذج HTML <form> يجب أن يتم ريندرها عند الطلب بدلاً من الريندر المسبق (prerendered) إلى HTML ثابت. يحتاج الريندر عند الطلب إلى محول خادم. بالنسبة لخادم Node مستضاف ذاتياً، هذا المحول هو @astrojs/node.3 أضفه باستخدام أمر astro add المدمج، والذي يثبت الحزمة ويعدل إعداداتك في خطوة واحدة:
npx astro add node
وافق على المطالبة. يقوم الأمر بتثبيت @astrojs/node ويضيف المحول إلى ملف astro.config.mjs. افتح ذلك الملف وتأكد من أنه يطابق ما يلي — على وجه الخصوص، تأكد من استدعاء المحول باستخدام mode: 'standalone'، لأن astro add لا يكتب خيار mode دائماً نيابة عنك:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
adapter: node({ mode: 'standalone' }),
});
npm install @astrojs/node@10.1.1
يقوم mode: 'standalone' ببناء خادم يبدأ تشغيل نفسه — وهو أمر مفيد لاحقاً عند تشغيل بناء الإنتاج (production build).3 لا تحتاج إلى ضبط output: 'server'. يحافظ Astro 6 على القيمة الافتراضية output: 'static'، والتي تقوم بريندر مسبق لكل صفحة إلى HTML؛ وتختار الصفحات الفردية للريندر عند الطلب باستخدام تصدير من سطر واحد، وهو ما تفعله الخطوة 5.5 بهذه الطريقة تظل صفحات التسويق الخاصة بك ثابتة وسريعة، وتعمل صفحة النموذج فقط لكل طلب.
الخطوة 3 — إنشاء مخزن ملاحظات محدد النوع
قبل أن يكون للـ "أكشن" مكان للكتابة فيه، امنحه مخزناً صغيراً. لإبقاء البرنامج التعليمي مكتفياً ذاتياً، هذا عبارة عن مصفوفة في الذاكرة (in-memory array) — تعيش طالما كانت عملية الخادم تعمل. في الإنتاج، ستستبدل الدوال الثلاث أدناه باستدعاءات قاعدة بيانات؛ ولن يتغير باقي التطبيق.
أنشئ ملف src/lib/store.ts:
// src/lib/store.ts
export interface Feedback {
id: string;
name: string;
email: string;
rating: number;
message: string;
createdAt: Date;
}
// In-memory only. Swap for a real database in production.
const entries: Feedback[] = [];
export function addFeedback(data: Omit<Feedback, 'id' | 'createdAt'>): Feedback {
const entry: Feedback = {
id: crypto.randomUUID(),
createdAt: new Date(),
...data,
};
entries.unshift(entry);
return entry;
}
export function listFeedback(limit = 5): Feedback[] {
return entries.slice(0, limit);
}
export function countFeedback(): number {
return entries.length;
}
crypto.randomUUID() مدمج في Node، لذا لا يوجد شيء لتثبيته. تعيد دالة addFeedback السجل الذي تم إنشاؤه، والذي سيستخدمه الـ "أكشن" لبناء عنوان URL لإعادة التوجيه.
الخطوة 4 — تعريف "أكشن" آمن النوع باستخدام Zod
تعيش جميع الإجراءات (actions) داخل كائن server يتم تصديره من src/actions/index.ts.1 يتم إنشاء كل إجراء بواسطة defineAction() ويتكون من ثلاثة أجزاء: وضع accept، ومخطط input، ومعالج handler.
أنشئ src/actions/index.ts:
// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
import { addFeedback } from '../lib/store';
export const server = {
submitFeedback: defineAction({
accept: 'form',
input: z.object({
name: z.string().min(2, 'Name must be at least 2 characters.'),
email: z.email('Enter a valid email address.'),
rating: z
.number('Choose a rating.')
.min(1, 'Rating must be between 1 and 5.')
.max(5, 'Rating must be between 1 and 5.'),
message: z
.string()
.min(10, 'Message must be at least 10 characters.')
.max(500, 'Message must be 500 characters or fewer.'),
}),
handler: async ({ name, email, rating, message }) => {
const entry = addFeedback({ name, email, rating, message });
return { id: entry.id, name: entry.name };
},
}),
};
هناك ثلاثة تفاصيل مهمة هنا. accept: 'form' يخبر Astro بتحليل إرسال نموذج HTML: فهو يقرأ سمة name لكل <input> ويبني كائناً قبل التحقق من الصحة.6 import { z } from 'astro/zod' يستخدم نسخة Zod التي تأتي مدمجة داخل Astro 6 — حيث يقوم Astro بتجميع Zod 4، لذا فإن أدوات التحقق الحديثة عالية المستوى مثل z.email() متاحة دون تثبيت منفصل.27 ويتلقى الـ handler مدخلاً (input) مكتوباً بالكامل (typed) ومتحققاً منه مسبقاً: بحلول وقت تشغيل الكود الخاص بك، يكون rating عبارة عن number بين 1 و 5، وليس نصاً خاماً من النموذج. إذا فشل التحقق، يعيد Astro خطأ BAD_REQUEST ولا يتم تشغيل المعالج أبداً.6
يقوم Astro بتطبيق بعض التحويلات لحقول النموذج تلقائياً. يتم التحقق من صحة <input type="number"> باستخدام z.number() على الرغم من أن قيم النموذج تصل كنصوص؛ ويتم التحقق من خانة الاختيار (checkbox) باستخدام z.coerce.boolean()؛ ومدخل الملف باستخدام z.instanceof(File)؛ ويصل حقل النص الفارغ كـ null بدلاً من نص فارغ.6 سلوك الـ null هذا هو السبب في أن name يستخدم z.string().min(2) — حيث يفشل الإرسال الفارغ في التحقق من الصحة بوضوح بدلاً من المرور كـ "".
يحتوي accept على وضعين. الافتراضي، 'json'، مخصص للإجراءات التي تستدعيها فقط من JavaScript مع وسيط كائن بسيط. أما 'form' — المستخدم هنا — فيجعل الإجراء يحلل FormData، وهو ما يرسله كل من نموذج HTML واستدعاء FormData من جانب العميل. اختر 'form' كلما كان هناك عنصر <form> حقيقي، لأن هذا هو الوضع الذي يعمل في متصفح بدون JavaScript. إذا حذفت مخطط input تماماً، يتلقى المعالج كائن FormData الخام وتقوم بالتحقق منه بنفسك؛ توفير المخطط هو ما يمنحك الـ input المكتوب والمتحقق منه مسبقاً.6
الخطوة 5 — بناء النموذج وإرساله بدون JavaScript
الآن قم بربط نموذج HTML بالإجراء. أنشئ src/pages/feedback.astro:
---
// src/pages/feedback.astro
export const prerender = false;
import { actions } from 'astro:actions';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Leave feedback</title>
</head>
<body>
<h1>Leave feedback</h1>
<form method="POST" action={actions.submitFeedback}>
<p><label>Name <input type="text" name="name" required /></label></p>
<p><label>Email <input type="email" name="email" required /></label></p>
<p>
<label>Rating
<input type="number" name="rating" min="1" max="5" required />
</label>
</p>
<p>
<label>Message
<textarea name="message" required></textarea>
</label>
</p>
<button type="submit">Send feedback</button>
</form>
</body>
</html>
يقوم سطران بالعمل الشاق. export const prerender = false يجعل هذه الصفحة تستخدم الرندرة عند الطلب (on-demand rendering)، وهو ما يتطلبه SSR الخاص بإجراءات النماذج.5 تعيين action={actions.submitFeedback} على نموذج method="POST" يخبر Astro بتوجيه الإرسال إلى الإجراء الخاص بك — حيث يتم رندرته كـ action="?_action=submitFeedback"، وهي سلسلة استعلام (query string) يتعامل معها الخادم نيابة عنك.6 لا حاجة لـ fetch()، ولا onsubmit، ولا أي JavaScript من جانب العميل على الإطلاق.
قم بتشغيل npm run dev وأرسل النموذج على http://localhost:4321/feedback. يتم تشغيل الإجراء وتخزين المدخل — لكن الصفحة تعيد التحميل فقط ولا يرى المستخدم شيئاً. النموذج الذي لا يقدم أي ملاحظات (feedback) ليس مكتملاً، وهذا ما ستصلحه الخطوة التالية.
الخطوة 6 — التعامل مع النتائج: إعادة التوجيه عند النجاح، ورندرة أخطاء الحقول
استدعاء Astro.getActionResult() على الخادم يعيد نتيجة إرسال النموذج: كائن يحتوي على data أو error، أو undefined إذا لم يتم استدعاء الإجراء في هذا الطلب.8 استخدمه للقيام بشيئين — إعادة التوجيه بعد إرسال ناجح، ورندرة أخطاء التحقق عندما يكون المدخل سيئاً.
إعادة التوجيه بعد POST ناجح هو نمط POST/Redirect/GET. فهو يمنع ظهور مربع حوار المتصفح "تأكيد إعادة إرسال النموذج؟" عند التحديث ويمنح المستخدم عنوان URL نظيفاً.6 بالنسبة لفشل التحقق، تقوم أداة المساعدة isInputError() بتضييق نطاق الخطأ إلى خطأ مدخلات وتكشف عن كائن fields مفتاحه اسم المدخل، مع مصفوفة من الرسائل لكل حقل فشل.9
استبدل src/pages/feedback.astro بالنسخة الكاملة:
---
// src/pages/feedback.astro
export const prerender = false;
import { actions, isInputError } from 'astro:actions';
const result = Astro.getActionResult(actions.submitFeedback);
// POST / Redirect / GET: leave the POST response on success.
if (result && !result.error) {
return Astro.redirect('/feedback?success=' + encodeURIComponent(result.data.name));
}
const fieldErrors = isInputError(result?.error) ? result.error.fields : {};
const formError =
result?.error && !isInputError(result.error) ? result.error.message : null;
const successName = Astro.url.searchParams.get('success');
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Leave feedback</title>
</head>
<body>
<h1>Leave feedback</h1>
{successName && <p class="success">Thanks, {successName}! Your feedback was recorded.</p>}
{formError && <p class="error">{formError}</p>}
<form method="POST" action={actions.submitFeedback}>
<p>
<label>Name <input type="text" name="name" required /></label>
{fieldErrors.name && <span class="error">{fieldErrors.name.join(' ')}</span>}
</p>
<p>
<label>Email <input type="email" name="email" required /></label>
{fieldErrors.email && <span class="error">{fieldErrors.email.join(' ')}</span>}
</p>
<p>
<label>Rating
<input type="number" name="rating" min="1" max="5" required />
</label>
{fieldErrors.rating && <span class="error">{fieldErrors.rating.join(' ')}</span>}
</p>
<p>
<label>Message
<textarea name="message" required></textarea>
</label>
{fieldErrors.message && <span class="error">{fieldErrors.message.join(' ')}</span>}
</p>
<button type="submit">Send feedback</button>
</form>
</body>
</html>
كل استدعاء إجراء — سواء من نموذج، أو من JavaScript العميل، أو من كود الخادم — ينتهي إلى نفس الكائن: يحمل data القيمة المرجعة المكتوبة لمعالجك، أو يحمل error أي خطأ حدث، ويوجد واحد فقط من الاثنين.1 فشل التحقق من الصحة والأخطاء الملقاة (thrown errors) كلاهما ينتهي في error، ولهذا السبب يكفي فحص واحد if (result && !result.error) للقيام بإعادة التوجيه. إذا كنت تفضل عدم استخدام التفرع (branching)، فإن إلحاق .orThrow() باستدعاء الإجراء يلقي خطأ عند الفشل ويعيد data مباشرة — وهو أمر مفيد داخل try/catch أو أثناء بناء النماذج الأولية.1
أرسل مدخلاً صالحاً وسينتهي المتصفح في /feedback?success=Your%20Name مع رسالة شكر. أرسل بريداً إلكترونياً سيئاً أو رسالة من كلمة واحدة وسيعود النموذج مع رسالة تحت كل حقل مخالف — ولم يتم تشغيل المعالج من الخطوة 4 أبداً، لأنه تم إرجاع BAD_REQUEST قبله.6 لا تزال سمات required و type="email" في المدخلات تقدم فحوصات فورية داخل المتصفح؛ الإجراء هو التحقق الرسمي من جانب الخادم الذي لا يمكن للمستخدم تجاوزه.
سطر formError يلتقط فئة مختلفة من الفشل، والتي ستنتجها الخطوة التالية عمداً.
الخطوة 7 — رفض المدخلات السيئة من المعالج باستخدام ActionError
يرفض Zod المدخلات التي لها شكل خاطئ — بريد إلكتروني غير صالح، تقييم خارج النطاق 1-5. لكن بعض حالات الفشل لا تظهر إلا بمجرد تشغيل المعالج: سجل مفقود، مستخدم غير مصرح له، إرسال يكسر قاعدة معينة. بالنسبة لهذه الحالات، قم بإلقاء ActionError من داخل المعالج. يحمل ActionError كود حالة (code) مثل BAD_REQUEST، أو UNAUTHORIZED، أو NOT_FOUND، بالإضافة إلى رسالة (message) اختيارية قابلة للقراءة من قبل البشر، وتصل في خاصية error للنتيجة مثل أي فشل آخر.9
لنفترض أن الملاحظات لا يجوز أن تحتوي على روابط. قم بتحديث استيراد الإجراء ومعالجه في src/actions/index.ts:
// src/actions/index.ts
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro/zod';
import { addFeedback } from '../lib/store';
export const server = {
submitFeedback: defineAction({
accept: 'form',
input: z.object({
name: z.string().min(2, 'Name must be at least 2 characters.'),
email: z.email('Enter a valid email address.'),
rating: z
.number('Choose a rating.')
.min(1, 'Rating must be between 1 and 5.')
.max(5, 'Rating must be between 1 and 5.'),
message: z
.string()
.min(10, 'Message must be at least 10 characters.')
.max(500, 'Message must be 500 characters or fewer.'),
}),
handler: async ({ name, email, rating, message }) => {
if (message.toLowerCase().includes('http://') || message.toLowerCase().includes('https://')) {
throw new ActionError({
code: 'BAD_REQUEST',
message: 'Links are not allowed in feedback.',
});
}
const entry = addFeedback({ name, email, rating, message });
return { id: entry.id, name: entry.name };
},
}),
};
لم تلمس feedback.astro. سطر formError الذي كتبته في الخطوة 6 يتعامل مع هذا بالفعل: isInputError() تكون false لخطأ ActionError قمت برميه بنفسك، لذا ينتقل الخطأ إلى formError ويظهر في اللافتة فوق النموذج. أرسل رسالة تحتوي على رابط وسترى "Links are not allowed in feedback." يظهر فشل Zod لكل حقل؛ ويظهر خطأ ActionError المرمي في اللافتة — مساران، كائن نتيجة واحد.9
الخطوة 8 — تحسين النموذج تدريجيًا باستخدام JavaScript
النموذج يعمل بالفعل بدون JavaScript. هذا هو الأساس الذي لا تريد خسارته أبدًا. التحسين التدريجي (Progressive enhancement) يعني إضافة طبقة من التجربة الأفضل فوق الأساس دون إزالته — إذا فشل تحميل السكربت، فسيظل النموذج العادي يرسل البيانات.
استورد actions في وسم <script> واستدعِ الأكشن مباشرةً. يقوم Astro بإنشاء دالة عميل (client function) مكتوبة الأنواع (typed) لكل أكشن، لذا فإن actions.submitFeedback(formData) هو استدعاء RPC حقيقي — لا يوجد fetch()، ولا سلاسل URL، ولا JSON يدوي.1 أضف هذه الكتلة البرمجية قبل </body> مباشرةً في src/pages/feedback.astro:
<script>
import { actions } from 'astro:actions';
const form = document.querySelector('form');
form?.addEventListener('submit', async (event) => {
event.preventDefault();
const { data, error } = await actions.submitFeedback(new FormData(form));
if (error) {
// Hand off to the normal server round-trip, which re-renders field errors.
form.submit();
return;
}
const note = document.createElement('p');
note.className = 'success';
note.textContent = `Thanks, ${data.name}! Your feedback was recorded.`;
form.replaceWith(note);
});
</script>
event.preventDefault() يوقف إرسال النموذج الأصلي حتى يتمكن السكربت من تولي المهمة. يتم استدعاء الأكشن باستخدام كائن FormData مبني من النموذج؛ عند النجاح، يعيد الـ data المكتوبة من المعالج الخاص بك، ويقوم السكربت باستبدال النموذج برسالة شكر في نفس الصفحة دون أي تنقل. عند حدوث فشل في التحقق أو ActionError، يقوم form.submit() بإعادة التحكم إلى دورة الخادم القياسية من الخطوة 6، بحيث يظل عرض أخطاء الحقول يعمل من خلال مسار كود واحد.6 نظرًا لأن كل أكشن مكشوف أيضًا في نقطة نهاية RPC عامة — /_actions/submitFeedback لهذا الأكشن — فإن استدعاء العميل وإرسال النموذج يشغلان نفس المعالج الموثق تمامًا.1
نظرًا لاستيراد الأكشن بدلاً من الإشارة إليه عبر URL، فإن استدعاء العميل مكتوب الأنواع بالكامل: يعرف محررك أن submitFeedback يأخذ الحقول الأربعة وأن الـ data الناجحة هي { id, name }. لا يوجد سلسلة نصية لنقطة النهاية تحتاج لمزامنتها ولا شكل استجابة لكتابته يدويًا — قم بتغيير اسم حقل في المخطط (schema) وسيتوقف موقع الاستدعاء عن التحقق من النوع. لإثبات ادعاء التحسين التدريجي، افتح أدوات مطور المتصفح، وعطل JavaScript، وأرسل النموذج مرة أخرى. سيظل يعمل، لأن وسم <form action={actions.submitFeedback}> لم يعتمد أبدًا على السكربت. السكربت هو تحسين محض: عندما يتم تحميله، يقوم الإرسال الناجح بتحديث الصفحة في مكانها بدلاً من إعادة التحميل؛ وعندما لا يتم تحميله، تتولى دورة الخادم من الخطوة 6 المهمة ولا ينكسر شيء.
الخطوة 9 — عرض المشاركات الجديدة باستخدام server island
يمكن أن تظل صفحتك الرئيسية ثابتة وفورية مع استمرار عرض البيانات الحية، باستخدام server island. وضع علامة على مكون باستخدام server:defer يخبر Astro بعرض الصفحة على الفور، وإرسال محتوى احتياطي (fallback) مكان المكون، ثم جلب HTML الحقيقي للمكون بشكل منفصل واستبداله.10 تحتاج الـ server islands إلى محول (adapter) — لقد قمت بتثبيت واحد بالفعل في الخطوة 2.
هذه مقايضة مختلفة عن المكون الذي يتم عرضه لدى العميل (client-rendered component). يشحن مكون العميل الـ JavaScript الخاص به ويعرض في المتصفح؛ أما الـ server island فيتم عرضها على الخادم وتشحن فقط سكربت تحميل صغير مكانها، لذا يمكنها قراءة المتجر أو قاعدة البيانات مباشرة دون الكشف عن API أو إرسال منطقها إلى المتصفح. استخدم server:defer عندما يحتاج جزء واحد من صفحة قابلة للتخزين المؤقت إلى بيانات خادم حديثة أو مخصصة — قائمة نشاط حديثة، صورة رمزية لمستخدم مسجل الدخول، سعر لكل طلب.
أنشئ مكون الـ island، src/components/RecentFeedback.astro:
---
// src/components/RecentFeedback.astro
import { listFeedback, countFeedback } from '../lib/store';
const recent = listFeedback(5);
const total = countFeedback();
---
<section>
<h2>Recent feedback ({total})</h2>
{recent.length === 0 ? (
<p>No feedback yet. Be the first to leave some.</p>
) : (
<ul>
{recent.map((item) => (
<li>
<strong>{item.name}</strong> rated us {item.rating}/5
<p>{item.message}</p>
</li>
))}
</ul>
)}
</section>
ثم استخدمه في الصفحة الرئيسية. استبدل src/pages/index.astro:
---
// src/pages/index.astro
import RecentFeedback from '../components/RecentFeedback.astro';
---
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Feedback demo</title>
</head>
<body>
<h1>Astro Actions feedback demo</h1>
<p><a href="/ar/feedback">Leave feedback</a></p>
<RecentFeedback server:defer>
<p slot="fallback">Loading recent feedback…</p>
</RecentFeedback>
</body>
</html>
لا يوجد prerender = false في هذه الصفحة — الصفحة الرئيسية لا تزال مستندًا ثابتًا. فقط الـ island RecentFeedback هي التي تعمل مع كل طلب، لذا يرى الزائر الصفحة والعنصر النائب "Loading recent feedback…" على الفور، وتصل القائمة الحقيقية بعد لحظة.10 المكون الفرعي الذي يحمل علامة slot="fallback" هو ما يظهر أثناء تحميل الـ island. أرسل تعليقًا، ثم عد إلى http://localhost:4321/، وستظهر مشاركتك في القائمة — بيانات حديثة على صفحة مخزنة مؤقتًا.
هناك قيدان يستحقان المعرفة قبل الاعتماد على الـ islands. أولاً، يتم عرض الـ server island في سياق معزول: يتم جلبها كطلب خاص بها، لذا فإن Astro.url داخل الـ island هو نقطة نهاية الـ island، وليس الصفحة التي يتواجد عليها الزائر. إذا كانت الـ island تحتاج إلى عنوان URL الخاص بالصفحة — لمعلمات الاستعلام مثلاً — فاقرأه من رأس طلب Referer.10 ثانيًا، يجب أن تكون أي خصائص (props) تمررها إلى مكون server:defer قابلة للتسلسل (كائنات بسيطة، أرقام، سلاسل نصية، مصفوفات، Date، وما شابه)؛ لا يمكن للدوال عبور تلك الحدود.10 هنا لا تأخذ الـ island أي خصائص وتقرأ المتجر مباشرة، لذا لا يؤثر أي من القيدين — لكنهما سيفعلان ذلك في اللحظة التي تمرر فيها البيانات.
التحقق
تأكد من سير العملية بالكامل من البداية للنهاية. مع تشغيل npm run dev:
- قم بزيارة
http://localhost:4321/feedbackوأرسل مدخلات صالحة. ستنتقل إلى/feedback?success=Your%20Nameمع رسالة شكر. - أرسل بريدًا إلكترونيًا خاطئًا ورسالة قصيرة. سيعود النموذج مع رسالة تحت كل حقل خاطئ؛ ولن يتم تخزين شيء.
- أرسل رسالة تحتوي على
https://example.com. ستظهر لافتة "Links are not allowed in feedback.". - قم بزيارة
http://localhost:4321/— ستظهر مشاركتك الصالحة في الـ server island.
يمكنك أيضًا اختبار نقطة نهاية النموذج من سطر الأوامر. تتطلب حماية CSRF المدمجة في Astro رأس Origin مطابقًا في عمليات إرسال النماذج، لذا مرره صراحةً:
curl -s -i -X POST 'http://localhost:4321/feedback?_action=submitFeedback' \
-H 'Origin: http://localhost:4321' \
--data 'name=Jordan+Lee&email=jordan%40example.com&rating=5&message=Great+feedback+widget.'
يستجيب الطلب الصالح بـ HTTP/1.1 302 Found ورأس location: يشير إلى عنوان URL للنجاح — نمط POST/Redirect/GET قيد التنفيذ.
للحصول على بناء إنتاجي، قم بتشغيل npm run build وابدأ الخادم المستقل:
npm run build
node ./dist/server/entry.mjs
يطبع البناء output: "static" مع mode: "server" — يتم عرض الصفحة الرئيسية مسبقًا كملف ثابت بينما يتم تجميع /feedback في حزمة الخادم عند الطلب، تمامًا كما أعددت في الخطوتين 2 و5. في وضع standalone، يصدر المحول dist/server/entry.mjs، وهو خادم يبدأ نفسه ويخدم أيضًا أصولك الثابتة. يبدأ على http://localhost:4321؛ قم بتجاوز الربط باستخدام متغيرات البيئة HOST و PORT إذا لزم الأمر.3
الأخطاء الشائعة
403 Cross-site POST form submissions are forbidden. هذه هي حماية CSRF الخاصة بـ Astro (المتمثلة في security.checkOrigin، والمفعلة افتراضيًا)، والتي ترفض عمليات POST بنمط النماذج (form-style) التي لا يتطابق فيها رأس Origin مع أصل الموقع.11 المتصفحات الحقيقية ترسل Origin مطابقًا تلقائيًا، لذا فإن هذه المشكلة تظهر فقط عند الاختبار عبر سطر الأوامر أو عند وجود بروكسي عكسي (reverse proxy) غير مهيأ بشكل صحيح. مع curl، أضف -H 'Origin: http://localhost:4321' — ولاحظ أن محول Node المستقل يبلغ عن أصله كـ localhost، لذا استخدم localhost بدلاً من 127.0.0.1 في كل من الرابط (URL) والرأس (header).
المعالج (handler) لا يعمل أبدًا والصفحة تكتفي بإعادة التحميل. أنت تفتقد إلى export const prerender = false. بدونها، يتم عرض الصفحة مسبقًا (prerendered) كـ HTML ثابت ولا يمكن للأكشن (action) أن يعمل.5
getActionResult يعيد دائمًا undefined. إنه يعيد قيمة فقط في الطلب الذي استدعى الأكشن — أي طلب POST. في طلب GET العادي، يكون undefined حسب التصميم.8 اقرأ البيانات المرسلة من النتيجة أثناء طلب POST، ثم قم بإعادة التوجيه.
415 Unsupported Media Type عند استدعاء الأكشن. الأكشن الخاص بك مضبوط على accept: 'form' ولكن الطلب أرسل JSON. الأكشنز بنمط النماذج تقبل أجسامًا مشفرة كنموذج (form-encoded) أو multipart/form-data؛ أرسل كائن FormData من العميل (كما هو موضح في الخطوة 8)، وليس كائن JSON.6
أخطاء الحقول لا تظهر أبدًا. يجب أن يحتوي كل عنصر <input> على سمة name تطابق مفتاحًا في مخطط Zod. يقوم Astro ببناء الكائن الذي تم التحقق منه من سمات name تلك، لذا فإن أي خطأ إملائي يعني وصول القيمة كـ undefined.6
المحرر الخاص بك يظهر خطأ Cannot find module 'astro:actions'. astro:actions هو موديول افتراضي يتم إنشاؤه عندما يقوم Astro بمزامنة مشروعك. تشغيل npm run dev يعيد إنشاؤه؛ إذا كنت ترى الخطأ في المحرر فقط، فقم بتشغيل npx astro sync مرة واحدة لتحديث الأنواع (types) المنشأة في مجلد .astro/.
قبل الإطلاق للإنتاج
التطبيق صحيح، ولكن هناك ثلاثة أمور تحتاج إلى اهتمام قبل أن تصل إليه الزيارات الحقيقية.
المخزن في الذاكرة (in-memory store) يكون لكل عملية (process). يتم مسحه مع كل إعادة تشغيل ولا يتم مشاركته بين مثيلات خادم متعددة، لذا فإن النشر المعتمد على موازن الأحمال (load-balanced deployment) سيظهر قوائم مختلفة في طلبات مختلفة. هذا هو المقابل المتعمد لإبقاء البرنامج التعليمي خاليًا من التبعيات الخارجية؛ استبدل src/lib/store.ts بقاعدة بيانات قبل الإطلاق.
كل أكشن هو نقطة نهاية (endpoint) عامة. يمكن الوصول إلى المعالج عبر /_actions/submitFeedback من قبل أي شخص يعرف الاسم، تمامًا مثل مسار REST — وثائق Astro صريحة في أنه يجب عليك تطبيق نفس إجراءات التفويض وتحديد معدل الطلبات (rate-limiting) التي قد تمنحها لنقطة نهاية API.1 بالنسبة لنموذج ملاحظات عام، يعني ذلك وضع حدود للمدخلات وحماية من سوء الاستخدام في المعالج؛ ولأي شيء خاص بالمستخدم، يعني ذلك فحص تفويض يرمي ActionError بكود UNAUTHORIZED.
تقوم جزر الخادم (Server islands) بتشفير الخصائص (props) الممرة إليها بمفتاح يتم إعادة إنشاؤه مع كل بناء (build). هذا جيد لخادم واحد، ولكن في عمليات النشر المتلاحقة (rolling deployments)، أو الاستضافة متعددة المناطق، أو شبكات CDN التي تخزن الصفحات التي تحتوي على جزر، يمكن أن يختلف بناء التشفير عن بناء فك التشفير. قم بإنشاء مفتاح ثابت مرة واحدة باستخدام astro create-key، وضعه كمتغير بيئة ASTRO_KEY في عمليات البناء والتشغيل، وسيبقى المفتاح ثابتًا عبر عمليات النشر.10
الخطوات التالية
لديك الآن نموذج آمن الأنواع (type-safe) مع تحقق من صحة البيانات على الخادم، وتحسين تدريجي (progressive enhancement)، وجزيرة خادم مباشرة — وهو نمط يتوسع من نموذج اتصال إلى لوحة تحكم كاملة. من هنا:
- استبدل المخزن في الذاكرة بقاعدة بيانات حقيقية. الدوال الثلاث في
src/lib/store.tsهي الكود الوحيد الذي سيتغير؛ بينما يظل الأكشن والصفحات كما هي. - أضف نظام مصادقة حتى يتمكن الأكشن من رفض المستخدمين غير المصرح لهم. نفس آلية
ActionErrorمع كودUNAUTHORIZEDهي ما يقود ذلك. - قارن هذا النموذج مع رؤية React في Server Actions و optimistic UI في React 16، ومع نهج بدون إطارات عمل في بناء لوحة Kanban مباشرة باستخدام htmx و Express.
- جزر الخادم هي أقارب وثيقين لواجهات المستخدم المتدفقة (streaming UI) — راجع الاسترداد المتدفق (streaming) و Suspense في React 16 للمكافئ في React.
الدرس الأساسي يظل ثابتًا عبر إطارات العمل: تحقق من الصحة على الخادم، اجعل المسار الذي لا يعتمد على JavaScript يعمل أولاً، ثم قم بالتحسين من هناك.
Footnotes
-
"Actions," Astro Docs — Actions added in
astro@4.15;defineAction(), theserverobject insrc/actions/index.ts, typed client calls, and the/_actions/{name}public endpoint. https://docs.astro.build/en/guides/actions/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
astronpm package — version6.3.7(latestdist-tag, published 2026-05-21) andengines.node: ">=22.12.0", verified vianpm view astroon 2026-05-23. https://www.npmjs.com/package/astro ↩ ↩2 ↩3 -
حزمة npm الخاصة بـ
@astrojs/node— الإصدار10.1.1(علامة التوزيعlatest، نُشرت في 13-05-2026)، ودليل تكامل@astrojs/nodeالذي يغطيastro add node، وmode: 'standalone'، ونقطة الدخولdist/server/entry.mjs، وتجاوزاتHOST/PORT. https://docs.astro.build/en/guides/integrations-guide/node/ ↩ ↩2 ↩3 ↩4 -
"تثبيت Astro،" وثائق Astro — أمر الهيكلة
npm create astro@latest، ومطالبات المعالج الخاص به، وخيارات قوالب البداية. https://docs.astro.build/en/install-and-setup/ ↩ -
"الرندرة عند الطلب،" وثائق Astro — نموذج
output: 'static'الافتراضي وتمكين الرندرة عند الطلب لكل صفحة باستخدامexport const prerender = false. https://docs.astro.build/en/guides/on-demand-rendering/ ↩ ↩2 ↩3 -
"Actions — قبول بيانات النموذج" و "استدعاء الـ actions من action نموذج HTML،" وثائق Astro —
accept: 'form'، وتحليل النموذج بواسطة مدخلname، وسلسلة الاستعلام?_action=، ومتطلبات الرندرة عند الطلب، ونمط POST/Redirect/GET. https://docs.astro.build/en/guides/actions/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
"astro/zod،" وثائق Astro — تقوم Astro بإعادة تصدير حزمة Zod المدمجة (Zod 4 في Astro 6) من وحدة
astro/zodللاستخدام في مخططات مدخلات الـ action. https://docs.astro.build/en/reference/modules/astro-zod/ ↩ -
"سياق الرندرة — getActionResult،" وثائق Astro — تعيد
Astro.getActionResult()الـdata/errorفي الطلب الذي استدعى الـ action، أوundefinedبخلاف ذلك. https://docs.astro.build/en/reference/API-reference/ ↩ ↩2 -
"Actions — التعامل مع أخطاء الواجهة الخلفية" و "عرض أخطاء مدخلات النموذج،" وثائق Astro — فئة
ActionErrorمع حالةcodeوmessage، وحارسisInputError()الذي يكشف عنerror.fields. https://docs.astro.build/en/reference/modules/astro-actions/ ↩ ↩2 ↩3 -
"Server islands،" وثائق Astro — توجيه
server:defer، ونائب المحتوىslot="fallback"، ومتطلبات المحول (adapter)، وكيفية جلب الـ islands المؤجلة بعد تحميل الصفحة. https://docs.astro.build/en/guides/server-islands/ ↩ ↩2 ↩3 ↩4 ↩5 -
"مرجع التكوين — security.checkOrigin،" وثائق Astro — التحقق من أصل إرسال النموذج عبر المواقع، مُمكّن افتراضيًا للصفحات التي يتم رندرتها عند الطلب. https://docs.astro.build/en/reference/configuration-reference/ ↩