بناء مدونة احترافية متعددة اللغات باستخدام Nuxt Content و Nuxt i18n

رائد البحري
رائد البحري
المؤسس و مطور منتجات10 months agoقراءة 25 دقيقة
بناء مدونة احترافية متعددة اللغات باستخدام Nuxt Content و Nuxt i18n

هذا الدليل سيرشدك خطوة بخطوة لبناء نظام مدونة متعدد اللغات متكامل باستخدام Nuxt 3 و Nuxt Content و Nuxt i18n. أنت تقرأ الآن مدونة مبنية بالضبط بهذه التقنيات! سأشارك معك تفاصيل التطبيق الحقيقي والكود والممارسات الأفضل التي استخدمتها.

لماذا بناء مدونة متعددة اللغات؟#

قبل الخوض في التفاصيل التقنية، دعنا نفهم الفوائد:

  • وصول أوسع: الوصول إلى جماهير عالمية بلغاتهم الأم
  • تحسين محركات البحث: الظهور في نتائج البحث عبر مناطق لغوية مختلفة
  • تجربة مستخدم أفضل: المستخدمون يتفاعلون أكثر مع المحتوى بلغتهم المفضلة
  • صورة احترافية: تظهر الالتزام بخدمة جماهير متنوعة

نظرة عامة على المشروع#

تدعم هذه المدونة كلاً من الإنجليزية والعربية (مع دعم RTL)، وتتضمن:

  • تنظيم المحتوى حسب اللغة
  • توطين ديناميكي للفئات والكتّاب
  • تحسين SEO مع علامات hreflang
  • مبدّل اللغات
  • تصميم متجاوب مع دعم RTL/LTR

الخطوة 1: إعداد المشروع#

أولاً، أنشئ مشروع Nuxt 3 جديد:

bash
npx nuxi@latest init my-multilingual-blog
cd my-multilingual-blog
npm install

ثبّت المكتبات المطلوبة:

bash
npm install @nuxt/content @nuxtjs/i18n @nuxtjs/tailwindcss
npm install dayjs

الخطوة 2: تكوين وحدات Nuxt#

حدّث ملف nuxt.config.ts:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    "@nuxt/content",
    "@nuxtjs/i18n",
    "@nuxtjs/tailwindcss"
  ],
  
  i18n: {
    locales: [
      {
        code: "en",
        iso: "en-US",
        name: "English",
        file: "en.json",
        dir: "ltr"
      },
      {
        code: "ar",
        iso: "ar-SA",
        name: "العربية",
        file: "ar.json",
        dir: "rtl" // مهم للغة العربية
      }
    ],
    defaultLocale: "en",
    strategy: "prefix_except_default", // روابط: /blog, /ar/blog
    lazy: true,
    langDir: "i18n/locales/",
    detectBrowserLanguage: {
      useCookie: true,
      cookieKey: "i18n_redirected",
      redirectOn: "root"
    }
  },

  content: {
    highlight: {
      theme: "github-dark"
    }
  }
});

الخطوة 3: إنشاء هيكل المحتوى#

نظّم محتواك حسب اللغة:

text
content/
├── en/
│   ├── articles/
│   │   ├── my-first-post.md
│   │   └── another-post.md
│   ├── authors/
│   │   └── raed-bahri.md
│   └── categories/
│       └── development.md
├── ar/
│   ├── articles/
│   │   ├── my-first-post.md
│   │   └── another-post.md
│   ├── authors/
│   │   └── رائد-البحري.md
│   └── categories/
│       └── التطوير.md

مثال على مقال إنجليزي في content/en/articles/my-first-post.md:

markdown
---
title: "My First Blog Post"
description: "Introduction to my multilingual blog"
publishedAt: "2025-01-10"
author: "رائد-البحري"
category: "development"
tags:
  - nuxt
  - tutorial
image: "/images/blog/post-1.jpg"
isFeatured: true
readingTime: 5
locale: "en"
---

Your content here...

مثال على مقال عربي في content/ar/articles/my-first-post.md:

