العودة إلى المدونة

مدونة Markdown في Next.js دون نظام إدارة محتوى أو قاعدة بيانات

مدونة Markdown في Next.js دون نظام إدارة محتوى أو قاعدة بيانات

8 أكتوبر 2026 — Next.js

تاريخ النشر

8 أكتوبر 2026

مدة القراءة

2 دقيقة للقراءة

التصنيف

Next.js

الوسوم

  • Next.js
  • Markdown
  • Blog
  • remark
  • Static Site

(مقدمة)مدونة كل مقالة فيها ملف Markdown في مستودع Git الخاص بك. البيانات الوصفية (Front Matter)، وتلوين الشيفرة، والصور، والفيديو، والصفحات الثابتة باستخدام Next.js App Router.

المدونة التي تقرأها الآن بلا قاعدة بيانات وبلا نظام إدارة محتوى مستضاف. كل مقالة مجلد في مستودع Git يحتوي على index.md وصوره. وعند دفع أي إيداع (commit) يُبنى الموقع وتُنشر المقالة. إليك كيف تبني الشيء نفسه باستخدام Next.js App Router.

لماذا Markdown داخل Git

  • مجاني وسريع. لا اشتراك في نظام إدارة محتوى، ولا استعلامات لقاعدة بيانات؛ كل صفحة HTML ثابت.
  • مُتتبَّع الإصدارات. كل تعديل إيداع يمكنك مراجعته والتراجع عنه.
  • قابل للنقل. يعيش Markdown أطول من أي منصة. والانتقال إلى إطار عمل آخر يعني تغيير أداة العرض، لا ترحيل المحتوى.
  • قابل للتعديل من أي مكان. محرر GitHub على الويب، أو VS Code، أو نظام إدارة محتوى مبني على Git مثل Decap أو Sveltia لمن يفضّل النماذج.

المقابل: يحتاج المحررون غير التقنيين إلى قليل من Git، أو إلى نظام إدارة محتوى مبني على Git أمامه.

بنية المجلدات

مجلد لكل مقالة يُبقي الصور بجوار النص الذي يستخدمها، كما تظهر الروابط النسبية بشكل صحيح في معاينة GitHub:

text
content/blog/
  my-first-post/
    index.md
    cover.jpg
    diagram.png

تبدأ المقالة ببيانات وصفية بصيغة YAML:

content/blog/my-first-post/index.md
---
title: My first post
date: 2026-10-08
excerpt: One or two sentences for the listing and the meta description.
cover: ./cover.jpg
tags: [Next.js, Markdown]
---

The post starts here.

الاعتماديات

bash
npm install gray-matter remark remark-gfm remark-html highlight.js
  • gray-matter تفصل البيانات الوصفية عن المتن.
  • remark مع remark-gfm تحلّلان Markdown مع امتدادات GitHub (الجداول وقوائم المهام).
  • remark-html تحوّله إلى HTML.
  • highlight.js تلوّن كتل الشيفرة أثناء البناء، فلا يحمّل القرّاء أي أداة تلوين.

تحميل المقالات

lib/blog.ts
import fs from 'node:fs'
import path from 'node:path'
import matter from 'gray-matter'
import { remark } from 'remark'
import remarkGfm from 'remark-gfm'
import remarkHtml from 'remark-html'

const DIR = path.join(process.cwd(), 'content/blog')

export async function getAllPosts() {
  const slugs = fs.readdirSync(DIR).filter((name) =>
    fs.existsSync(path.join(DIR, name, 'index.md')),
  )
  const posts = await Promise.all(slugs.map(getPost))
  return posts.sort((a, b) => b.date.localeCompare(a.date))
}

export async function getPost(slug: string) {
  const file = fs.readFileSync(path.join(DIR, slug, 'index.md'), 'utf8')
  const { data, content } = matter(file)
  const html = String(
    await remark().use(remarkGfm).use(remarkHtml, { sanitize: false }).process(content),
  )
  return {
    slug,
    title: String(data.title),
    date: new Date(data.date).toISOString().slice(0, 10),
    excerpt: String(data.excerpt ?? ''),
    html,
  }
}

يسمح sanitize: false بمرور HTML الخام في المقالات، ولا بأس بذلك إن كنت الكاتب الوحيد. أما إن كان بإمكان آخرين إرسال مقالات، فاستمر في التنقية.

وبما أن هذا لا يعمل إلا في مكونات الخادم وgenerateStaticParams، فلا مشكلة في fs: إنه يعمل أثناء البناء، لا في المتصفح أبدًا.

الصفحات

