این وبلاگ با MkDocs و قالب Material شروع شد؛ ترکیبی که نوشتن با Markdown و انتشار یک سایت ایستا را برایم ممکن کرد. با رشد نوشته‌ها و دوره‌ها، نیاز به کنترل مستقیم صفحهٔ اصلی، مسیرهای یادگیری، بایگانی و فرایند ساخت بیشتر شد. مهاجرت به Hugo فرصتی بود تا این اجزا را در ساختاری روشن‌تر کنار هم قرار بدهم.

در این نوشته توضیح می‌دهم دو ابزار چگونه HTML تولید می‌کنند، تفاوت زبان و مخزن‌هایشان چه اثری بر نگه‌داری دارد و در همین وبلاگ چه چیزهایی تبدیل شد. اگر یک سایت Markdown دارید و به تغییر مولد آن فکر می‌کنید، نتیجهٔ مورد انتظار این است که هزینه‌ها و معیار موفقیت مهاجرت را پیش از شروع بشناسید.

مولد سایت ایستا دقیقاً چه کاری می‌کند؟#

مولد سایت ایستا یا Static Site Generator، متن، تنظیمات و قالب‌ها را هنگام ساخت به فایل‌های آمادهٔ انتشار تبدیل می‌کند. مرورگر فایل HTML نهایی را دریافت می‌کند؛ برای هر بازدید، لازم نیست متن Markdown دوباره پردازش شود.

هم MkDocs و هم Hugo از این الگو استفاده می‌کنند. خروجی می‌تواند روی یک وب‌سرور معمولی یا میزبان فایل‌های ایستا قرار بگیرد. بنابراین حذف نیاز به اجرای Python روی سرورِ نمایش سایت، دستاورد تازهٔ این مهاجرت نبود؛ نسخهٔ MkDocs هم به چنین اجرایی نیاز نداشت. تفاوت اصلی در محیط ساخت و شیوهٔ سازمان‌دهی محتوا بود. راهنمای انتشار MkDocs هم همین استقلال خروجی از محیط تولید را توضیح می‌دهد.

از Python تا Go؛ HTML چگونه ساخته می‌شود؟#

MkDocs با Python نوشته شده است. در مسیر معمول ساخت، تنظیمات mkdocs.yml و فایل‌های محتوا خوانده می‌شوند، متن با Python-Markdown به HTML تبدیل می‌شود و قالب‌های Jinja2 آن را در صفحه قرار می‌دهند. قالب مشخص می‌کند عنوان، منو و بدنه کجا باشند. وابستگی به این کتابخانه‌ها در تعریف بستهٔ MkDocs قابل مشاهده است.

افزونه‌ها می‌توانند در مراحل مختلف ساخت دخالت کنند؛ برای مثال، متن یا اطلاعات صفحه را پیش از تولید خروجی تغییر دهند. این نقطه‌های ورود برای شخصی‌سازی با Python مفیدند، ولی رفتار نهایی سایت به ترکیب افزونه‌ها هم وابسته می‌شود. مستندات افزونه‌های MkDocs این مراحل را شرح می‌دهد؛ راهنمای قالب‌ها نیز نقش Jinja2 را روشن می‌کند.

Hugo با Go نوشته شده و معمولاً به‌شکل یک فایل اجرایی آماده نصب می‌شود. با داشتن آن فایل، برای ساخت معمول سایت لازم نیست کامپایلر Go نصب کنیم. Markdown در تنظیمات پیش‌فرض با Goldmark پردازش می‌شود و قالب‌های Go، محتوا و اطلاعات صفحات را به خروجی تبدیل می‌کنند. این دو بخش در تنظیمات پردازش Markdown و راهنمای قالب‌های Hugo توضیح داده شده‌اند.

تصویر ساده‌شدهٔ مسیر ساخت چنین است؛ افزونه‌ها و امکانات خاص می‌توانند مرحله‌های دیگری به آن اضافه کنند:

MkDocs:
Markdown + mkdocs.yml
  -> Python-Markdown + plugin hooks
  -> Jinja2 templates
  -> HTML + CSS + JavaScript

Hugo:
Markdown + front matter + configuration
  -> content model + Goldmark
  -> Go templates + output formats
  -> HTML + CSS + JavaScript + feeds/data

