Astro 5: بناء موقع محتوى فائق السرعة باستخدام هندسة الجُزر

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

مقدمة

في عالم ويب غارق بالـ JavaScript، يتخذ Astro نهجاً مختلفاً جذرياً: لا يُرسل أي JavaScript بشكل افتراضي. أصدر Astro 5 في أواخر 2025 مضاعفاً الرهان على هذه الفلسفة مع طبقات المحتوى، والجُزر الخادمية، وتجربة مطوّر محسّنة تجعله أحد أقوى أُطر العمل لمواقع المحتوى في 2026.

إذا كنت تبني مدوّنة، أو موقع توثيق، أو صفحة تسويقية، أو معرض أعمال، فإن Astro يقدّم شيئاً نادراً — درجات Lighthouse مثالية دون مجهود تحسين خارق. هذا الدرس يأخذك خطوة بخطوة لبناء موقع محتوى متكامل من الصفر، مستفيداً من هندسة الجُزر في Astro 5 لترطيب المكوّنات التفاعلية فقط عند الحاجة.

ما الذي ستبنيه

مدوّنة تقنية متكاملة تتضمّن:

  • عرض ثابت أولاً لتحميل فوري للصفحات
  • مكوّنات تفاعلية بـ React (تُرطَّب عند الطلب)
  • مجموعات محتوى مع Markdown/MDX آمنة الأنواع
  • توليد صور OG ديناميكية
  • نشر على Cloudflare Pages

المتطلبات المسبقة

قبل أن نبدأ، تأكّد من توفّر:

  • Node.js 20+ مثبّت (تحقق بـ node -v)
  • npm أو pnpm (سنستخدم pnpm في هذا الدرس)
  • معرفة أساسية بـ HTML وCSS وJavaScript
  • إلمام بأُطر العمل المبنية على المكوّنات (React أو Vue أو Svelte)
  • محرر أكواد (يُنصح بـ VS Code مع إضافة Astro)

💡 نصيحة: إذا كنت قادماً من Next.js أو Nuxt، سيبدو Astro مألوفاً — لكن النموذج الذهني مختلف جوهرياً. فكّر فيه كإطار عمل يبدأ بالـ HTML ويختار JavaScript عند الحاجة، وليس العكس.


الخطوة 1: إنشاء مشروع Astro

افتح الطرفية ونفّذ:

pnpm create astro@latest my-tech-blog

سيسألك معالج الإعداد بعض الأسئلة:

Where should we create your new project? → ./my-tech-blog
How would you like to start your new project? → Empty
Install dependencies? → Yes
Do you plan to write TypeScript? → Yes (Strict)
Initialize a new git repository? → Yes

ادخل إلى المشروع:

cd my-tech-blog

هيكل المشروع يبدو هكذا:

my-tech-blog/
├── astro.config.mjs
├── package.json
├── public/
│   └── favicon.svg
├── src/
│   └── pages/
│       └── index.astro
└── tsconfig.json

شغّل خادم التطوير:

pnpm dev

زُر http://localhost:4321 — سترى صفحة ترحيبية بسيطة.


الخطوة 2: فهم هندسة الجُزر

قبل كتابة المزيد من الكود، لنفهم الابتكار الجوهري في Astro.

تطبيقات الصفحة الواحدة التقليدية مقابل جُزر Astro

في تطبيق React أو Next.js تقليدي، الصفحة بأكملها هي تطبيق JavaScript. حتى لو كان 90% من صفحتك محتوى ثابت، يقوم المتصفح بتحميل وتحليل وتنفيذ JavaScript للكل.

Astro يقلب هذا النموذج:

  1. كل صفحة هي HTML ثابت افتراضياً — لا يُرسَل JavaScript
  2. المكوّنات التفاعلية هي "جُزر" — جيوب معزولة من JavaScript في بحر من HTML الثابت
  3. كل جزيرة تُرطَّب بشكل مستقل — أنت تتحكم في التوقيت والطريقة