markdown
---
title: "مقالتي الأولى"
description: "مقدمة إلى مدونتي متعددة اللغات"
publishedAt: "2025-01-10"
author: "رائد-البحري"
category: "development"
tags:
  - nuxt
  - تعليم
image: "/images/blog/post-1.jpg"
isFeatured: true
readingTime: 5
locale: "ar"
---

المحتوى هنا...

الخطوة 4: إنشاء تعريفات الأنواع#

أنشئ ملف types/content.ts لدعم TypeScript:

typescript
// types/content.ts
import { z } from "zod";

export const ArticleSchema = z.object({
  title: z.string(),
  description: z.string(),
  publishedAt: z.string(),
  author: z.string(),
  category: z.string(),
  tags: z.array(z.string()).optional(),
  image: z.string().optional(),
  isFeatured: z.boolean().optional(),
  readingTime: z.number().optional(),
  locale: z.string(),
});

export type Article = z.infer<typeof ArticleSchema> & {
  path: string;
  _path: string;
};

export type Category = {
  title: string;
  description: string;
  slug: string;
  image?: string;
  locale: string;
  _path: string;
};

export type Author = {
  name: string;
  slug: string;
  bio: string;
  avatar: string;
  locale: string;
};

// دوال مساعدة للحصول على المجموعات
export const getArticlesCollection = (locale: string) => {
  return `${locale}/articles`;
};

export const getCategoriesCollection = (locale: string) => {
  return `${locale}/categories`;
};

export const getAuthorsCollection = (locale: string) => {
  return `${locale}/authors`;
};

الخطوة 5: بناء صفحة فهرس المدونة#

أنشئ ملف app/pages/index.vue:

vue
<script setup lang="ts">
import { getArticlesCollection, type Article } from "@@/types/content";

const { locale } = useI18n();
const localePath = useLocalePath();

// جلب المقالات للغة الحالية
const { data: posts } = await useAsyncData<Article[]>(
  `articles-${locale.value}`,
  async () => {
    const collection = getArticlesCollection(locale.value);
    return queryCollection(collection).all();
  },
  { watch: [locale] } // مهم: مراقبة تغييرات اللغة
);

// الحصول على المقالات المميزة
const featuredPosts = computed(() => 
  posts.value?.filter(post => post.isFeatured) || []
);
</script>

<template>
  <main>
    <section class="container py-12">
      <h1 class="text-4xl font-bold mb-8">
        {{ $t('blog.title') }}
      </h1>
      
      <!-- شبكة المقالات المميزة -->
      <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
        <NuxtLink
          v-for="post in featuredPosts"
          :key="post.path"
          :to="localePath(post.path)"
          class="border rounded-lg overflow-hidden hover:shadow-lg transition"
        >
          <img 
            :src="post.image" 
            :alt="post.title"
            class="w-full h-48 object-cover"
          />
          <div class="p-4">
            <h2 class="text-xl font-bold mb-2">{{ post.title }}</h2>
            <p class="text-gray-600">{{ post.description }}</p>
          </div>
        </NuxtLink>
      </div>
    </section>
  </main>
</template>

الخطوة 6: إنشاء صفحة تفاصيل المقال#

أنشئ ملف app/pages/[...slug].vue لعرض المقالات الديناميكية:

vue
<script setup lang="ts">
const { locale } = useI18n();
const route = useRoute();

// إنشاء المسار الكامل مع اللغة
const path = `/${locale.value}/articles/${route.params.slug}`;

const { data: article } = await useAsyncData(`article-${path}`, () =>
  queryContent()
    .where({ _path: path })
    .findOne()
);

if (!article.value) {
  throw createError({ 
    statusCode: 404, 
    statusMessage: "المقال غير موجود" 
  });
}

// تعيين meta tags لتحسين SEO
useHead({
  title: article.value.title,
  meta: [
    { name: "description", content: article.value.description },
    { property: "og:title", content: article.value.title },
    { property: "og:description", content: article.value.description },
    { property: "og:image", content: article.value.image },
  ],
});
</script>

