في هذه الصفحة
- الصورة الإجمالية
- سلسلة الأدوات: Bun بدلاً من npm
- Astro: ثابت افتراضياً
- توصيل JavaScript
- أداء التحميل ومؤشرات Core Web Vitals
- التصميم البصري
- مجموعات المحتوى
- التوجيه والصفحات
- تعدد اللغات
- معالجة الصور
- التحسين والمتغيرات المتجاوبة
- حقن الأبعاد التلقائي
- SEO وقابلية الاكتشاف
- HTML head
- خريطة الموقع
- تغذيات RSS
- robots.txt
- llms.txt
- النشر: GitHub + Cloudflare Pages
- CI: GitHub Actions
- ترويسات HTTP
- إعادة التوجيه
- التعليقات: Giscus
- الصورة الكاملة
- أين دور الذكاء الاصطناعي؟
- والآن دورك أنت
كيف يعمل هذا الموقع: نظرة تقنية على البنية التقنية (Stack)

في مقال مرافق، كتبت عن سبب إعادة بناء هذا الموقع باستخدام Astro وكيف نمت تلك القرارة إلى مشروع أكبر بكثير. ذلك المقال تحدّث عن الرحلة والتفكير. هذا المقال يتحدث عن النتيجة: كيف يعمل الموقع فعلاً، من مرحلة كتابة المقال حتى ظهوره في متصفح القارئ.
الصورة الإجمالية
في جوهره، الموقع موقع ثابت (ستاتيك). كل صفحة تُولَّد وقت البناء وتُشحن كـHTML وCSS وجافاسكريبت محدود. لا يوجد خادم يعالج الطلبات، ولا قاعدة بيانات، ولا بنية تحتية تعمل في الوقت الفعلي تحتاج إلى صيانة. ما يُسلّمه Cloudflare للقراء هو بالضبط ما أنتجه Astro أثناء عملية البناء.
┌──────────────────────────────────────────────────────────────┐
│ الكاتب │
│ كتابة Markdown/MDX → git push → GitHub │
└───────────────────┬────────────────────────┬─────────────────┘
│ PR (بالتوازي) │ دمج في main
▼ ▼
┌────────────────────┐ ┌───────────────────────────────────┐
│ GitHub Actions │ │ Cloudflare Pages │
│ فحص البناء فقط │ │ بناء → نشر على CDN │
│ (بدون نشر) │ │ PR → معاينة على *.workers.dev │
└────────────────────┘ └──────────────┬────────────────────┘
│
▼
متصفح القارئ
(HTML + CSS + ~0 JS لكل صفحة)
هذه البساطة مقصودة. المواقع الثابتة سريعة ورخيصة وسهلة الفهم. أدواتها البرمجية يمكن أن تكون مثيرة للاهتمام رغم ذلك، وهذا ما يستكشفه هذا المقال.
سلسلة الأدوات: Bun بدلاً من npm
المشروع يتطلب Bun بإصدار >=1.0.0 كبيئة تشغيل JavaScript ومدير حزم. استبدل Bun حزمة npm أثناء الهجرة، والفرق في التجربة اليومية ملحوظ. التثبيت أسرع، ملف القفل (bun.lock) أكثر إيجازاً، والأوامر تبدو أخف لمشروع بهذا الحجم.
سكريبتات package.json تلقائية:
{
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"deploy": "bun run build && wrangler pages deploy dist/ --project-name=website"
}
}
bun run dev يشغّل خادم تطوير محلي مع إعادة تحميل فوري. bun run build ينتج المخرجات الثابتة في dist/. سكريبت deploy هو مخرج محلي احتياطي: عملياً، النشر في الإنتاج يتم تلقائياً عبر تكامل Git في Cloudflare Pages وليس يدوياً.
Astro: ثابت افتراضياً
Astro هو الإطار المسؤول عن تحويل المحتوى والمكوّنات إلى الصفحات التي تُشحن للقراء. الإعدادات تعكس مجموعة واضحة من الأولويات:
// astro.config.mjs
export default defineConfig({
site: "https://ammar-najjar.com",
trailingSlash: "always",
output: "static",
markdown: {
shikiConfig: { theme: "css-variables" },
rehypePlugins: [rehypeImgDimensions],
},
integrations: [
mdx(),
sitemap({
filter: page => !page.includes(".xml") && !page.includes("/404/"),
}),
],
});
output: 'static' يعني أن البناء يُنتج ملفات HTML نقية. الموقع يعمل كلياً كأصول ثابتة على Cloudflare Pages دون الحاجة لمحوّل خادم.
توصيل JavaScript
أحد مبادئ Astro الأساسية هو شحن صفر JavaScript بالافتراض. كل مكوّن .astro يُصيَّر إلى HTML ثابت وقت البناء ولا يرسل شيئاً للمتصفح إلا إذا اخترت صراحةً التفعيل بتوجيه client:*. هذا الموقع لا يستخدم أياً من هذه التوجيهات. كل مكوّن هو HTML يُصيَّر على الخادم بحتاً.
الجافاسكريبت الوحيد الذي يصل فعلاً للقراء هو:
- سكريبت مضمّن صغير في
<head>يقرأlocalStorageويضبطdata-themeقبل أول رسم، مانعاً وميض الثيم الخاطئ - سكريبت يربط زر الوضع الداكن ومبدّل اللغة
- أداة تعليقات Giscus، تُحمَّل بشكل كسول فقط عندما يصل القارئ لأسفل المقال
هذا كل شيء. لا بيئة تشغيل إطار، لا توجد تكاليف إضافية، لا شجرة مكوّنات تُشغَّل في المتصفح. الموقع السابق (Docusaurus المبني على React) كان يشحن بيئة تشغيل React الكاملة وحزمة المكوّنات في كل صفحة بغض النظر عن الحاجة للتفاعلية. الفرق في حجم الحزمة كبير: معظم صفحات هذا الموقع تُوصّل أقل من 10 كيلوبايت من JavaScript، مقارنةً بحزمة React التي كانت 100-200 كيلوبايت وتُحمَّل دون شرط في السابق.
أداء التحميل ومؤشرات Core Web Vitals
تقيس Core Web Vitals ثلاثة أشياء يشعر بها القراء فعلاً: مدى سرعة تحميل أكبر عنصر مرئي (LCP)، ومقدار إزاحة التخطيط أثناء التحميل (CLS)، وسرعة استجابة الصفحة للتفاعل (INP). كل منها يتأثر مباشرةً بقرارات معمارية اتُّخذت في مكان آخر من هذا الإطار.

