كنت أشغّل هذا الموقع سابقاً على نطاق مخصص مربوط بالخدمة القديمة من Google Sites (زمن Google Apps). أوقفت Google تلك الخدمة في 2021، فظل عنوان https://www.ixam.net معطلاً فترة طويلة. في النهاية قررت أن أعيد استغلال النطاق. أردت حلاً بلا خوادم لا يتقيد بمنصة قد تختفي فجأة، وهكذا بدأت رحلة إعادة البناء برفقة ChatGPT.

كنت أظن أن ChatGPT سيجعل المهمة سهلة، لكن الواقع مختلف. نسيت متطلبات أساسية، وتعقّدنا في دائرة مغلقة أثناء الاستكشاف، واضطررت إلى مطالعة مستودعات مثل https://github.com/patrickgrey/website/ لأجد الطريق. كان جهداً شاقاً لكنه اكتمل كعمل مشترك بيني وبين الذكاء الاصطناعي.

الإحساس كان أشبه بتوجيه زميل موهوب لكنه يسيء الفهم كثيراً والعودة به إلى المسار الصحيح ─ وهو دليل على أن الذكاء الاصطناعي وصل إلى مرحلة نافعة حقاً. لكن كلما تعقدت الأمور، ازداد الإحباط من الأخطاء المتكررة.

الخلاصة بالنسبة لي: بدلاً من إجبار الذكاء الاصطناعي على تذكر كل الخلفية في محادثة واحدة، من الأفضل التوقف مؤقتاً، تلخيص الوضع بنفسك، ثم فتح جلسة جديدة بتعليمات واضحة.

ومع كل ذلك، لا أعتقد أنني كنت لأحقق هذا القدر من البحث والتجارب والتعلم بمفردي. إنتاجية الذكاء الاصطناعي التوليدي مذهلة.


الأهداف

  • إتاحة المدونة على https://www.example.com/
  • تسجيل الدخول إلى DecapCMS عبر https://www.example.com/admin/ وإنشاء المقالات
  • حفظ المقالات الجديدة في GitHub ثم نشرها تلقائياً إلى Cloudflare
  • إبقاء التكاليف التشغيلية الإضافية صفراً (رسوم تسجيل النطاق خارج هذا النطاق)

0. المتطلبات المسبقة وهيكل المجلدات

0-1. المتطلبات

  • يفترض أن النطاق (مثل example.com) مسجل مسبقاً

  • يصلح كل من Windows وmacOS (أدرجت أوامر النظامين)

  • الخدمات المطلوبة (النسخ المجانية تكفي)

0-2. مثال على بنية المستودع

hugo.toml
content/
  blog/
  posts/
data/
functions/
  api/
    auth.js
    callback.js
layouts/
  _default/
    baseof.html
    index.html
    list.html
    single.html
static/
  _headers
  admin/
    config.yml
    index.html
  css/
    main.css