در Hugo، مدل محتوا شامل اطلاعاتی مانند نوع صفحه، بخش، تاریخ و دسته‌هاست. قالب می‌تواند از همین اطلاعات فهرست نوشته‌ها یا صفحهٔ یک دسته را بسازد. خواندن و نوشتن Go Template برایم بخشی از هزینهٔ مهاجرت بود؛ داشتن یک فایل اجرایی به معنی حذف نیاز به یادگیری قالب‌نویسی نیست.

مقایسهٔ دو مخزن GitHub#

برای بررسی خود ابزارها باید سراغ مخزن MkDocs و مخزن Hugo رفت. نام محصول دوم Hugo است؛ gohugo.io نشانی وب‌سایت و gohugoio/hugo نشانی مخزن آن است.

موضوعMkDocsHugo
زبان اصلیPythonGo
تمرکز معرفی پروژهمستندات پروژه با Markdownمولد عمومی سایت، از وبلاگ تا مستندات
ساختار قابل بررسی مخزنبستهٔ mkdocs/ و تعریف بسته در pyproject.tomlبخش‌هایی مانند hugolib/، markup/ و tpl/؛ وابستگی‌ها در go.mod
روش معمول دریافت ابزارنصب بستهٔ Python و وابستگی‌هایشدریافت فایل اجرایی مناسب سیستم‌عامل
مجوز هستهBSD-2-ClauseApache-2.0
مسیر شخصی‌سازی سایتقالب Jinja2، افزونه و hook پایتونیقالب Go، فایل داده، shortcode و تنظیم خروجی

این مقایسه از معرفی و ساختار همین مخزن‌ها و مستندات توسعهٔ آن‌ها به دست می‌آید. مجوز قالب یا افزونهٔ جانبی را باید جدا بررسی کرد. برای نمونه، Material پروژه‌ای جدا از هستهٔ MkDocs است و افزونهٔ بلاگ آن را نباید قابلیت مستقل هستهٔ MkDocs حساب کرد.

برای انتخاب ابزار، ساختار وابستگی‌ها و امکان نگه‌داری تغییرات خودم از تعداد ستاره‌های GitHub مهم‌تر بود. تعداد ستاره، issue باز یا commit به‌تنهایی نشان نمی‌دهد یک ابزار برای این وبلاگ مناسب‌تر است. در اکوسیستم MkDocs می‌توان با Python وارد روند ساخت شد؛ در Hugo بخش بزرگی از نیازهای این سایت در قالب و داده تعریف شده است. این تفاوت، محل انجام کار سفارشی را تغییر می‌دهد.

نقطهٔ شروع این وبلاگ چه بود؟#

پیکربندی قبل از مهاجرت، قالب Material و افزونه‌های search، blog، tags، rss و minify را فعال می‌کرد. افزونه‌های Markdown نیز برای هشدارها، جزئیات بازشونده، برجسته‌سازی کد و تب‌ها تنظیم شده بودند. سه hook محلی با نام‌های jalali_dates.py، reading_ui.py و topic_pages.py هم در ساخت مشارکت داشتند.

این ترکیب کار می‌کرد و راهنمای ساخت بلاگ با MkDocs همچنان بخشی از مطالب سایت است. اما برای ادامهٔ توسعه، ترجیح دادم تولید صفحه، تعریف مسیرهای یادگیری و کنترل خروجی را با ساختار Hugo هماهنگ کنم.

تغییر اصلی در commit با شناسهٔ 97538ac و عنوان BREAKING CHANGE: migrate from mkdocs to hugo ثبت شده است. شرح زیر از تفاوت فایل‌های آن تغییر و ساختار فعلی مخزن به دست آمده؛ مراحل، بازسازی منطقی فرایند هستند و ادعای ثبت لحظه‌به‌لحظهٔ اجرای مهاجرت ندارند.

تبدیل محتوا و قالب، مرحله به مرحله#

۱. حفظ محتوا و آدرس‌ها پیش از تغییر ظاهر#

فایل‌های Markdown در docs/ باقی ماندند. در hugo.toml، گزینهٔ contentDir = 'docs' همین پوشه را منبع محتوا و publishDir = 'site' مقصد خروجی معرفی می‌کند. برای مهاجرت لازم نبود همهٔ محتوا را به پوشه‌ای تازه ببریم.

آدرس عمومی نوشته‌ها هم با الگوی زیر نگه داشته شد:

/blog/YYYY/MM/DD/slug/