يؤكد هذه النتيجة تأثير القرارات المعمارية: الحد الأدنى من نقل JavaScript، وعدم وجود موارد تمنع التصيير، والصور المحسّنة تضمن تحميل كل صفحة بشكل شبه فوري.
- LCP — الصورة البارزة في المقال. يقبل
BaseLayoutخاصيةpreloadImageتُصدر<link rel="preload" fetchpriority="high">، تخبر المتصفح بجلبها بأعلى أولوية قبل تحليل متن الصفحة. شبكة CDN من Cloudflare تخدّمها من عقدة حافة قريبة من القارئ. - CLS — يُمنع بثلاثة أشياء: سمات
widthوheightتُحقن وقت البناء (المتصفح يحجز المساحة قبل تحميل الصورة)، خطوط النظام (لا تبديل خطوط ويب)، ولا تصيير على جانب العميل يُدرج عناصر بعد الرسم الأولي. - INP — مع عدم وجود إطار JavaScript يعمل، لا يوجد تنافس على حلقة الأحداث من مطابقة المكوّنات أو الترطيب. السكريبتات الوحيدة الموجودة صغيرة ومركّزة.
تسليط الضوء على الكود يستخدم Shiki مع ثيم css-variables. بدلاً من خبز مخطط ألوان ثابت في المخرجات، تُعبَّر كل ألوان الرموز كخصائص CSS مخصصة. هذا يجعل التبديل بين الوضعين الفاتح والداكن شأناً خالصاً للـCSS: تغيير قيم المتغيرات وتتبع كتل الكود دون جافاسكريبت.
التصميم البصري
الطبقة البصرية كلها CSS، لا إطار. كل الألوان معرَّفة كخصائص CSS مخصصة على :root، مع كتلة override للـ[data-theme='dark'] تبدّل كل متغير عند تفعيل المستخدم للوضع الداكن:
:root {
--color-bg: #ffffff;
--color-text: #1a1a2e;
--color-primary: #003366;
--font-sans: system-ui, -apple-system, sans-serif;
--font-mono: "SF Mono", "Fira Code", monospace;
--max-width: 900px;
}
[data-theme="dark"] {
--color-bg: #0d1117;
--color-text: #c9d1d9;
--color-primary: #3399ff;
}
يقرأ المبدّل localStorage عند تحميل الصفحة، يرجع إلى prefers-color-scheme، ويكتب سمة data-theme على <html>:
const saved = localStorage.getItem("theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (saved === "dark" || (!saved && prefersDark))
document.documentElement.setAttribute("data-theme", "dark");
هذا هو الجافاسكريبت الوحيد المتعلق بالثيمات. الطباعة تستخدم خطوط النظام طوال الوقت: لا خطوط ويب تُحمَّل، لا طلبات تحجب التصيير، لا إزاحة تخطيط. التخطيط محدود بـ--max-width: 900px ويتكيف مع الشاشات الأصغر عبر استعلامات الوسائط. الصور تُضبط على max-width: 100%; height: auto عالمياً، مما يجعل كل صورة متجاوبة دون معالجة لكل صورة على حدة.
مجموعات المحتوى
مجموعات محتوى Astro هي العمود الفقري لطبقة المحتوى. كل مقال يعيش في src/content/ كملف Markdown أو MDX، وكل مجموعة يصفها مخطط Zod يتحقق من الـfrontmatter وقت البناء:
// src/content.config.ts
const blogSchema = z.object({
slug: z.string(),
title: z.string(),
authors: z.string(),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
description: z.string().optional(),
image: z.string().optional(),
imageStyle: z.string().optional(),
draft: z.boolean().default(false),
});
إذا كانت frontmatter المقال تفتقر لحقل مطلوب أو كان من النوع الخاطئ، يفشل البناء. هذا مقصود: فشل البناء أفضل من نشر مقال ببيانات وصفية معطوبة.
مجلدات المحتوى تعكس مباشرةً متغيرات اللغة:
src/content/
blog/ ← مقالات إنجليزية
blog-de/ ← مقالات ألمانية
blog-ar/ ← مقالات عربية
books/ ← ملاحظات قراءة إنجليزية
books-de/ ← ملاحظات قراءة ألمانية
books-ar/ ← ملاحظات قراءة عربية
authors/ ← ملفات YAML بيانات المؤلفين
محتوى كل لغة هو مجموعة خاصة به مسجّلة تحت اسم مختلف، لكنها تشترك كلها في نفس المخطط. مساعد collectionName() في src/i18n/collections.ts يتعامل مع اتفاقية التسمية:
export function collectionName(type: ContentType, locale: Locale): string {
if (locale === "en") return type;
return `${type}-${locale}`;
}
فـcollectionName('blog', 'ar') يُعيد 'blog-ar'، وطبقة الصفحات تستعلم المجموعة الصحيحة بناءً على اللغة التي تُصيَّر من أجلها.
حقلا tags وauthors في المخطط يقودان طبقة الاكتشاف.
الوسوم لها غرضان. للقراء، هي ملاحة: قارئ يصل لمقال عن Cloudflare ويريد المزيد حول هذا الموضوع يمكنه اتباع الوسم لصفحة قائمة مخصصة على /blog/tags/cloudflare/ تعرض كل ما وُسِم بنفس الطريقة. لمحركات البحث، صفحات الوسوم هي URLs مركّزة على موضوع بعناوين كانونية خاصة، تركّز إشارات الصلة حول موضوع بدلاً من تشتيتها عبر مقالات غير مترابطة. كل وسم مستخدم في أي مقال يحصل تلقائياً على صفحته الخاصة وقت البناء عبر getStaticPaths() — لا تنظيم يدوي مطلوب.
حقل authors يربط المقالات بملفات تعريف المؤلفين على /authors/:author/. الترقيم الصفحي على /blog/page/:n/ يمنع فهرس المدونة من النمو بلا حدود مع تنامي الأرشيف. معاً يمنحان القراء مسارات متعددة للوصول إلى المحتوى تتجاوز تغذية زمنية واحدة.
التوجيه والصفحات
التوجيه القائم على الملفات في Astro يترجم بنية المجلدات مباشرةً إلى URLs. شجرة src/pages/ تعكس بنية URL الموقع:
src/pages/
index.astro → /
about.astro → /about/
blog/[...slug].astro → /blog/:slug/
blog/tags/[tag].astro → /blog/tags/:tag/
blog/page/[page].astro → /blog/page/:n/
books/[...slug].astro → /books/:slug/
authors/[author].astro → /authors/:author/
rss.xml.js → /rss.xml
ar/ → /ar/* (مرآة اللغة العربية)
de/ → /de/* (مرآة اللغة الألمانية)
المسارات الديناميكية مثل [...slug].astro تستخدم getStaticPaths() لحصر كل مقال وقت البناء:
export async function getStaticPaths() {
const posts = await getPublishedCollection("blog");
return posts.map(post => ({
params: { slug: post.id },
props: { slug: post.id, id: post.id },
}));
}
وقت البناء، يستدعي Astro getStaticPaths() لكل صفحة ديناميكية، يجمع كل مجموعات المعاملات، ويُصيّر كلاً منها إلى ملف HTML ثابت. النتيجة مجلد dist/ يشبه تماماً بنية URL الموقع الحي.
تعدد اللغات
الموقع يدعم ثلاث لغات: الإنجليزية (الافتراضية)، والألمانية (/de/)، والعربية (/ar/). التنفيذ صريح لا سحري: لا اكتشاف تلقائي للغة، لا إعادة توجيه من جانب الخادم بناءً على ترويسات المتصفح. كل لغة هي مجلد متوازٍ من الصفحات تستعلم مجموعتها الخاصة.
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ /blog/ │ │ /de/blog/ │ │ /ar/blog/ │
│ (إنجليزي) │ │ (ألماني) │ │ (عربي) │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
▼ ▼ ▼
blog collection blog-de collection blog-ar collection
مبدّل اللغة في شريط التنقل يتيح للقراء تغيير اللغة يدوياً. يقبل BaseLayout خاصية dir )'ltr' أو 'rtl') ليحصل الموقع العربي على اتجاه نص صحيح وانعكاس تخطيط على مستوى HTML دون أي جافاسكريبت على جانب العميل.
مساعدات اللغة تضمن بناء URL متسقاً في قاعدة الكود:
export function localizedPrefix(locale: Locale): string {
return locale === "en" ? "" : `/${locale}`;
}
export function localizedUrl(path: string, locale: Locale): string {
const prefix = localizedPrefix(locale);
return prefix === "" && path.startsWith("/") ? path : `${prefix}${path}`;
}
المسارات الإنجليزية تبقى دون تغيير؛ المسارات الألمانية والعربية تكسب بادئة لغتها.
معالجة الصور
الصور تعيش في مكانين. صور المحتوى (ترويسات المقالات، المخططات، لقطات الشاشة) مرافقة لمقالها داخل src/content/، مما يعني أن كل مجلد مقال مكتفٍ بذاته:
src/content/blog/every-camera-tells-a-story/
index.md
header.webp
الصور الثابتة (صورة الملف الشخصي، أيقونات التواصل الاجتماعي، الرموز المفضلة) تعيش في public/img/ وتُنسخ إلى المخرجات كما هي.
التحسين والمتغيرات المتجاوبة
صور المحتوى المشار إليها في المكوّنات تستخدم مكوّن <Image> المدمج في Astro من astro:assets. وقت البناء يُحسّن الملف المصدر تلقائياً، يحوّله لصيغة حديثة (WebP بالافتراض)، ويولّد srcset بمتغيرات أحجام متعددة لكي يُحمّل المتصفح فقط الدقة التي يحتاجها:
import { Image } from 'astro:assets';
<Image
src={headerImage}
alt={title}
width={120}
loading="lazy"
/>
المكوّن أيضاً يُصدر سمات width وheight على <img> المُخرَج، يستخدمها المتصفح لحجز المساحة قبل تحميل الصورة، ما يُلغي إزاحة التخطيط.
حقن الأبعاد التلقائي
للصور المكتوبة مباشرةً في Markdown (بدلاً من مكوّن)، إضافة Rehype مخصصة تتولى حقن الأبعاد وقت البناء. تقرأ width وheight من الملف الفعلي باستخدام Sharp وتحقنها على كل عنصر <img> يشير لملف في public/img/:
// src/plugins/rehype-img-dimensions.mjs
async function getDimensions(src) {
if (!src || !src.startsWith("/img/")) return null;
const filePath = join(projectRoot, "public", src);
const { width, height } = await sharp(filePath).metadata();
return width && height ? { width, height } : null;
}
الإضافة تعالج صيغة الصور القياسية في Markdown () وسوم <img> المكتوبة كـHTML مضمّن. النتيجة نفس مكوّن <Image>: المتصفح يعرف الأبعاد قبل وصول الصورة، وإزاحة التخطيط تُلغى دون إدارة يدوية للسمات.
SEO وقابلية الاكتشاف
عدة أنظمة تعمل معاً لجعل المحتوى قابلاً للعثور عليه.
HTML head
BaseLayout.astro هو مكوّن التخطيط الوحيد الذي يُغلّف كل صفحة. يدير <head> بمجموعة متسقة من الوسوم:
<title>{fullTitle}</title>
<meta name="description" content="{description}" />
<meta property="og:title" content="{fullTitle}" />
<meta property="og:image" content="{image}" />
<link rel="canonical" href="https://ammar-najjar.com{pathname}" />
<link rel="alternate" hreflang="{locale}" href="..." />
<link rel="alternate" type="application/rss+xml" href="/rss.xml" />
مكوّن تخطيط واحد يتعامل مع كل هذا يضمن الاتساق: لا مقال يُشحن بدون عنوان، لا صفحة تفتقر لـURL الكانونية.
العنوان والوصف يظهران مباشرةً في نتائج البحث ومعاينات الروابط الاجتماعية. وسوم Open Graph تتحكم في كيفية ظهور الرابط المشارك على المنصات الاجتماعية؛ بدونها تخمّن المنصات، وغالباً بشكل سيئ. URL الكانونية إشارة موثوقة لمحركات البحث حول العنوان الأصلي لكل صفحة، تمنع الغموض من الشرطات المائلة في النهاية أو إعادة التوجيه. hreflang يُخبر محركات البحث بالنسخة اللغوية التي تُعرض لأي جمهور؛ بدونه قد يصل قارئ عربي للنسخة الإنجليزية حتى لو كانت النسخة العربية موجودة. RSS alternate موجود في كل صفحة ليتمكن قراء التغذيات من اكتشاف رابط الاشتراك دون الحاجة للبحث عنه.
خريطة الموقع
خريطة الموقع تسلّم الزاحفين قائمة كاملة بكل URL على الموقع بدلاً من الاعتماد على متابعة الروابط. لموقع متعدد اللغات هذا أهم من مدونة بسيطة: النسخ الإنجليزية والألمانية والعربية من كل مقال تعيش تحت URLs منفصلة، وزاحف يتبع الروابط الداخلية فقط قد لا يصل لكلها.
تكامل @astrojs/sitemap يولّد sitemap-index.xml وقت البناء من كل صفحة ثابتة، مُرشِّحاً صفحات 404 وURLs الـXML. يُشار إليه في robots.txt وترويسة Link في كل صفحة، فيجده الزاحفون بغض النظر عن كيفية وصولهم.
إعادة توجيه تتعامل مع المسار التقليدي:
/sitemap.xml → /sitemap-index.xml (301)
تغذيات RSS
الموقع ينشر تغذيتين RSS:
/rss.xml: مقالات المدونة، مرتبة بالتاريخ تنازلياً/books/rss.xml: ملاحظات القراءة، مرتبة بالتاريخ تنازلياً
كل منهما يُولَّد بنقطة نهاية Astro (rss.xml.js) تستعلم المجموعة المنشورة وتُعيد تغذية منسقة بشكل صحيح باستخدام @astrojs/rss.
robots.txt
User-agent: *
Allow: /
Content-Signal: ai-train=no, search=yes, ai-input=yes
Sitemap: https://ammar-najjar.com/sitemap-index.xml
robots.txt من أقدم الاتفاقيات على الويب وأول شيء يتحقق منه كل زاحف. الملف هنا يفتح الموقع كاملاً لجميع الزاحفين، يشير للخريطة، ويضيف توجيه Content-Signal. هذا الجزء الأخير امتداد مقترح للتعبير عن نية ترخيص الذكاء الاصطناعي: ai-train=no يطلب عدم استخدام المحتوى لتدريب النماذج؛ ai-input=yes يسمح باستخدامه في التوليد المعزز بالاسترجاع حيث النموذج يُجيب على سؤال محدد بدلاً من استيعاب المحتوى في أوزانه. غير قابل للتنفيذ قانونياً، لكنه يُعبّر عن النية بوضوح في مكان تزوره الزاحفات بالفعل.
llms.txt
robots.txt يُخبر الزاحفين بما يمكنهم الوصول إليه. llms.txt يُخبر نماذج اللغة بما يحتويه الموقع فعلاً.
عندما يُسأل مساعد ذكاء اصطناعي عن شخص أو موقع، يمكنه إما استرجاع شيء من بيانات التدريب (مجمّد في الوقت، قد يكون غير دقيق) أو الزحف على الموقع (بطيء، مكلف). llms.txt يقدم خياراً ثالثاً: ملخص نصي منظّم على مسار معروف يغطي هوية المؤلف وفئات المحتوى واللغات المتاحة. llms-full.txt النسخة الموسّعة، مُنظَّمة بالموضوع مع أوصاف للمقالات الفردية. نموذج يقرأ أياً منهما أولاً يمكنه الإجابة بدقة دون جلب كل صفحة. الاتفاقية لا تزال ناشئة، لكن المشكلة التي تحلها حقيقية.
النشر: GitHub + Cloudflare Pages
سير عمل النشر يربط ثلاثة أجزاء: محرر نصوص محلي، وGitHub للتحكم في الإصدارات، وCloudflare Pages كمنصة بناء وتوصيل. قصة كيفية وسبب انتقال الاستضافة من GitHub Pages إلى Cloudflare، بما في ذلك مقارنة مسجلي النطاقات والعقبات التي واجهت التحويل، مغطاة في مقال منفصل.
┌───────────────┐ git push ┌──────────────────────────────┐
│ محلي │ ────────────────► │ GitHub │
│ (كتابة + │ │ فرع main → إنتاج │
│ commit) │ │ فرع PR → بناء معاينة │
└───────────────┘ └───────┬──────────────┬───────┘
│ PR │ دمج في main
▼ ▼
┌─────────────────────────────────────────────┐
│ Cloudflare Pages │
│ bun install + bun run build │
│ │
│ PR → معاينة على *.workers.dev │
│ (Actions يشغّل فحص البناء بالتوازي) │
│ main → نشر dist/ على CDN الإنتاج │
└─────────────────────────────────────────────┘
Cloudflare Pages يراقب مستودع GitHub. الدفع إلى main يُشغّل بناءً ونشراً للإنتاج. فتح pull request يُشغّل بناء معاينة: نشر منفصل على URL فريد، لمراجعة التغييرات بصرياً قبل الدمج.
CI: GitHub Actions
يوجد سير عمل GitHub Actions واحد، test-deploy.yml، يعمل على كل pull request ضد main:
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install --frozen-lockfile
- run: bun run build
وظيفته الوحيدة التحقق من نجاح astro build. لا ينشر شيئاً. إذا فشل البناء على PR، يُحجب الـPR قبل أن يصل نشر معطوب لـCloudflare Pages. إذا نجح البناء، يتولى Cloudflare Pages النشر الفعلي كخطوة منفصلة مُشغَّلة بتكامل Git الخاص به. كل فرع أيضاً يحصل تلقائياً على نشر معاينة حي على نطاق فرعي *.workers.dev، لمراجعة التغييرات في متصفح حقيقي على URL قابل للمشاركة قبل الدمج.
ترويسات HTTP
Cloudflare يقرأ public/_headers لتطبيق ترويسات الاستجابة على كل URL مُنشر. الاستراتيجية تعكس ممارسة التخزين المؤقت القياسية للمواقع الثابتة:
/_astro/*
Cache-Control: public, max-age=31536000, immutable
/img/*
Cache-Control: public, max-age=31536000, immutable
/*
Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Astro يُخرج أسماء ملفات مجزأة لكل الأصول المحزومة (مثل /_astro/main.Bx9qKt2a.css). لأن اسم الملف يتغير كلما تغيّر المحتوى، يمكن تخزين هذه الملفات مؤقتاً إلى الأبد (max-age=31536000, immutable) دون خطر رؤية القراء لـCSS أو جافاسكريبت قديمة. صفحات HTML، التي لا تملك تجزئة محتوى، تستخدم max-age أقصر لمدة ساعة مع نافذة stale-while-revalidate حتى يتمكن CDN من Cloudflare من تقديم صفحات قديمة قليلاً بينما يُعيد التحقق في الخلفية.
ترويسات الأمان تُطبَّق عالمياً: X-Content-Type-Options يمنع استنشاق MIME، X-Frame-Options: DENY يحجب تضمين الموقع في iframes، وPermissions-Policy يعطّل صراحةً الوصول للموقع الجغرافي والميكروفون والكاميرا.
إعادة التوجيه
public/_redirects يتعامل مع إعادات التوجيه الدائمة للـURLs التي تغيّرت أثناء الهجرة. الموقع القديم كان له صفحات منفصلة /experience/ و/skills/؛ أُزيلت ودُمج محتواها في /about/. إعادات التوجيه تحفظ أي روابط واردة وتُمرّر إشارات محركات البحث للـURLs الجديدة:
/sitemap.xml /sitemap-index.xml 301
/blog/archive/ /blog/ 301
/experience/ /about/ 301
/ar/experience/ /ar/about/ 301
/de/experience/ /de/about/ 301
/skills/ /about/ 301
/ar/skills/ /ar/about/ 301
/de/skills/ /de/about/ 301
هذه تضمن أن الروابط من مواقع أخرى تشير لـURLs قديمة لا تزال تُحلّ بشكل صحيح بعد إعادة الهيكلة، وأن أي إدخالات في فهرس محركات البحث للمسارات القديمة تُمرّر إشاراتها للمسارات الجديدة.
التعليقات: Giscus
التعليقات مدعومة بـGiscus، الذي يخزّن النقاشات في ميزة Discussions لمستودع GitHub. لا قاعدة بيانات تعليقات منفصلة ولا منصة خارجية متضمّنة. القراء يحتاجون حساب GitHub للتعليق، وهذا يناسب الجمهور التقني الذي يميل الموقع لاستقطابه.
مكوّن GiscusComments يحمّل سكريبت Giscus بشكل كسول باستخدام IntersectionObserver:
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
loadGiscus();
observer.disconnect();
}
},
{ rootMargin: "200px" },
);
أداة التعليقات تُهيَّأ فقط عندما يصل القارئ لمسافة 200 بكسل من أسفل الصفحة. هذا يتجنب تحميل سكريبت خارجي في كل مشاهدة صفحة. فقط القراء الذين يصلون فعلاً لنهاية مقال يُشغّلون طلب الشبكة.
الصورة الكاملة
┌──────────────────────────────────────────────────────────────┐
│ الكتابة │
│ src/content/blog/<slug>/index.md (Markdown + frontmatter) │
│ src/content/blog-de/<slug>/index.md (ألماني) │
│ src/content/blog-ar/<slug>/index.md (عربي) │
└──────────────────────────┬───────────────────────────────────┘
│ تُتحقق بمخطط Zod وقت البناء
▼
┌──────────────────────────────────────────────────────────────┐
│ البناء (bun run build) │
│ ├─ Astro يجمع كل إدخالات مجموعات المحتوى │
│ ├─ getStaticPaths() يولّد مساراً لكل مقال × لغة │
│ ├─ <Image> يحسّن + يولّد srcset لصور المحتوى │
│ ├─ إضافة Rehype تحقن width/height للصور عبر Sharp │
│ ├─ Shiki يُبرز الكود (CSS variables، بدون JS) │
│ ├─ @astrojs/sitemap يكتب sitemap-index.xml │
│ └─ dist/ يحتوي ملفات HTML مسطّحة تطابق بنية URL │
└──────────────────────────┬───────────────────────────────────┘
│ git push to main
▼
┌───────────────────────────────────────────────────────────────┐
│ Cloudflare Pages │
│ ├─ يشغّل نفس البناء (bun install + bun run build) │
│ ├─ ينشر dist/ على حافة CDN العالمية │
│ ├─ يطبّق _headers (cache + أمان) │
│ └─ يطبّق _redirects (حفظ URLs القديمة) │
└──────────────────────────┬────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ متصفح القارئ │
│ ├─ HTML من حافة CDN (أقل من 100ms عالمياً) │
│ ├─ CSS مع CSS variables من Shiki لكتل كود واعية بالثيم │
│ ├─ أصول مجزأة مخزّنة مؤقتاً لسنة │
│ └─ Giscus يُحمَّل عند التمرير (تعليقات، بدون JS متسرّع) │
└───────────────────────────────────────────────────────────────┘
كل جزء من هذه البنية يعكس نسخة من نفس المبدأ: أنجز أكبر قدر ممكن وقت البناء، وصّل أقل قدر ممكن وقت التشغيل، واجعل البنية التحتية بسيطة بما يكفي لفهمها كلياً. Astro يتولى البناء، Cloudflare يتولى التوصيل، والفجوة بين كتابة مقال وظهوره على الإنترنت هي بالضبط git push واحدة.
أين دور الذكاء الاصطناعي؟
قد تتساءل: في عام 2026، أين يقع الذكاء الاصطناعي من كل هذا؟ الإجابة المختصرة هي أن الذكاء الاصطناعي ليس جزءاً من بيئة التشغيل (runtime stack) للموقع؛ فلا توجد واجهات برمجة تطبيقات (APIs) للذكاء الاصطناعي يتم استدعاؤها أثناء مرحلة البناء، ولا توجد نماذج تعلم آلي مضمنة في الصفحة، ولا أدوات دردشة آلية (chatbots). ومع ذلك، لعب الذكاء الاصطناعي دوراً محورياً في كيفية بناء الموقع.
لقد استخدمتُ سير عمل تطوير يعتمد على المواصفات (spec-driven development) ومدعوماً بإعداد محلي للذكاء الاصطناعي: حيث استُخدم LM Studio لتشغيل النموذج، وOpenCode كمساعد برمجي ذكي (agentic coding assistant)، وQwen 3.5 كالنموذج المحلي الذي يعمل على جهاز MacBook Pro. الفكرة بسيطة: قبل كتابة أي كود برمجي، تصف ما تريده بلغة طبيعية، وتترك الوكيل الذكي (AI agent) يستكشف قاعدة الكود، ويخطط لآلية التنفيذ ثم يطبقها؛ كل ذلك دون مغادرة واجهة الأوامر (terminal) ودون إرسال سطر كود واحد إلى واجهة برمجة تطبيقات سحابية.
لقد صاغ هذا النهج العديد من القرارات المتعلقة بالبنية التقنية التي تراها أعلاه. فقد تم تطوير كل من مجموعات المحتوى (content collections)، وأدوات دعم التدويل (i18n helpers)، وإضافة Rehype، ومسار النشر (deployment pipeline)، من خلال محادثات قائمة على المواصفات مع نموذج محلي. وتولى الذكاء الاصطناعي مهام استكشاف المستودع البرمجي، وتوليد الأكواد الأساسية المتكررة (boilerplate)، واقتراح أنماط معمارية، كما ساعد في كتابة هذا المقال ومراجعة دقته. وللحصول على نظرة أعمق حول النماذج والأدوات التي تعمل فعلياً على أجهزة ذات موارد محدودة، ولماذا تُعد قدرات الوكيل الذكي (agent capability) أكثر أهمية من درجات اختبارات الأداء البرمجي (benchmarks)، يمكنك الاطلاع على مقالي المصاحب حول إعداد بيئة تطوير محلية تعتمد على الذكاء الاصطناعي.
والآن دورك أنت
هل لديك أيضًا موقعك الشخصي؟ يسعدني أن أعرف الخيارات التي اتخذتها أثناء بنائه. هل اعتمدت على موقع ثابت أم على Server Rendering؟ هل تستضيفه بنفسك أم تستخدم خدمة استضافة مُدارة؟ هل كان هدفك البساطة، أم كثرة الميزات، أم شيئًا مختلفًا تمامًا؟ ولو بدأت اليوم من جديد، فما الذي ستفعله بشكل مختلف؟
اترك تعليقًا في الأسفل. أقرأ جميع التعليقات.