┌─────────────────────────────────────────┐
│        HTML ثابت (بدون JS)              │
│  ┌─────────────┐    ┌──────────────┐   │
│  │  React       │    │  Svelte      │   │
│  │  عدّاد       │    │  بحث         │   │
│  │  (جزيرة)     │    │  (جزيرة)     │   │
│  └─────────────┘    └──────────────┘   │
│                                         │
│        HTML ثابت (بدون JS)              │
└─────────────────────────────────────────┘

توجيهات الترطيب

يوفّر Astro توجيهات للتحكم الدقيق في وقت ترطيب الجزيرة:

التوجيهمتى يتم الترطيب
client:loadفوراً عند تحميل الصفحة
client:idleعندما يكون المتصفح في وضع الخمول
client:visibleعندما يظهر المكوّن في نافذة العرض
client:mediaعندما يتحقق استعلام CSS media
client:onlyيتخطى SSR ويعرض على العميل فقط

هذا التحكم الدقيق هو ما يجعل مواقع Astro سريعة جداً — أنت تدفع ثمن JavaScript الذي تحتاجه فعلاً فقط.


الخطوة 3: إعداد هيكل المشروع

لننظّم مدوّنتنا بشكل صحيح. أنشئ المجلدات التالية:

mkdir -p src/{components,layouts,content/blog,styles}

تثبيت Tailwind CSS

لدى Astro تكامل ممتاز مع Tailwind:

pnpm astro add tailwind

هذا يهيّئ Tailwind تلقائياً وينشئ ملف tailwind.config.mjs. حدّثه لمدوّنتنا:

// tailwind.config.mjs
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  theme: {
    extend: {
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
      colors: {
        accent: {
          50: '#fef3f2',
          100: '#fee4e2',
          500: '#ef4444',
          600: '#dc2626',
          700: '#b91c1c',
        },
      },
    },
  },
  plugins: [require('@tailwindcss/typography')],
}

ثبّت إضافة Typography:

pnpm add -D @tailwindcss/typography

أنشئ ملف الأنماط العامة:

/* src/styles/global.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
 
@tailwind base;
@tailwind components;
@tailwind utilities;
 
@layer base {
  html {
    scroll-behavior: smooth;
  }
 
  body {
    @apply bg-zinc-950 text-zinc-100 antialiased;
  }
}

الخطوة 4: إنشاء القالب الأساسي

أنشئ القالب الرئيسي الذي ستستخدمه كل الصفحات:

---
// src/layouts/BaseLayout.astro
interface Props {
  title: string;
  description?: string;
  ogImage?: string;
}
 
const {
  title,
  description = 'مدوّنة تقنية حديثة مبنية بـ Astro 5',
  ogImage = '/og-default.png',
} = Astro.props;
 
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
 
<!doctype html>
<html lang="ar" dir="rtl">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <link rel="canonical" href={canonicalURL} />
 
    <title>{title}</title>
    <meta name="description" content={description} />
 
    <!-- Open Graph -->
    <meta property="og:type" content="website" />
    <meta property="og:url" content={canonicalURL} />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={new URL(ogImage, Astro.site)} />
  </head>
  <body class="min-h-screen flex flex-col">
    <header class="border-b border-zinc-800 sticky top-0 z-50 bg-zinc-950/80 backdrop-blur-md">
      <nav class="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
        <a href="/" class="text-xl font-bold tracking-tight hover:text-accent-500 transition-colors">
          ⚡ مدوّنة تِك
        </a>
        <div class="flex items-center gap-6 text-sm">
          <a href="/blog" class="hover:text-accent-500 transition-colors">المقالات</a>
          <a href="/about" class="hover:text-accent-500 transition-colors">حول</a>
        </div>
      </nav>
    </header>
 
    <main class="flex-1">
      <slot />
    </main>
 
    <footer class="border-t border-zinc-800 py-8 mt-16">
      <div class="max-w-4xl mx-auto px-4 text-center text-sm text-zinc-500">
        <p>مبني بـ Astro 5 · هندسة الجُزر · صفر JavaScript افتراضياً</p>
      </div>
    </footer>
  </body>
</html>

⚠️ تنبيه: لاحظ dir="rtl" و lang="ar" — ضروريان لعرض المحتوى العربي بشكل صحيح. تأكّد أيضاً من ضبط Astro.site في الإعدادات.


الخطوة 5: إعداد مجموعات المحتوى

قدّم Astro 5 واجهة طبقة المحتوى (Content Layer API)، ترقية قوية على مجموعات المحتوى السابقة. لنُعدّها:

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
 
const blog = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishedAt: z.coerce.date(),
    updatedAt: z.coerce.date().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    coverImage: z.string().optional(),
    author: z.string().default('مجهول'),
  }),
});
 
export const collections = { blog };

أنشئ تدوينة تجريبية:

---
# src/content/blog/hello-islands.md
title: "فهم هندسة الجُزر"
description: "غوص عميق في لماذا تُغيّر هندسة الجُزر كل شيء لمواقع المحتوى"
publishedAt: 2026-02-20
tags: ["astro", "هندسة", "أداء"]
author: "كاتب تقني"
coverImage: "/images/islands-cover.jpg"
---
 
## لماذا الجُزر مهمة
 
الويب لديه مشكلة JavaScript. الموقع المتوسط يُرسل **أكثر من 500 كيلوبايت من JavaScript**،
معظمها موجود لعرض محتوى ثابت لا يتغير أبداً.
 
هندسة الجُزر تحل هذه المشكلة بمعاملة التفاعلية كاستثناء وليس كقاعدة.
 
### تأثير الأداء
 
عندما تُرسل JavaScript أقل، كل شيء يصبح أسرع:
 
- **أول رسم محتوى (FCP)** ينخفض بشكل كبير
- **وقت التفاعل (TTI)** يقترب من الصفر للمحتوى الثابت
- **إزاحة التصميم التراكمية (CLS)** تتحسن لأن المكوّنات لا تُعاد رسمها بعد الترطيب
 
```javascript
// هذا المكوّن لا يُرسل أي JavaScript
// يُعرَض كـ HTML ثابت وقت البناء
const StaticHero = () => (
  <section className="hero">
    <h1>مرحباً بكم في المستقبل</h1>
    <p>هذا مجرد HTML. لا حاجة لـ JS.</p>
  </section>
);

الجمال يكمن فيما لا تُرسله.


أنشئ تدوينة ثانية:

```md
---
# src/content/blog/astro-vs-nextjs.md
title: "Astro مقابل Next.js: متى تختار أيهما"
description: "مقارنة عملية لمساعدتك في اختيار الأداة المناسبة لمشروعك"
publishedAt: 2026-02-22
tags: ["astro", "nextjs", "مقارنة"]
author: "كاتب تقني"
---

## قرار اختيار الإطار

ليس كل مشروع يحتاج نفس الأداة. إليك متى يتألق كل إطار...

### اختر Astro عندما:
- موقعك في الأساس محتوى (مدوّنات، توثيق، تسويق)
- الأداء أولوية قصوى
- تريد مزج أُطر واجهة المستخدم
- معظم صفحاتك ثابتة أو نادراً ما تتغير

### اختر Next.js عندما:
- تبني تطبيق ويب (لوحات تحكم، SaaS)
- تحتاج تفاعلية كثيفة من جانب العميل
- فريقك مستثمر بالفعل في React
- تحتاج أنماط جلب بيانات متقدمة (ISR, streaming)

الخلاصة الأساسية: **Astro و Next.js يحلّان مشاكل مختلفة.**

الخطوة 6: بناء صفحة قائمة المدوّنة

أنشئ صفحة فهرس المدوّنة التي تجلب وتعرض كل التدوينات:

---
// src/pages/blog/index.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
 
const posts = (await getCollection('blog'))
  .filter((post) => !post.data.draft)
  .sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf());
---
 
<BaseLayout title="المدوّنة | مدوّنة تِك" description="أحدث المقالات في تطوير الويب والتكنولوجيا">
  <div class="max-w-4xl mx-auto px-4 py-12">
    <h1 class="text-4xl font-bold mb-2">المدوّنة</h1>
    <p class="text-zinc-400 mb-12">أفكار حول تطوير الويب والأداء والأدوات الحديثة.</p>
 
    <div class="space-y-8">
      {posts.map((post) => (
        <article class="group border border-zinc-800 rounded-xl p-6 hover:border-zinc-600 transition-colors">
          <a href={`/blog/${post.id}`} class="block">
            <div class="flex items-center gap-2 text-sm text-zinc-500 mb-3">
              <time datetime={post.data.publishedAt.toISOString()}>
                {post.data.publishedAt.toLocaleDateString('ar-SA', {
                  year: 'numeric',
                  month: 'long',
                  day: 'numeric',
                })}
              </time>
              <span>·</span>
              <span>{post.data.author}</span>
            </div>
 
            <h2 class="text-2xl font-semibold group-hover:text-accent-500 transition-colors mb-2">
              {post.data.title}
            </h2>
 
            <p class="text-zinc-400 mb-4">{post.data.description}</p>
 
            <div class="flex flex-wrap gap-2">
              {post.data.tags.map((tag) => (
                <span class="text-xs px-2 py-1 rounded-full bg-zinc-800 text-zinc-400">
                  #{tag}
                </span>
              ))}
            </div>
          </a>
        </article>
      ))}
    </div>
  </div>
</BaseLayout>

الخطوة 7: إنشاء صفحات التدوينات الديناميكية

أنشئ صفحات التدوينات الفردية باستخدام التوجيه الديناميكي:

---
// src/pages/blog/[id].astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection, render } from 'astro:content';
 
export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { id: post.id },
    props: { post },
  }));
}
 
const { post } = Astro.props;
const { Content } = await render(post);
---
 
<BaseLayout title={`${post.data.title} | مدوّنة تِك`} description={post.data.description}>
  <article class="max-w-3xl mx-auto px-4 py-12">
    <header class="mb-10">
      <div class="flex items-center gap-2 text-sm text-zinc-500 mb-4">
        <time datetime={post.data.publishedAt.toISOString()}>
          {post.data.publishedAt.toLocaleDateString('ar-SA', {
            year: 'numeric',
            month: 'long',
            day: 'numeric',
          })}
        </time>
        <span>·</span>
        <span>{post.data.author}</span>
      </div>
 
      <h1 class="text-4xl md:text-5xl font-bold leading-tight mb-4">
        {post.data.title}
      </h1>
 
      <p class="text-xl text-zinc-400">
        {post.data.description}
      </p>
 
      <div class="flex flex-wrap gap-2 mt-6">
        {post.data.tags.map((tag) => (
          <a
            href={`/tags/${tag}`}
            class="text-sm px-3 py-1 rounded-full bg-zinc-800 text-zinc-400 hover:bg-zinc-700 transition-colors"
          >
            #{tag}
          </a>
        ))}
      </div>
    </header>
 
    <div class="prose prose-invert prose-lg max-w-none
                prose-headings:font-semibold
                prose-a:text-accent-500 prose-a:no-underline hover:prose-a:underline
                prose-code:text-accent-500 prose-code:bg-zinc-800 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded
                prose-pre:bg-zinc-900 prose-pre:border prose-pre:border-zinc-800">
      <Content />
    </div>
  </article>
</BaseLayout>

💡 نصيحة: فئات prose من @tailwindcss/typography تتعامل مع كل تنسيقات Markdown. متغير prose-invert مُصمم للخلفيات الداكنة.


الخطوة 8: إضافة جُزر تفاعلية (مكوّنات React)

الآن الجزء المثير — إضافة جُزر تفاعلية. أولاً، أضف تكامل React:

pnpm astro add react

إنشاء مكوّن البحث

هذه حالة استخدام كلاسيكية للجزيرة — مربع البحث يحتاج JavaScript من جانب العميل، لكن باقي الصفحة لا يحتاج:

// src/components/SearchDialog.tsx
import { useState, useEffect, useRef } from 'react';
 
interface SearchResult {
  id: string;
  title: string;
  description: string;
  tags: string[];
}
 
export default function SearchDialog({ posts }: { posts: SearchResult[] }) {
  const [isOpen, setIsOpen] = useState(false);
  const [query, setQuery] = useState('');
  const inputRef = useRef<HTMLInputElement>(null);
 
  const filtered = posts.filter(
    (post) =>
      post.title.toLowerCase().includes(query.toLowerCase()) ||
      post.description.toLowerCase().includes(query.toLowerCase()) ||
      post.tags.some((tag) => tag.toLowerCase().includes(query.toLowerCase()))
  );
 
  // اختصار لوحة المفاتيح: Cmd+K أو Ctrl+K
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        setIsOpen(true);
      }
      if (e.key === 'Escape') {
        setIsOpen(false);
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);
 
  useEffect(() => {
    if (isOpen && inputRef.current) {
      inputRef.current.focus();
    }
  }, [isOpen]);
 
  return (
    <>
      <button
        onClick={() => setIsOpen(true)}
        className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-zinc-800 text-zinc-400 text-sm hover:bg-zinc-700 transition-colors"
      >
        <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
        </svg>
        <span className="hidden sm:inline">بحث</span>
        <kbd className="hidden sm:inline text-xs bg-zinc-700 px-1.5 py-0.5 rounded">⌘K</kbd>
      </button>
 
      {isOpen && (
        <div className="fixed inset-0 z-[100] flex items-start justify-center pt-[20vh]">
          <div className="fixed inset-0 bg-black/60" onClick={() => setIsOpen(false)} />
          <div className="relative w-full max-w-lg mx-4 bg-zinc-900 rounded-xl border border-zinc-700 shadow-2xl overflow-hidden">
            <input
              ref={inputRef}
              type="text"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder="ابحث في المقالات..."
              className="w-full px-4 py-3 bg-transparent text-zinc-100 placeholder-zinc-500 outline-none border-b border-zinc-700"
            />
            <div className="max-h-80 overflow-y-auto p-2">
              {query.length > 0 && filtered.length === 0 && (
                <p className="text-zinc-500 text-sm p-3">لا نتائج.</p>
              )}
              {filtered.slice(0, 8).map((post) => (
                <a
                  key={post.id}
                  href={`/blog/${post.id}`}
                  className="block p-3 rounded-lg hover:bg-zinc-800 transition-colors"
                  onClick={() => setIsOpen(false)}
                >
                  <h3 className="font-medium text-zinc-100">{post.title}</h3>
                  <p className="text-sm text-zinc-400 mt-1 line-clamp-1">{post.description}</p>
                </a>
              ))}
            </div>
          </div>
        </div>
      )}
    </>
  );
}

استخدمه في القالب كـ جزيرة:

---
// في BaseLayout.astro
import SearchDialog from '../components/SearchDialog';
import { getCollection } from 'astro:content';
 
const posts = (await getCollection('blog'))
  .filter((p) => !p.data.draft)
  .map((p) => ({
    id: p.id,
    title: p.data.title,
    description: p.data.description,
    tags: p.data.tags,
  }));
---
 
<!-- استبدل العنصر النائب بـ: -->
<SearchDialog client:idle posts={posts} />

لاحظ توجيه client:idle — هذا يعني:

  • زر البحث يُعرَض كـ HTML فوراً (معروض من الخادم)
  • JavaScript يُحمَّل فقط عندما يكون المتصفح في وضع الخمول (بعد العرض الحرج)
  • المستخدمون يرون الزر فوراً لكن التفاعلية تنطلق بعد لحظات

إنشاء زر "العودة للأعلى"

مرشح مثالي آخر للجزيرة:

// src/components/BackToTop.tsx
import { useState, useEffect } from 'react';
 
export default function BackToTop() {
  const [visible, setVisible] = useState(false);
 
  useEffect(() => {
    const handler = () => setVisible(window.scrollY > 400);
    window.addEventListener('scroll', handler, { passive: true });
    return () => window.removeEventListener('scroll', handler);
  }, []);
 
  return (
    <button
      onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
      className={`fixed bottom-6 left-6 p-3 rounded-full bg-accent-600 text-white shadow-lg
        transition-all duration-300 hover:bg-accent-700
        ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4 pointer-events-none'}`}
      aria-label="العودة للأعلى"
    >
      <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
      </svg>
    </button>
  );
}

أضفه للقالب:

<!-- في BaseLayout.astro، قبل وسم </body> -->
<BackToTop client:visible />

هنا نستخدم client:visible — المكوّن يُحمَّل فقط عندما يظهر في نافذة العرض.


الخطوة 9: إضافة دعم MDX للمحتوى الغني

MDX يتيح لك استخدام المكوّنات داخل محتوى Markdown:

pnpm astro add mdx

أنشئ مكوّن تنبيه قابل لإعادة الاستخدام:

---
// src/components/Callout.astro
interface Props {
  type?: 'info' | 'warning' | 'tip' | 'danger';
  title?: string;
}
 
const { type = 'info', title } = Astro.props;
 
const styles = {
  info: 'border-blue-500 bg-blue-500/10 text-blue-200',
  warning: 'border-yellow-500 bg-yellow-500/10 text-yellow-200',
  tip: 'border-green-500 bg-green-500/10 text-green-200',
  danger: 'border-red-500 bg-red-500/10 text-red-200',
};
 
const icons = {
  info: 'ℹ️',
  warning: '⚠️',
  tip: '💡',
  danger: '🚨',
};
---
 
<div class={`border-r-4 rounded-l-lg p-4 my-6 ${styles[type]}`}>
  {title && (
    <p class="font-semibold mb-1">
      {icons[type]} {title}
    </p>
  )}
  <div class="text-sm">
    <slot />
  </div>
</div>

💡 نصيحة: لاحظ استخدام border-r-4 و rounded-l-lg بدلاً من border-l-4 — هذا للتوافق مع اتجاه الكتابة من اليمين لليسار.

الآن يمكنك استخدامه في تدوينات MDX:

---
title: "عزّز محتواك مع MDX"
description: "كيف تستخدم المكوّنات داخل Markdown لمحتوى أغنى"
publishedAt: 2026-02-24
tags: ["mdx", "astro", "محتوى"]
author: "كاتب تقني"
---
 
import Callout from '../../components/Callout.astro';
 
## MDX هو Markdown++
 
MDX يتيح لك تضمين المكوّنات مباشرة في كتابتك.
 
<Callout type="tip" title="نصيحة احترافية">
  يمكنك استيراد أي مكوّن Astro أو إطار عمل في ملفات MDX. المكوّنات الثابتة لا تضيف أي JavaScript للصفحة.
</Callout>

الخطوة 10: تحديث إعدادات Astro

لنُنهي الإعدادات:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
import react from '@astrojs/react';
import mdx from '@astrojs/mdx';
 
export default defineConfig({
  site: 'https://my-tech-blog.pages.dev',
  integrations: [tailwind(), react(), mdx()],
  markdown: {
    shikiConfig: {
      theme: 'github-dark-default',
      wrap: true,
    },
  },
  build: {
    inlineStylesheets: 'auto',
  },
  vite: {
    build: {
      cssMinify: 'lightningcss',
    },
  },
});

الخطوة 11: البناء وقياس الأداء

لنبني ونرى النتائج:

pnpm build

ستشاهد مخرجات مثل:

 generating static routes
▶ src/pages/index.astro
  └─ /index.html (+12ms)
▶ src/pages/blog/index.astro
  └─ /blog/index.html (+8ms)
▶ src/pages/blog/[id].astro
  ├─ /blog/hello-islands/index.html (+15ms)
  ├─ /blog/astro-vs-nextjs/index.html (+11ms)
  └─ /blog/using-mdx/index.html (+14ms)

✓ Completed in 1.2s

  Total pages: 5
  Total size: 42KB (HTML فقط!)

شغّل تدقيق Lighthouse — يجب أن ترى درجات قريبة من:

  • الأداء: 100
  • سهولة الوصول: 100
  • أفضل الممارسات: 100
  • SEO: 100

المقياس الأهم: افحص تبويب الشبكة. الصفحات الثابتة تُحمَّل بدون أي JavaScript. فقط الصفحات التي تحتوي جُزراً ستظهر حِزم JS، وتلك الحِزم تحتوي فقط كود الجزيرة، وليس وقت تشغيل الإطار الكامل.


الخطوة 12: النشر على Cloudflare Pages

أضف محوّل Cloudflare:

pnpm astro add cloudflare

حدّث إعداداتك إذا احتجت مسارات SSR (اختياري — مدوّنتنا ثابتة بالكامل):

// astro.config.mjs
import cloudflare from '@astrojs/cloudflare';
 
export default defineConfig({
  // ... الإعدادات الموجودة
  output: 'static', // 'hybrid' إذا احتجت بعض مسارات SSR
  adapter: cloudflare(),
});

انشر عبر CLI الخاص بـ Cloudflare:

pnpm add -D wrangler
npx wrangler pages deploy dist

أو اربط مستودع GitHub بـ Cloudflare Pages للنشر التلقائي مع كل push:

  1. اذهب إلى لوحة تحكم Cloudflare Pages
  2. انقر "Create a project" → "Connect to Git"
  3. اختر مستودعك
  4. اضبط أمر البناء: pnpm build
  5. اضبط مجلد المخرجات: dist

كل git push الآن يُطلق نشراً جديداً مع روابط معاينة للفروع.


مقارنة الأداء

لوضع نهج Astro في سياقه، إليك مقارنة واقعية لمدوّنة بها 50 مقالة:

المقياسNext.js (App Router)GatsbyAstro 5
JS المُرسَل (صفحة القائمة)~180KB~210KB0KB
JS المُرسَل (صفحة التدوينة)~165KB~195KB~12KB (جزيرة البحث فقط)
وقت البناء~45 ثانية~90 ثانية~8 ثوانٍ
أداء Lighthouse9288100
وقت التفاعل2.1 ثانية2.8 ثانية0.3 ثانية

الفرق هائل، خاصة على الأجهزة المحمولة ذات المعالجات والشبكات الأبطأ.


الخلاصة

في هذا الدرس، بنيت موقع محتوى متكاملاً مع Astro 5 يتميّز بـ:

صفر JavaScript افتراضياً — الصفحات HTML خالص ✅ جُزر للتفاعلية — نافذة البحث وزر العودة للأعلى يُرطَّبان بشكل مستقل ✅ مجموعات محتوى — Markdown/MDX آمنة الأنواع مع واجهة طبقة المحتوى ✅ درجة 100 في Lighthouse — الأداء ميزة وليس فكرة لاحقة ✅ نشر على الحافة — Cloudflare Pages للتوزيع العالمي

النقاط الرئيسية

  1. ليس كل موقع يحتاج إطار JavaScript. لمواقع المحتوى، نهج Astro المبني على HTML يقدّم أداءً متفوقاً.
  2. هندسة الجُزر تمنحك أفضل ما في العالَمين — عرض ثابت للمحتوى، ترطيب ديناميكي للتفاعلية.
  3. توجيهات الترطيب (client:idle, client:visible, إلخ) تمنحك تحكماً دقيقاً في وقت تحميل JavaScript.
  4. مجموعات المحتوى توفّر إدارة محتوى آمنة الأنواع تتوسع مع موقعك.
  5. مستقل عن الأُطر — استخدم React أو Vue أو Svelte أو Solid لجُزرك. امزج وطابق حسب الحاجة.

الخطوات التالية

💡 نصيحة أخيرة: توثيق Astro ممتاز. إذا واجهتك مشكلة، زُر docs.astro.build — وهو مبني بـ Astro أيضاً، ويحصل طبيعياً على 100 في Lighthouse.

بناءً سعيداً! ⚡


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على كيفية توليد المؤثرات الصوتية باستخدام واجهة برمجة تطبيقات ElevenLabs في JavaScript.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

البدء مع ALLaM-7B-Instruct-preview

تعلم كيفية استخدام نموذج ALLaM-7B-Instruct-preview مع Python، وكيفية التفاعل معه من JavaScript عبر واجهة برمجة مستضافة (مثل Hugging Face Spaces).

8 د قراءة·