Astro 5 در ۳ دسامبر ۲۰۲۴ منتشر شد و Content Layer و Server Islands را به جریان اصلی ساخت سایتهای محتوایی آورد. این تاریخ مهم است، چون معرفی آن را نباید خبر انتشار تازه در سال ۲۰۲۶ خواند. اعلام رسمی Astro 5 نقطه شروع این مطلب است. برای یک مجله، سایت مستندات یا پورتفولیو، پرسش عملی این است که محتوا چگونه وارد پروژه میشود، چه زمانی تازه میشود و کدام قسمت صفحه به اطلاعات لحظهای نیاز دارد.
نسخه قبلی مقاله اتصال به هر CMS با یک خط کد و بیاثر بودن بخشهای پویا بر عملکرد را وعده میداد. Loader هر منبع به تنظیم، تبدیل داده و مدیریت شکست خودش نیاز دارد. درخواست بخش پویا هم هزینه شبکه و سرور دارد. امکانات Astro کمک میکنند این هزینه را در جای مشخصی نگه دارید؛ آن را حذف نمیکنند. انتخاب مناسب زمانی روشن میشود که چرخه انتشار و نیاز واقعی خواننده را به معماری وصل کنید.
سایت محتوایی چه قراردادهایی دارد؟
مقاله یک عنوان، نشانی پایدار، تاریخ انتشار و بدنه دارد. ممکن است نویسنده، دسته، توضیح سئو و وضعیت پیشنویس هم داشته باشد. وقتی این اطلاعات در فایلها یا CMS پراکندهاند، قالب صفحه مجبور میشود نبودن هر فیلد را حدس بزند. تعریف قرارداد داده در ابتدای کار، خطا را زودتر آشکار میکند. نویسنده باید بداند کدام فیلد اجباری است و توسعهدهنده باید بداند چه مقداری به قالب میرسد.
نشانی مقاله را از عنوان جدا نگه دارید. اصلاح عنوان فارسی نباید خودکار لینک قدیمی را بشکند. اگر مهاجرت مسیر لازم است، نگاشت مسیر قبلی به جدید را ثبت کنید و مسئولیت آن را به انتشار محتوا بسپارید. عنوان برای خواننده قابل تغییر است؛ شناسه و مسیر در لینکها، اشتراکگذاری و ارجاعهای قبلی نقش دارند. این تفاوت در یک وبلاگ کوچک هم ارزش توجه دارد.
برای مجلهای که چند نفر در آن مینویسند، پیشنویس باید خارج از فهرست عمومی بماند. تنها مخفی کردن لینک کافی نیست اگر صفحه پیشنویس همچنان تولید شود. فهرست و مسیرهای جزئیات باید از یک قانون انتشار پیروی کنند. در نمونه آموزشی پایین، فیلد draft همین تصمیم را روشن میکند. در پروژه واقعی ممکن است نقش کاربر و زمان انتشار هم به قانون اضافه شوند.
Content Layer محتوا را قبل از قالب منظم میکند
Content Layer در Astro 5 از Loader برای آوردن داده به collection استفاده میکند. Loaderهای داخلی مانند glob فایلها را میخوانند و برای منابع دیگر میتوان Loader مناسب ساخت یا انتخاب کرد. تعریف schema فیلدها را اعتبارسنجی میکند. راهنمای Content collections محل مرجع تعریف collection، خواندن داده و render کردن بدنه است. داشتن این API به معنی یکسان بودن رفتار همه منابع داده نیست.
اگر محتوا در Markdown محلی است، تغییر فایل همراه کد وارد فرایند build میشود. اگر داده از CMS میآید، باید روشن کنید چه رویدادی build را آغاز میکند و هنگام unavailable بودن CMS چه اتفاقی میافتد. داده نامعتبر بهتر است در یک مرحله مشخص گزارش شود، نه اینکه در صفحه عمومی با عنوان خالی دیده شود. قرارداد خطا را برای تیم محتوا به زبان قابل فهم بنویسید؛ نام یک exception به تنهایی راه اصلاح را نشان نمیدهد.
Loader سفارشی فقط درخواست HTTP نیست. باید تصمیم بگیرد شناسه هر entry چیست، چه مقدار از داده ذخیره شود و چگونه تغییرات منبع تشخیص داده شوند. اگر CMS یک مقاله را حذف کند، خروجی سایت باید مطابق سیاست شما تغییر کند. حذف اشتباه، قطع اتصال و لغو انتشار یک معنا ندارند. طراحی این تفاوتها از وعده «اتصال فوری به هر منبع» دقیقتر است و از گم شدن محتوای سالم جلوگیری میکند.
یک مجموعه مقاله محلی بسازید
برای نمونه مستقل، با npm create astro@latest یک پروژه بسازید و template خالی را انتخاب کنید. این کد برای API خانواده Astro 5 است؛ نسخه نصبشده را در package.json کنترل کنید و اگر نسخه major دیگری نصب شده، راهنمای آن نسخه را بخوانید. برای استفاده مشخص از خانواده 5، وابستگی astro را روی آخرین patch موجود در major 5 نگه دارید و Lockfile را ثبت کنید. نمونه به integration اضافی یا سرویس بیرونی نیاز ندارد.
در src/content.config.ts مجموعهای به نام articles تعریف میکنیم. Loader فایلهای Markdown در src/data/articles را میخواند. title اجباری است، description حداقل طول دارد و publishedAt به Date تبدیل میشود. draft در صورت نبودن false خواهد بود. این schema قرارداد همین نمونه است، نه نسخهای که باید بدون بررسی برای همه مجلهها استفاده شود. محدودیتها را با نیاز تحریریه هماهنگ کنید.
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const articles = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/data/articles" }),
schema: z.object({
title: z.string().min(1),
description: z.string().min(10),
publishedAt: z.coerce.date(),
draft: z.boolean().default(false),
}),
});
export const collections = { articles };
فایل src/data/articles/first-post.md را با محتوای زیر بسازید. بخش بین خطهای جداکننده، frontmatter است و بقیه فایل بدنه Markdown. مقدارهای این مثال داده آموزشیاند. تاریخ را بهصورت رشته خوانا نگه داشتهایم تا تبدیل آن در schema انجام شود. با اضافه کردن فایل دوم به همین پوشه، collection یک entry دیگر خواهد داشت؛ لازم نیست آن را دستی در آرایه فهرست وارد کنید.
---
title: "اولین یادداشت آموزشی"
description: "یک مقاله محلی برای آشنایی با collection و صفحه جزئیات."
publishedAt: "2026-10-10"
draft: false
---
این بدنه یک مقاله آموزشی است و از فایل Markdown خوانده میشود.
## قرارداد محتوا
عنوان، توضیح و تاریخ انتشار در frontmatter ثبت شدهاند.
فایل src/pages/index.astro فهرست عمومی را میسازد. getCollection فقط مقالههایی را برمیگرداند که draft نیستند. مرتبسازی بر اساس تاریخ انجام میشود و ID هر entry در لینک استفاده میشود. متن عنوان از داده collection میآید و style همین صفحه در فایل خودش قرار دارد. HTML فارسی دارای lang و dir است و نمونه فونت، تصویر یا فایل معرفینشدهای بارگذاری نمیکند.
---
import { getCollection } from "astro:content";
const articles = (await getCollection("articles", ({ data }) => !data.draft))
.sort((a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime());
---
<!doctype html>
<html lang="fa" dir="rtl">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width" /><title>یادداشتها</title></head>
<body>
<main>
<h1>یادداشتها</h1>
<ul>
{articles.map((article) => (
<li><a href={`/articles/${article.id}/`}>{article.data.title}</a><p>{article.data.description}</p></li>
))}
</ul>
</main>
</body>
</html>
<style>
main { max-width: 48rem; margin: 2rem auto; padding: 1rem; font-family: sans-serif; line-height: 1.8; }
li { margin-block: 1rem; }
</style>
برای صفحه هر مقاله، فایل src/pages/articles/[...id].astro را اضافه کنید. مسیر rest اجازه میدهد IDهای پوشهدار هم با همان ساختار مدیریت شوند. getStaticPaths فهرست مسیرها را از همان مقالههای منتشرشدنی میگیرد و entry را به props میفرستد. render بدنه Markdown را به component قابل نمایش تبدیل میکند. عنوان صفحه جزئیات تنها در صفحه تولیدشده h1 است و در خود مقاله Markdown تکرار نشده است.
---
import { getCollection, render } from "astro:content";
import type { CollectionEntry } from "astro:content";
export async function getStaticPaths() {
const articles = await getCollection("articles", ({ data }) => !data.draft);
return articles.map((article) => ({ params: { id: article.id }, props: { article } }));
}
interface Props { article: CollectionEntry<"articles">; }
const { article } = Astro.props;
const { Content } = await render(article);
---
<!doctype html>
<html lang="fa" dir="rtl">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width" /><title>{article.data.title}</title><meta name="description" content={article.data.description} /></head>
<body>
<main>
<a href="/">بازگشت به فهرست</a>
<article><h1>{article.data.title}</h1><Content /></article>
</main>
</body>
</html>
<style>
main { max-width: 48rem; margin: 2rem auto; padding: 1rem; font-family: sans-serif; line-height: 1.8; }
</style>
npm run dev محیط توسعه را شروع میکند. برای ساخت خروجی static از npm run build استفاده کنید و برای دیدن همان خروجی محلی npm run preview را اجرا کنید. این دستورهای معرفی روش اجرا هستند و نتیجه آزمایش انجامشده در این مقاله محسوب نمیشوند. خروجی static این نمونه به سرور SSR نیاز ندارد؛ افزودن قابلیت وابسته به درخواست، نیاز استقرار را تغییر میدهد و باید جداگانه بررسی شود.
Client Island و Server Island را جدا بشناسید
Client Island به بخشی مربوط است که در مرورگر رفتار تعاملی دارد، مثل انتخاب گزینه یا کنترل فرم. Server Island برای جدا کردن رندر بخشی از صفحه در سمت سرور به کار میآید. این دو پاسخ به دو نیاز متفاوتاند. یک منوی ساده الزاماً به React و hydration نیاز ندارد؛ یک پیام وابسته به نشست هم نباید برای پنهان کردن راز، داده خام حساب را به مرورگر بفرستد.
در مستندات Server islands، server:defer رندر component را از رندر اولیه صفحه جدا میکند. برای این قابلیت به adapter و محیطی با قابلیت اجرای سرور نیاز دارید. fallback هنگام آماده نبودن محتوا نمایش داده میشود. اگر سایت شما اکنون فقط فایل static تحویل میدهد، افزودن این directive بدون فراهم کردن محیط اجرا کافی نیست.
برای تصمیمگیری، بخش شخصی را تا حد ممکن کوچک نگه دارید. در یک صفحه راهنما، معرفی محصول و متن آموزش میتواند عمومی بماند و وضعیت ورود کاربر جدا بیاید. شکست وضعیت ورود نباید خواندن آموزش را متوقف کند. در مقابل، اگر محتوای اصلی صفحه محرمانه است، پوسته عمومی را طوری ننویسید که اطلاعات حساس را پیشاپیش آشکار کند. مرز component باید با مرز مجوز هماهنگ باشد.
تازه شدن محتوا و مسئولیت انتشار
ساخت static به شما خروجی قابل توزیع میدهد، اما محتوا تا build بعدی تغییر نمیکند. تحریریه باید بداند دکمه انتشار CMS چه زمانی در سایت دیده میشود. Webhook، صف build و گزارش شکست را در چرخه محتوا لحاظ کنید. اگر دو ویرایش پشت سر هم انجام میشود، باید روشن باشد کدام build نماینده آخرین وضعیت است. انتشار خروجی قدیمی پس از خروجی جدید، از نظر خواننده عقبگرد محتواست.
برای انتقال پروژه قدیمی به Content Layer، ابتدا مسیرها، schema و روش خواندن entryها را ثبت کنید. راهنمای ارتقا به Astro 5 تفاوت APIهای قبلی و مدل جدید را توضیح میدهد. مهاجرت محتوای همه مقالهها و تغییر طراحی صفحه را در یک تغییر مبهم جمع نکنید. حفظ مسیرهای قبلی و معنای draft باید از ابتدا بخشی از برنامه باشد، نه کاری که پس از مشاهده لینک شکسته انجام شود.
انتخاب Astro به معنی بینیازی از طراحی خواندن نیست. ترتیب خبر، طول خلاصه و تفاوت فهرست با صفحه جزئیات روی تجربه مجله اثر دارند. مقاله سلسلهمراتب خبر در سایتهای خبری به این بخش مربوط است. گام بعدی برای یک پروژه محتوایی، نوشتن schema چند entry واقعی و مسیر انتشار آنهاست؛ بعد از روشن شدن قرارداد داده، انتخاب بخشهای static و پویا آسانتر میشود.
هنوز دیدگاهی ثبت نشده
اولین نفری باشید که نظر میدهد!