<template>
  <article class="container prose prose-lg mx-auto py-12">
    <h1>{{ article.title }}</h1>
    <div class="text-gray-600 mb-8">
      {{ article.publishedAt }} • {{ article.readingTime }} دقيقة قراءة
    </div>
    
    <ContentRenderer :value="article" />
  </article>
</template>

الخطوة 7: تنفيذ مبدّل اللغات#

أنشئ مكون app/components/LanguageSwitcher.vue:

vue
<script setup lang="ts">
const { locale, locales } = useI18n();
const switchLocalePath = useSwitchLocalePath();

const availableLocales = computed(() => 
  (locales.value as Array<{code: string, name: string}>)
    .filter(l => l.code !== locale.value)
);
</script>

<template>
  <div class="flex gap-2">
    <NuxtLink
      v-for="loc in availableLocales"
      :key="loc.code"
      :to="switchLocalePath(loc.code)"
      class="px-3 py-1 rounded border hover:bg-gray-100"
    >
      {{ loc.name }}
    </NuxtLink>
  </div>
</template>

الخطوة 8: عرض الفئات الموطّنة#

أنشئ composable للحصول على عناوين الفئات باللغة الحالية.

أنشئ ملف app/composables/useCategoryTitle.ts:

typescript
import { getCategoriesCollection, type Category } from "@@/types/content";

export const useCategoryTitle = async (categorySlug: string) => {
  const { locale } = useI18n();
  
  const { data: category } = await useAsyncData<Category | null>(
    `category-${locale.value}-${categorySlug}`,
    async () => {
      const collection = getCategoriesCollection(locale.value);
      const foundCategory = await queryCollection(collection)
        .where("slug", "=", categorySlug)
        .first();
      return foundCategory ?? null;
    }
  );

  return computed(() => category.value?.title || categorySlug);
};

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

vue
<script setup lang="ts">
const props = defineProps<{ categorySlug: string }>();
const categoryTitle = await useCategoryTitle(props.categorySlug);
</script>

<template>
  <span>{{ categoryTitle }}</span>
</template>

مثال في app/components/blog/BlogPostCard.vue:

vue
<script setup lang="ts">
import { getCategoriesCollection, type Category } from "@@/types/content";

const props = defineProps<{ post: Article }>();
const { locale } = useI18n();

// الحصول على عنوان الفئة باللغة الحالية
const { data: category } = await useAsyncData<Category | null>(
  `category-${locale.value}-${props.post.category}`,
  async () => {
    const collection = getCategoriesCollection(locale.value);
    const foundCategory = await queryCollection(collection)
      .where("slug", "=", props.post.category)
      .first();
    return foundCategory ?? null;
  }
);

const categoryTitle = computed(() => 
  category.value?.title || props.post.category
);
</script>

<template>
  <div>
    <!-- سيظهر "التطوير" بدلاً من "development" في النسخة العربية -->
    <Badge>{{ categoryTitle }}</Badge>
  </div>
</template>

الخطوة 9: تحسين محركات البحث (SEO)#

أضف علامات hreflang في التخطيطات (app/layouts/default.vue):

vue
<script setup lang="ts">
const { locale } = useI18n();

const i18nHead = useLocaleHead({
  addDirAttribute: true,
  identifierAttribute: "id",
  addSeoAttributes: true,
});

useHead(() => ({
  htmlAttrs: {
    lang: i18nHead.value.htmlAttrs!.lang,
    dir: locale.value === 'ar' ? 'rtl' : 'ltr',
  },
  link: [...(i18nHead.value.link || [])],
  meta: [...(i18nHead.value.meta || [])],
}));
</script>

الخطوة 10: دعم RTL مع Tailwind#

حدّث ملف tailwind.config.ts:

typescript
export default {
  content: [
    "./components/**/*.{js,vue,ts}",
    "./layouts/**/*.vue",
    "./pages/**/*.vue",
    "./app.vue",
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require("@tailwindcss/typography"),
  ],
};

أضف فئات الأدوات لـ RTL في app/assets/css/tailwind.css:

css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  * {
    @apply rtl:font-arabic ltr:font-latin;
  }
}

كوّن الخطوط في nuxt.config.ts:

typescript
app: {
  head: {
    link: [
      {
        rel: 'stylesheet',
        href: 'https://fonts.googleapis.com/css2?family=Almarai:wght@300;400;700&display=swap',
      },
      {
        rel: 'stylesheet',
        href: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap',
      },
    ],
  },
},

أفضل الممارسات والنصائح#

1. استخدم slugs متسقة#

احتفظ بنفس الـ slug عبر اللغات لسهولة الربط المتبادل:

  • EN: content/en/articles/my-post.md
  • AR: content/ar/articles/my-post.md

2. نفّذ محتوى احتياطي#

قدّم دائماً قيماً احتياطية عندما قد يكون المحتوى مفقوداً:

vue
const categoryTitle = computed(() => 
  category.value?.title || post.category || 'غير مصنف'
);

3. راقب تغييرات اللغة#

تأكد من مراقبة تغييرات اللغة في useAsyncData:

typescript
const { data } = await useAsyncData(
  `key-${locale.value}`,
  () => fetchData(),
  { watch: [locale] }  // مهم!
);

4. اختبر كل من LTR و RTL#

اختبر دائماً تخطيطاتك في كلا الاتجاهين:

  • تحقق من الهوامش والحشوات (ms-4 بدلاً من ml-4)
  • تحقق من مواضع الأيقونات
  • اختبر تخطيطات النماذج
  • تحقق من قوائم التنقل

5. حسّن الصور#

استخدم Nuxt Image للتحسين التلقائي:

vue
<NuxtImg
  :src="post.image"
  :alt="post.title"
  width="800"
  height="400"
  format="webp"
  loading="lazy"
/>

المشاكل الشائعة والحلول#

المشكلة: اللغة لا تستمر#

الحل: تأكد من تفعيل الكشف عن ملفات تعريف الارتباط في تكوين i18n:

typescript
detectBrowserLanguage: {
  useCookie: true,
  cookieKey: "i18n_redirected",
}

المشكلة: المحتوى لا يتحدث عند تبديل اللغة#

الحل: استخدم خيار watch في useAsyncData:

typescript
{ watch: [locale] }

المشكلة: تخطيط RTL ينكسر#

الحل: استخدم الخصائص المنطقية (start/end بدلاً من left/right):

vue
<!-- سيء -->
<div class="ml-4">

<!-- جيد -->
<div class="ms-4">

اعتبارات النشر#

1. توليد المسارات الثابتة#

أضف هذا إلى nuxt.config.ts للتوليد الثابت:

typescript
nitro: {
  prerender: {
    routes: ["/", "/ar"],
    crawlLinks: true,
  },
},

2. كوّن مسارات الخادم#

تأكد من أن خادمك يتعامل مع بادئات اللغة بشكل صحيح.

3. أضف خريطة الموقع#

ثبّت @nuxtjs/sitemap وكوّنها للغات متعددة:

typescript
sitemap: {
  hostname: 'https://yourdomain.com',
  i18n: true,
}

الخلاصة#

بناء مدونة متعددة اللغات مع Nuxt 3 و Nuxt Content و Nuxt i18n أمر مباشر بمجرد فهم المفاهيم الأساسية. المفتاح هو:

  1. نظّم المحتوى حسب اللغة من البداية
  2. استخدم تعريفات الأنواع المناسبة لتجربة تطوير أفضل
  3. نفّذ composables متسقة للمهام الشائعة
  4. اختبر بدقة في جميع اللغات المدعومة
  5. حسّن لمحركات البحث مع علامات meta و hreflang المناسبة

هذا النهج يتوسع بشكل جيد - يمكنك إضافة لغات جديدة ببساطة عن طريق إنشاء مجلدات محتوى جديدة وتحديث تكوين i18n الخاص بك.

المدونة التي تقرأها الآن تستخدم هذه البنية بالضبط، وتتعامل مع الإنجليزية والعربية مع دعم RTL الكامل، وفئات موطّنة، وتحسين SEO. يمكنك استكشاف الكود المصدري وتكييفه لمشاريعك الخاصة.

برمجة سعيدة! 🚀