slug همان بخش نام‌دار و ثابت آدرس است. حفظ آن همراه با تاریخ انتشار، مانع تغییر ناخواستهٔ لینک مطالب می‌شود. در پیکربندی پروژه، قاعدهٔ cascade این نوع و الگوی آدرس را به نوشته‌های زیر docs/blog/posts/ اعمال می‌کند؛ یعنی لازم نیست برای هر فایل، تنظیم یکسانی را دوباره بنویسیم.

۲. تبدیل فرانت‌متر و صفحه‌های بخش#

فرانت‌متر، اطلاعات ابتدای فایل مانند عنوان و تاریخ است. در محتوای قبلی، تاریخ انتشار زیر date.created قرار داشت. در نسخهٔ فعلی، date مستقیماً تاریخ را نگه می‌دارد و عنوان به همین بخش منتقل شده است. نمونهٔ کوتاه‌شده از تبدیل مقالهٔ MkDocs چنین است:

# Before: metadata used by the previous blog setup
date:
  created: 2026-07-25
slug: building-a-blog-with-mkdocs
# After: metadata used by the current Hugo setup
title: ساخت وبلاگ و قالب اختصاصی با MkDocs
date: 2026-07-25
slug: building-a-blog-with-mkdocs

عنوان سطح اول از بدنه حذف شد تا دوبار نمایش داده نشود. جداکنندهٔ خلاصه نیز به <!--more--> تبدیل شد. ایندکس بخش‌هایی مانند خانه و بلاگ از index.md به _index.md تغییر کرد؛ این تصمیم مربوط به صفحه‌های بخش بود، نه دستور تغییر نام همهٔ فایل‌های index.md. معنای فیلدهای محتوا را می‌توان در راهنمای فرانت‌متر Hugo بررسی کرد.

۳. بازسازی قابلیت‌ها در جای مناسب#

قالب Material با Terminal و قالب‌های اختصاصی پروژه جایگزین شد. فونت‌های محلی، راست‌به‌چپ بودن صفحه، تاریخ شمسی و ظاهر تیره و سبز در لایهٔ نمایش پیاده‌سازی شدند. تاریخ منبع میلادی باقی ماند و قالب، تاریخ قابل مشاهده را به شمسی تبدیل می‌کند؛ فیلد ماشین‌خوان تاریخ همچنان میلادی است.

در ساختار فعلی، مسئولیت‌ها این‌طور تقسیم شده‌اند:

نیاز سایتمحل پیاده‌سازی فعلی
تعریف و ترتیب دوره‌هاdata/learning.yaml و قالب مسیرهای یادگیری
آرشیو سال و موضوعفایل‌های _content.gotmpl و قالب‌های فهرست
خوراک RSS برای دنبال‌کردن مطالبقالب‌های XML و layouts/partials/feed.html
دادهٔ جست‌وجوlayouts/index.search.json
اجرای جست‌وجو در مرورگرstatic/javascripts/search.js
ظاهر اختصاصیstatic/style.css و قالب‌های layouts/

داشتن قالب و فایل داده، هزینهٔ نگه‌داری را صفر نمی‌کند؛ فقط منطق سفارشی را در محل مشخصی قرار می‌دهد. جست‌وجوی این سایت هم پیاده‌سازی خود پروژه است، نه قابلیتی که با عوض‌کردن نام ابزار خودکار از Material منتقل شده باشد.

۴. بررسی لینک‌ها و تفاوت‌های Markdown#

یکسان‌بودن پسوند .md تضمین نمی‌کند دو پردازشگر، همهٔ نشانه‌گذاری‌ها را یکسان تفسیر کنند. تب‌ها، هشدارها، HTML داخل متن و شناسهٔ تیترها نیاز به بررسی دارند. قابلیت وابسته به افزونهٔ قبلی باید به نشانه‌گذاری سازگار تبدیل شود یا برایش پشتیبانی مشخصی در خروجی جدید نوشته شود.

برای لینک‌های نسبی به فایل Markdown، پروژه یک render hook دارد؛ یعنی قالبی که هنگام تولید لینک اجرا می‌شود. این قالب صفحهٔ مقصد را پیدا می‌کند و آدرس انتشارش را می‌نویسد. اگر مقصد پیدا نشود، ساخت خطا می‌دهد. به این ترتیب، جابه‌جایی ساختار تولید نباید بی‌صدا به لینک شکسته منجر شود.

جداسازی درس‌ها از نوشته‌های مستقل نیز بعداً به ساختار فعلی اضافه شد. عضویت در مسیر یادگیری از دادهٔ دوره تشخیص داده می‌شود؛ فهرست بلاگ آن مطالب را کنار می‌گذارد، ولی فایل و آدرس درس دست‌نخورده می‌ماند. این نمونه‌ای از کنترلی است که اکنون روی نمایش محتوا داریم.