تعرض صفحة الفهرس المقالات، ومسار التفاصيل ثابت لكل slug:

app/blog/[slug]/page.tsx
import { getAllPosts, getPost } from '@/lib/blog'

export const dynamicParams = false

export async function generateStaticParams() {
  return (await getAllPosts()).map(({ slug }) => ({ slug }))
}

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  )
}

تلوين الشيفرة أثناء البناء

إضافة remark صغيرة تستبدل كل عقدة شيفرة بـ HTML ملوّن:

lib/highlight.ts
import hljs from 'highlight.js'
import { visit } from 'unist-util-visit'

export function highlightCode() {
  return (tree: any) => {
    visit(tree, 'code', (node: any, index, parent: any) => {
      const lang = node.lang && hljs.getLanguage(node.lang) ? node.lang : null
      const html = lang
        ? hljs.highlight(node.value, { language: lang }).value
        : hljs.highlightAuto(node.value).value
      parent.children[index!] = {
        type: 'html',
        value: `<pre><code class="hljs language-${lang ?? 'text'}">${html}</code></pre>`,
      }
    })
  }
}

أضفها قبل remarkHtml بـ .use(highlightCode)، واستورد سمة من highlight.js مثل highlight.js/styles/github-dark.css.

الصور والفيديو

يقدّم Next.js الملفات الثابتة من public/، لذا يجب أن تنتهي وسائط المقالات هناك. سكربت صغير يعمل قبل dev وbuild ينسخ كل ما ليس Markdown:

scripts/copy-blog-media.mjs
import fs from 'node:fs'
import path from 'node:path'

const SRC = 'content/blog'
const OUT = 'public/blog-media'
fs.rmSync(OUT, { recursive: true, force: true })

for (const slug of fs.readdirSync(SRC)) {
  const dir = path.join(SRC, slug)
  if (!fs.statSync(dir).isDirectory()) continue
  for (const file of fs.readdirSync(dir)) {
    if (file.endsWith('.md')) continue
    fs.mkdirSync(path.join(OUT, slug), { recursive: true })
    fs.copyFileSync(path.join(dir, file), path.join(OUT, slug, file))
  }
}
package.json
{
  "scripts": {
    "predev": "node scripts/copy-blog-media.mjs",
    "prebuild": "node scripts/copy-blog-media.mjs"
  }
}

ثم في إضافة remark أخرى، أعد كتابة روابط الصور النسبية (./cover.jpg) إلى /blog-media/<slug>/cover.jpg. وما دمت هناك، حوّل الصور التي تشير إلى ملفات .mp4 أو .webm إلى عناصر <video controls>، وروابط YouTube إلى مقاطع مضمّنة؛ ويواصل الكتّاب استخدام صيغة الصور العادية في Markdown.

أضف public/blog-media إلى .gitignore؛ فهو مجلد مولَّد.

النشر

اربط المستودع بمستضيف للمواقع الثابتة (Cloudflare Pages أو Netlify أو Vercel)، مع npm run build أمرًا للبناء. وتصبح كتابة مقالة كالتالي:

  1. أنشئ مجلدًا وindex.md.
  2. أودِع التعديل وادفعه.
  3. يبني المستضيف الموقع وينشره خلال دقائق.

إضافات لتحسين محركات البحث تستحق العناء

  • generateMetadata مع عنوان المقالة ومقتطفها ورابطها الأساسي (canonical) وصورة الغلاف كصورة Open Graph.
  • ملف sitemap.ts يسرد كل المقالات.
  • بيانات JSON-LD من نوع BlogPosting.
  • معرّفات للعناوين، ليمكن الربط بالأقسام.

يغطي كل ذلك مقال تحسين محركات البحث تقنيًا في Next.js App Router.

متى تتجاوزه

إذا كان عدة أشخاص غير تقنيين ينشرون يوميًا، ويحتاجون إلى جدولة المقالات أو مسارات تحرير ومراجعة، يبدأ نظام إدارة المحتوى بلا واجهة (headless CMS) في استرداد تكلفته. أما لموقع شخصي، أو استوديو، أو مدونة منتج تنشر بضع مقالات شهريًا، فمن الصعب التفوق على Markdown داخل Git.

النشرة البريدية

تصلك المقالات الجديدة على بريدك

رسالة قصيرة عند نشر كل جديد. بلا رسائل مزعجة، ويمكنك إلغاء الاشتراك في أي وقت.

نموذج تواصل بلا خوادم باستخدام Cloudflare Workers وD1
نموذج تواصل بلا خوادم باستخدام Cloudflare Workers وD1
المقال التالي