وتؤدي هذه البنية الأدوار التالية:

  • hugo.toml … إعدادات الموقع بالكامل (يجب استبدال baseURL بعنوان الإنتاج)
  • functions/api/*.js … دوال Cloudflare Pages (النقاط /api/auth و/api/callback لمصادقة GitHub)
  • layouts/_default/*.html … قوالب Hugo
  • static/admin/ … واجهة وإعدادات DecapCMS
  • static/css/main.css … المظهر (CSS)
  • static/_headers … إعدادات رؤوس HTTP في Cloudflare Pages (اختيارية)

ستجد في الدليل علامات “راجع الاستبدال” تشير إلى الأماكن التي يلزم تكييفها مع بيئتك.


1. التحضير

1-1. إنشاء حساب GitHub

  • أنشئ حساب GitHub من المتصفح.

1-2. إنشاء حساب Cloudflare

  • أنشئ حساب Cloudflare من المتصفح.

1-3. تثبيت Git وHugo

  • Windows (PowerShell)

    PS C:\Users\alice> winget install Git.Git
    PS C:\Users\alice> winget install Hugo.Hugo.Extended
    
  • macOS (Terminal)

    mac:~ dev$ brew install git
    mac:~ dev$ brew install hugo
    

2. التنفيذ الفعلي (حتى أول نشر)

2-1. تجهيز المستودع محلياً

  1. أنشئ مستودعاً فارغاً في GitHub

    • المسار: GitHub > New repository
    • مثال الاسم: my-hugo-blog
  2. انسخه إلى جهازك المحلي

    • Windows

      PS C:\work> git clone https://github.com/<YOUR_GH_USERNAME>/my-hugo-blog.git
      PS C:\work> cd .\my-hugo-blog
      
    • macOS

      mac:~/work dev$ git clone https://github.com/<YOUR_GH_USERNAME>/my-hugo-blog.git
      mac:~/work dev$ cd my-hugo-blog
      
  3. ضع مجموعة الملفات والمجلدات المذكورة أعلاه مباشرة تحت المستودع

قائمة “راجع الاستبدال” (تغييرات إلزامية)

  • hugo.toml

    baseURL = "https://www.example.com"   # ← استبدل بعنوان الإنتاج (مثال: https://www.example.com)
    languageCode = "ja-jp"
    title = "Example Blog"
    publishDir = "public"
    
    [permalinks]
      blog = "/blog/:year/:month/:slug/"
    
  • static/admin/config.yml

    backend:
      name: github
      repo: <YOUR_GH_USERNAME>/my-hugo-blog   # ← استبدل بمستودعك على GitHub
      branch: master                          # ← اسم الفرع الافتراضي (main أو master)
      base_url: https://www.example.com       # ← استبدل بعنوان الإنتاج
      auth_endpoint: /api/auth                # ← نقطة الدالة (ثابتة)
    media_folder: static/uploads
    public_folder: /uploads
    

ملاحظة: ملفا functions/api/auth.js وfunctions/api/callback.js يعملان كما هما لأنهما يستخدمان url.origin، فلا حاجة لتعديل خاص بالبيئة.

  1. نفّذ أول عملية commit وpush

    • Windows

      PS C:\work\my-hugo-blog> git add -A
      PS C:\work\my-hugo-blog> git commit -m "Initial commit: Hugo + DecapCMS + CF Pages"
      PS C:\work\my-hugo-blog> git push -u origin master
      
    • macOS

      mac:~/work/my-hugo-blog dev$ git add -A
      mac:~/work/my-hugo-blog dev$ git commit -m "Initial commit: Hugo + DecapCMS + CF Pages"
      mac:~/work/my-hugo-blog dev$ git push -u origin master
      

2-2. إنشاء تطبيق OAuth في GitHub (لتسجيل دخول DecapCMS)

  1. المسار: GitHub > Settings > Developer settings > OAuth Apps > New OAuth App

  2. أدخل البيانات:

    • Application name: DecapCMS for my-hugo-blog
    • Homepage URL: https://www.example.com
    • Authorization callback URL: https://www.example.com/api/callbackمهم جداً
  3. بعد الإنشاء ستحصل على:

    • Client ID
    • Client Secret (أنشئ واحداً جديداً واحتفظ به)

هذه القيم ستُسجل كمتغيرات بيئة في Cloudflare Pages.


2-3. إنشاء مشروع Cloudflare Pages

  1. المسار: Cloudflare Dashboard > Pages > Create a project > Connect to Git

  2. اربط GitHub واختر المستودع my-hugo-blog

  3. إعدادات البناء:

    • Framework preset: None (أو اترك Cloudflare يتعرف عليه تلقائياً)

    • Build command: hugo

    • Build output directory: public

    • متغيرات البيئة:

      • HUGO_VERSION = 0.128.0 (مثال؛ من الأفضل مطابقته مع إصدارك المحلي)
      • GITHUB_CLIENT_ID = (القيمة من الخطوة السابقة)
      • GITHUB_CLIENT_SECRET = (القيمة من الخطوة السابقة)
  4. اضغط Save and Deploy وانتظر عملية النشر الأولى

    • عند النجاح ستحصل على رابط معاينة *.pages.dev

2-4. ربط النطاق المخصص www.example.com

  1. المسار: Cloudflare > Pages > (المشروع) > Custom domains > Set up a custom domain

  2. أدخل www.example.com وأضفه

  3. إذا لم تكن قد نقلت النطاق إلى Cloudflare:

    • سجّل معلومات سجل DNS التي يعرضها Cloudflare (مثل CNAME www -> <project>.pages.dev)
    • توجه إلى لوحة تحكم الجهة المسجلة للنطاق وأضف السجل وفق التعليمات الخاصة بها
  4. بعد الانتشار تأكد من فتح https://www.example.com/

تلميح: إذا كان النطاق مداراً بالكامل في Cloudflare فيمكن إنشاء سجلات DNS المطلوبة بنقرة واحدة.


2-5. تسجيل الدخول إلى لوحة DecapCMS

  1. افتح https://www.example.com/admin/
  2. اضغط زر Login with GitHub ووافق على OAuth
  3. في المرة الأولى سيطلب GitHub تأكيد التفويض؛ اضغط Authorize

إذا فشل تسجيل الدخول، فتحقق من عنوان callback في OAuth ومن متغيرات البيئة في Cloudflare (GITHUB_CLIENT_ID/SECRET).


3. إجراءات التشغيل (إنشاء المقالات، مزامنة المحلي، تعديل القوالب)

3-1. إنشاء مقال عبر لوحة CMS

  1. الواجهة: https://www.example.com/admin/
  2. من القائمة اليسرى اختر Blog ثم New blog
  3. أدخل: Title وPublish Date وDescription وBody
  4. عند الضغط على Publish سيُنشَر المقال: commit في GitHub → نشر تلقائي في Cloudflare

ملف Markdown الناتج سيوضع في content/blog/.

3-2. تحديث المستودع المحلي بعد النشر من CMS

  • Windows

    PS C:\work\my-hugo-blog> git pull origin master
    
  • macOS

    mac:~/work/my-hugo-blog dev$ git pull origin master
    

بهذه الطريقة تحصل محلياً على أحدث المقالات، ما يسمح بتعديل القوالب أو CSS بأمان.

3-3. تعديل التصميم أو القوالب محلياً

  1. شغّل خادم التطوير المحلي

    • Windows

      PS C:\work\my-hugo-blog> hugo server -D
      
    • macOS

      mac:~/work/my-hugo-blog dev$ hugo server -D
      
    • افتح http://localhost:1313/ في المتصفح للتحقق

  2. أمثلة على نقاط التعديل:

    • layouts/_default/baseof.html … عناصر <head> والرأس والتذييل
    • layouts/_default/index.html … قائمة “أحدث المقالات” في الصفحة الرئيسية
    • layouts/_default/single.html … قالب صفحات المقالات
    • static/css/main.css … الألوان، الخطوط، الهوامش
  3. بعد التعديل نفّذ commit ثم push

    • Windows

      PS C:\work\my-hugo-blog> git add -A
      PS C:\work\my-hugo-blog> git commit -m "Update theme/layouts"
      PS C:\work\my-hugo-blog> git push
      
    • macOS

      mac:~/work/my-hugo-blog dev$ git add -A
      mac:~/work/my-hugo-blog dev$ git commit -m "Update theme/layouts"
      mac:~/work/my-hugo-blog dev$ git push
      
    • بعد ثوانٍ أو دقائق سينفذ Cloudflare نشرًا تلقائياً

3-4. لمحة عن إدارة الفروع

  • تأكد من تطابق اسم الفرع الافتراضي (main أو master) بين إعداد config.yml في DecapCMS وإعدادات Cloudflare
  • لإنشاء بيئة معاينة، افتح Pull Request في GitHub وسيولّد Cloudflare Pages بيئة Preview تلقائياً

4. إضافات مفيدة (اختيارية)

4-1. مثال لملف static/_headers (منع التخزين المؤقت للوحة الإدارة)

/admin/*
  Cache-Control: no-store

4-2. robots وملف sitemap (حسب الحاجة)

  • أنشئ static/robots.txt للتحكم بالزواحف
  • أضف إعداد outputs في hugo.toml لتوسيع توليد RSS أو sitemap

5. أعطال شائعة وكيفية التعامل معها

  • لوحة CMS تدور بلا توقف
    → تحقق من أن عنوان callback في OAuth هو https://www.example.com/api/callback وأن متغيرات البيئة GITHUB_CLIENT_ID/SECRET في Cloudflare صحيحة
  • الصفحة الرئيسية تعمل لكن صفحات المقالات تعطي 404
    → تأكد من تطابق baseURL في hugo.toml مع النطاق الفعلي، وراجع سجل البناء في Cloudflare للتأكد من إنشاء public/
  • صفحة /admin بيضاء بالكامل
    → تأكد من عدم حظر تحميل DecapCMS (قد تمنعه إضافات المتصفح). عطّل الإضافات وأعد التحميل

6. قائمة المراجعة للأجزاء التي تتطلب استبدالاً (إعادة تأكيد)

الملف/الإعداد المفتاح قيمة الاستبدال (مثال)
hugo.toml baseURL https://www.example.com
static/admin/config.yml repo <YOUR_GH_USERNAME>/my-hugo-blog
static/admin/config.yml branch main أو master وفق الواقع
static/admin/config.yml base_url https://www.example.com
Cloudflare Pages HUGO_VERSION مثال: 0.128.0
Cloudflare Pages GITHUB_CLIENT_ID Client ID لتطبيق OAuth في GitHub
Cloudflare Pages GITHUB_CLIENT_SECRET Client Secret لتطبيق OAuth في GitHub
تطبيق GitHub OAuth Callback URL https://www.example.com/api/callback
Cloudflare Pages Custom domain إضافة www.example.com

7. ملاحق: أمثلة ملفات يمكنك النسخ منها

7-1. functions/api/auth.js

// Cloudflare Pages Functions (/api/auth)
export async function onRequest(context) {
  const { request, env } = context;
  const client_id = env.GITHUB_CLIENT_ID;
  try {
    const url = new URL(request.url);
    const redirectUrl = new URL('https://github.com/login/oauth/authorize');
    redirectUrl.searchParams.set('client_id', client_id);
    redirectUrl.searchParams.set('redirect_uri', `${url.origin}/api/callback`);
    redirectUrl.searchParams.set('scope', 'repo user');
    redirectUrl.searchParams.set('state', crypto.getRandomValues(new Uint8Array(12)).join(''));
    return Response.redirect(redirectUrl.href, 302);
  } catch (err) {
    return new Response(String(err?.message || err), { status: 500 });
  }
}

7-2. functions/api/callback.js

function renderBody(status, content) {
  const html = `
  <script>
    const receiveMessage = (message) => {
      window.opener.postMessage(
        'authorization:github:${status}:${JSON.stringify(content)}',
        message.origin
      );
      window.removeEventListener("message", receiveMessage, false);
    }
    window.addEventListener("message", receiveMessage, false);
    window.opener.postMessage("authorizing:github", "*");
  </script>`;
  return new Blob([html]);
}

export async function onRequest(context) {
  const { request, env } = context;
  const client_id = env.GITHUB_CLIENT_ID;
  const client_secret = env.GITHUB_CLIENT_SECRET;
  try {
    const url = new URL(request.url);
    const code = url.searchParams.get('code');
    const response = await fetch('https://github.com/login/oauth/access_token', {
      method: 'POST',
      headers: { 'content-type': 'application/json', 'user-agent': 'cf-pages-oauth', 'accept': 'application/json' },
      body: JSON.stringify({ client_id, client_secret, code }),
    });
    const result = await response.json();
    if (result.error) {
      return new Response(renderBody('error', result), { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 401 });
    }
    const token = result.access_token;
    const provider = 'github';
    return new Response(renderBody('success', { token, provider }), { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 200 });
  } catch (error) {
    return new Response(error.message, { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 500 });
  }
}

7-3. static/admin/config.yml

backend:
  name: github
  repo: <YOUR_GH_USERNAME>/my-hugo-blog
  branch: main
  base_url: https://www.example.com
  auth_endpoint: /api/auth

media_folder: static/uploads
public_folder: /uploads

collections:
  - name: 'blog'
    label: 'Blog'
    folder: 'content/blog'
    create: true
    slug: '{{slug}}'
    editor:
      preview: false
    fields:
      - { label: 'Title', name: 'title', widget: 'string' }
      - { label: 'Publish Date', name: 'date', widget: 'datetime' }
      - { label: 'Description', name: 'description', widget: 'string' }
      - { label: 'Body', name: 'body', widget: 'markdown' }

9. نصائح لتحسين SEO والأداء

بعد بناء المدونة يمكنك تحسين ترتيب البحث وسرعة العرض عبر الخطوات التالية:

  1. إعداد خريطة الموقع وrobots.txt

    • سجل public/sitemap.xml المولد تلقائياً في Google Search Console
    • أضف قواعد الزواحف في static/robots.txt
  2. ضبط OGP (Open Graph) وبطاقات تويتر

    • أضف عناصر <meta property="og:title"> و<meta property="og:image"> إلى layouts/_default/baseof.html
    • يساعد ذلك على ظهور الروابط بشكل جذاب عند مشاركتها على الشبكات الاجتماعية
  3. تحويل الصور إلى WebP

    • استعمل resources/_gen/images/ وخط أنابيب Hugo لتحويل الصور تلقائياً إلى WebP
    • يسرّع زمن تحميل الصفحات
  4. إضافة وظائف التصنيفات والوسوم

    • أضف categories وtags إلى Front Matter في content/blog/ واعرض غيمة وسوم في القالب
  5. إدخال البيانات المنظمة

    • استخدم JSON-LD لتمرير معلومات المقالات لمحركات البحث والحصول على نتائج غنية

8. الخلاصة

  • أنشئ مستودع GitHub، واربطه بـ Cloudflare Pages، وأعدد OAuth لـ DecapCMS واربط النطاق المخصص ─ وهكذا يكتمل الهيكل
  • المقالات تُنشأ من /admin/ → تُحفظ في GitHub → تنشر تلقائياً عبر Cloudflare
  • عدّل القوالب والتصميم محلياً عبر hugo server، ثم ادفع التغييرات باستخدام Git

بهذه الخطوات تحصل على بيئة مدونات بلا خوادم باستخدام Hugo + GitHub + Decap CMS + Cloudflare قابلة للتشغيل والإدارة.

بعد اكتمال البنية الأساسية منخفضة التكلفة، ما زلت أشعر أن الموقع ينقصه البحث الداخلي والتصنيفات وغيمة الوسوم مقارنة بالمدونات الناضجة. لكن لا بأس أن نبنيها تدريجياً بالعمل الثنائي مع ChatGPT.