ساخت و بررسی نتیجه در این پروژه#

برای اجرای نسخهٔ فعلی، روی Linux به Hugo Extended نسخهٔ 0.167.0 یا جدیدتر، Git، Bash، Python 3 و rsync نیاز داریم. ابزارها باید از قبل نصب باشند؛ قالب هم داخل مخزن نگه‌داری می‌شود. فرمان زیر از ریشهٔ پروژه، نسخهٔ Hugo را نشان می‌دهد تا وجود عبارت +extended و نسخهٔ مناسب بررسی شود:

hugo version

سپس ساخت و بررسی خروجی با این فرمان انجام می‌شود:

bash scripts/build.sh

اسکریپت ابتدا در یک پوشهٔ موقت خالی خروجی می‌سازد و اخطار Hugo را موجب توقف می‌داند. بعد scripts/check_site.py آدرس مطالب، لینک‌های داخلی، تاریخ‌ها، RSS و دادهٔ جست‌وجو را بررسی می‌کند. تنها پس از موفقیت این مرحله، خروجی به site/ منتقل می‌شود. این پوشه خروجی تولیدشده است؛ فایل‌های اضافی آن هنگام همگام‌سازی حذف می‌شوند و نباید فایل دستی یا محتوای اصلی را در آن گذاشت.

Python در این روند هنوز حضور دارد، اما وظیفه‌اش بررسی خروجی است. ساخت production بستهٔ تازه‌ای از اینترنت دریافت نمی‌کند. گردش‌کار Forgejo هم همین اسکریپت را روی رانر میزبان اجرا می‌کند و پس از موفقیت، فقط خروجی site/ را منتشر می‌کند.

نتیجهٔ مورد انتظار، پایان موفق ساخت و گزارش بررسی صفحات است. این بررسی‌ها سلامت همهٔ جنبه‌های سایت را تضمین نمی‌کنند: ظاهر موبایل، خوانایی راست‌به‌چپ، جست‌وجوی فارسی و نمایش مثال‌های خاص باید در مرورگر هم بررسی شوند. برای مهاجرتی مشابه، خروجی نسخهٔ قبلی را تا تأیید نسخهٔ جدید نگه دارید تا امکان بازگشت داشته باشید.

چرا این مهاجرت برای من موفق و استثنایی بود؟#

ارزش این تغییر در کنار هم قرارگرفتن چند نتیجه بود: محتوای Markdown و آدرس‌ها حفظ شدند، ساخت با فایل اجرایی Hugo انجام می‌شود، قالب‌ها و داده‌ها جای مشخصی دارند و بررسی خروجی بخشی از مسیر انتشار است. می‌توانم شکل نمایش وبلاگ را تغییر بدهم، بدون آنکه برای جداکردن دوره‌ها آدرس صدها درس را عوض کنم.

Hugo برای این نیازها ابزار مناسبی بود؛ پشتیبانی آن از دسته‌بندی، قالب‌ها و خروجی‌های مختلف با ساختار این سایت جور درآمد. فهرست قابلیت‌های رسمی Hugo این امکانات عمومی را معرفی می‌کند. نتیجهٔ خاص این وبلاگ، حاصل تنظیم و پیاده‌سازی آن‌ها در پروژه است.

برای سرعت، اندازه‌گیری هم‌شرایطی از نسخهٔ قبلی و فعلی ندارم؛ بنابراین نسبت شتاب یا بهبود زمان بارگذاری مرورگر اعلام نمی‌کنم. زبان Go و اجرای کامپایل‌شده به‌تنهایی چنین عددی را ثابت نمی‌کنند. قالب، افزونه‌ها، تعداد صفحات و پردازش دارایی‌ها هم در زمان ساخت اثر دارند؛ سرعت نمایش نیز به HTML، تصویرها، CSS، JavaScript و میزبانی وابسته است.

برای پروژه‌ای که مستندات فنی و افزونه‌های Python در مرکز آن هستند، MkDocs همچنان گزینهٔ قابل بررسی است. انتخاب من برای این وبلاگ Hugo بود، چون توانستم نیازهای واقعی آن را با ساختاری روشن و قابل بررسی پیاده کنم. موفقیت مهاجرت را در همین کنترل بر محتوا، حفظ لینک‌ها و اطمینان بیشتر به فرایند انتشار می‌بینم.