بناء موقع حديث باستخدام Sanity v3 و Next.js App Router

Noqta Team
بواسطة Noqta Team ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

Sanity v3 هو منصة محتوى قابلة للتركيب تمنحك Studio قابلاً للتخصيص بالكامل، ومخزن محتوى يعمل في الوقت الفعلي، و GROQ — واحدة من أقوى لغات استعلام المحتوى المتاحة. في هذا البرنامج التعليمي، ستبني مدونة كاملة مع معاينة مباشرة وتحسين الصور وتجربة تحرير مخصصة.

ما الذي ستبنيه

DevJournal — منصة مدونة للمطورين تتضمن:

  • Sanity Studio v3 مدمج داخل تطبيق Next.js
  • مخططات مستندات مخصصة للمقالات والمؤلفين والتصنيفات
  • استعلامات GROQ لجلب المحتوى بمرونة
  • معاينة مباشرة تعرض تغييرات المسودة في الوقت الفعلي
  • Sanity Image مع تحسين تلقائي للصور المتجاوبة
  • عرض Portable Text للمحتوى الغني
  • تحرير مرئي مع وظيفة النقر للتحرير
  • إعادة التحقق عند الطلب عبر webhooks
  • بيانات SEO الوصفية وتوليد صور Open Graph
  • تنسيق باستخدام Tailwind CSS
  • نشر في بيئة الإنتاج على Vercel

المتطلبات الأساسية

قبل البدء، تأكد من توفر:

  • Node.js 18+ مثبت
  • npm أو pnpm كمدير حزم
  • معرفة أساسية بـ React و TypeScript و Next.js App Router
  • حساب Sanity.io (متاح مستوى مجاني على sanity.io)
  • محرر أكواد (يُنصح بـ VS Code)
  • إلمام بمفاهيم إدارة المحتوى

لماذا Sanity v3؟

تتبع Sanity نهجاً مختلفاً جذرياً في إدارة المحتوى. بدلاً من تخزين المحتوى في قاعدة بيانات تقليدية، تستخدم مخزن محتوى يعمل في الوقت الفعلي — مخزن بيانات سحابي بدون مخطط يتزامن في الوقت الفعلي.

الميزةSanity v3Strapi 5Payload CMS 3Contentful
البنيةمخزن محتوى سحابيخادم مستضاف ذاتياًداخل Next.jsSaaS سحابي
لغة الاستعلامGROQ (مخصصة)REST / GraphQLREST / GraphQLREST / GraphQL
Studioتطبيق React قابل للتخصيصلوحة إدارةإدارة Next.jsلوحة سحابية
الوقت الفعليمدمجيتطلب إعداديتطلب إعدادWebhooks فقط
خط أنابيب الصورCDN مدمج + تحويلاتيتطلب إضافةمحول رفعCDN مدمج
التسعيرمستوى مجاني سخيمجاني (مستضاف ذاتياً)مجاني (مستضاف ذاتياً)مستوى مجاني محدود
Portable Textصيغة نص غني أصليةBlocks / Markdownمحرر LexicalRich text JSON
المعاينة المباشرةدعم من الدرجة الأولىإضافة مجتمعيةمدمجةمحدودة

المزايا الرئيسية لـ Sanity: التعاون في الوقت الفعلي، و لغة استعلام GROQ المرنة للغاية، و Studio قابل للتخصيص بنسبة 100% لأنه مجرد تطبيق React.

الخطوة 1: إنشاء مشروع Next.js

ابدأ بإنشاء تطبيق Next.js جديد:

npx create-next-app@latest devjournal --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd devjournal

الخطوة 2: تثبيت حزم Sanity

ثبّت عميل Sanity و Studio والحزم المرتبطة:

npm install next-sanity @sanity/image-url @sanity/vision @portabletext/react
npm install -D @sanity/types

حزمة next-sanity هي التكامل الرسمي التي توفر:

  • عميل Sanity مهيأ لـ Next.js
  • أدوات المعاينة المباشرة
  • منشئ عناوين URL للصور
  • مساعدات تضمين Studio

الخطوة 3: إنشاء مشروع Sanity

إذا لم يكن لديك مشروع Sanity بالفعل، أنشئ واحداً:

npx sanity@latest init --env

عند المطالبة:

  • اسم المشروع: devjournal
  • استخدام تكوين مجموعة البيانات الافتراضي؟ نعم
  • مسار إخراج المشروع: اختر الدليل الحالي
  • حدد قالب المشروع: مشروع نظيف بدون مخططات محددة مسبقاً

هذا ينشئ ملف .env.local مع بيانات اعتماد مشروعك:

NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
NEXT_PUBLIC_SANITY_DATASET="production"
SANITY_API_READ_TOKEN="your-read-token"

يمكنك أيضاً العثور على معرف مشروعك في لوحة تحكم Sanity على sanity.io/manage.

الخطوة 4: تكوين عميل Sanity

أنشئ ملفات تكوين Sanity:

// src/sanity/config.ts
export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!
export const apiVersion = '2026-04-07'

الآن أنشئ عميل Sanity:

// src/sanity/client.ts
import { createClient } from 'next-sanity'
import { projectId, dataset, apiVersion } from './config'
 
export const client = createClient({
  projectId,
  dataset,
  apiVersion,
  useCdn: true,
})
 
// عميل المعاينة مع رمز مميز للمحتوى المسودة
export const previewClient = createClient({
  projectId,
  dataset,
  apiVersion,
  useCdn: false,
  token: process.env.SANITY_API_READ_TOKEN,
  perspective: 'previewDrafts',
})
 
export function getClient(preview = false) {
  return preview ? previewClient : client
}

الخطوة 5: تعريف مخططات المحتوى

تحدد مخططات Sanity بنية محتواك. أنشئ مخططات للمقالات والمؤلفين والتصنيفات.

مخطط المؤلف

// src/sanity/schemas/author.ts
import { defineField, defineType } from 'sanity'
 
export const author = defineType({
  name: 'author',
  title: 'Author',
  type: 'document',
  fields: [
    defineField({
      name: 'name',
      title: 'Name',
      type: 'string',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: {
        source: 'name',
        maxLength: 96,
      },
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'image',
      title: 'Image',
      type: 'image',
      options: { hotspot: true },
    }),
    defineField({
      name: 'bio',
      title: 'Bio',
      type: 'array',
      of: [{ type: 'block' }],
    }),
  ],
  preview: {
    select: {
      title: 'name',
      media: 'image',
    },
  },
})

مخطط التصنيف

// src/sanity/schemas/category.ts
import { defineField, defineType } from 'sanity'
 
export const category = defineType({
  name: 'category',
  title: 'Category',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: {
        source: 'title',
        maxLength: 96,
      },
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'description',
      title: 'Description',
      type: 'text',
    }),
  ],
})

مخطط المقالة

// src/sanity/schemas/post.ts
import { defineField, defineType } from 'sanity'
 
export const post = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: {
        source: 'title',
        maxLength: 96,
      },
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'author',
      title: 'Author',
      type: 'reference',
      to: [{ type: 'author' }],
      validation: (rule) => rule.required(),
    }),
    defineField({
      name: 'mainImage',
      title: 'Main Image',
      type: 'image',
      options: { hotspot: true },
      fields: [
        {
          name: 'alt',
          type: 'string',
          title: 'Alternative Text',
        },
      ],
    }),
    defineField({
      name: 'categories',
      title: 'Categories',
      type: 'array',
      of: [{ type: 'reference', to: [{ type: 'category' }] }],
    }),
    defineField({
      name: 'publishedAt',
      title: 'Published at',
      type: 'datetime',
    }),
    defineField({
      name: 'excerpt',
      title: 'Excerpt',
      type: 'text',
      rows: 3,
    }),
    defineField({
      name: 'body',
      title: 'Body',
      type: 'array',
      of: [
        { type: 'block' },
        {
          type: 'image',
          options: { hotspot: true },
          fields: [
            { name: 'alt', type: 'string', title: 'Alternative Text' },
            { name: 'caption', type: 'string', title: 'Caption' },
          ],
        },
        {
          type: 'code',
          title: 'Code Block',
        },
      ],
    }),
    defineField({
      name: 'seo',
      title: 'SEO',
      type: 'object',
      fields: [
        { name: 'metaTitle', type: 'string', title: 'Meta Title' },
        { name: 'metaDescription', type: 'text', title: 'Meta Description', rows: 3 },
        { name: 'ogImage', type: 'image', title: 'Open Graph Image' },
      ],
    }),
  ],
  preview: {
    select: {
      title: 'title',
      author: 'author.name',
      media: 'mainImage',
    },
    prepare(selection) {
      const { author } = selection
      return { ...selection, subtitle: author ? `بواسطة ${author}` : '' }
    },
  },
  orderings: [
    {
      title: 'تاريخ النشر، الأحدث',
      name: 'publishedAtDesc',
      by: [{ field: 'publishedAt', direction: 'desc' }],
    },
  ],
})

فهرس المخططات

// src/sanity/schemas/index.ts
import { author } from './author'
import { category } from './category'
import { post } from './post'
 
export const schemaTypes = [author, category, post]

الخطوة 6: تكوين Sanity Studio

أنشئ ملف تكوين Studio:

// src/sanity/studio.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemas'
import { projectId, dataset } from './config'
 
export default defineConfig({
  name: 'devjournal-studio',
  title: 'DevJournal Studio',
  projectId,
  dataset,
  plugins: [structureTool(), visionTool()],
  schema: {
    types: schemaTypes,
  },
})

الخطوة 7: تضمين Studio في Next.js

أنشئ مساراً لـ Sanity Studio داخل تطبيق Next.js:

// src/app/studio/[[...tool]]/page.tsx
'use client'
 
import { NextStudio } from 'next-sanity/studio'
import config from '@/sanity/studio'
 
export default function StudioPage() {
  return <NextStudio config={config} />
}

أضف ملف تخطيط لمنع تأثر Studio بتخطيط تطبيقك:

// src/app/studio/[[...tool]]/layout.tsx
export const metadata = {
  title: 'DevJournal Studio',
}
 
export default function StudioLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

الآن قم بزيارة http://localhost:3000/studio للوصول إلى Sanity Studio الخاص بك. سترى واجهة التحرير الكاملة مع مخططاتك المخصصة.

الخطوة 8: كتابة استعلامات GROQ

GROQ (Graph-Relational Object Queries) هي لغة استعلام Sanity. إنها قوية للغاية لجلب البيانات التي تحتاجها بالضبط.

أنشئ ملف الاستعلامات:

// src/sanity/queries.ts
import { groq } from 'next-sanity'
 
// جلب جميع المقالات المنشورة
export const postsQuery = groq`
  *[_type == "post" && defined(publishedAt)] | order(publishedAt desc) {
    _id,
    title,
    slug,
    excerpt,
    publishedAt,
    mainImage,
    "author": author->{name, slug, image},
    "categories": categories[]->{ title, slug }
  }
`
 
// جلب مقالة واحدة بالـ slug
export const postBySlugQuery = groq`
  *[_type == "post" && slug.current == $slug][0] {
    _id,
    title,
    slug,
    excerpt,
    publishedAt,
    mainImage,
    body,
    "author": author->{name, slug, image, bio},
    "categories": categories[]->{ title, slug },
    seo
  }
`
 
// جلب جميع الـ slugs للتوليد الثابت
export const postSlugsQuery = groq`
  *[_type == "post" && defined(slug.current)][].slug.current
`
 
// جلب المقالات حسب التصنيف
export const postsByCategoryQuery = groq`
  *[_type == "post" && $categorySlug in categories[]->slug.current] | order(publishedAt desc) {
    _id,
    title,
    slug,
    excerpt,
    publishedAt,
    mainImage,
    "author": author->{name, slug, image},
    "categories": categories[]->{ title, slug }
  }
`
 
// المقالات ذات الصلة
export const relatedPostsQuery = groq`
  *[_type == "post" && _id != $currentId && count(categories[@._ref in $categoryIds]) > 0] | order(publishedAt desc) [0...3] {
    _id,
    title,
    slug,
    excerpt,
    publishedAt,
    mainImage
  }
`

لاحظ كيف يستخدم GROQ عامل التشغيل -> لحل المراجع بشكل مضمن. هذه واحدة من أقوى ميزاته — يمكنك ربط المستندات ذات الصلة بدون كتابة استعلامات معقدة.

الخطوة 9: إعداد معالجة الصور

يوفر Sanity خط أنابيب صور قوي مع تغيير الحجم والقص وتحويل التنسيق تلقائياً. أنشئ أداة مساعدة للصور:

// src/sanity/image.ts
import createImageUrlBuilder from '@sanity/image-url'
import { projectId, dataset } from './config'
import type { Image } from 'sanity'
 
const imageBuilder = createImageUrlBuilder({ projectId, dataset })
 
export function urlForImage(source: Image | undefined) {
  if (!source) return undefined
  return imageBuilder.image(source).auto('format').fit('max')
}

أنشئ مكون صورة قابل لإعادة الاستخدام:

// src/components/sanity-image.tsx
import Image from 'next/image'
import { urlForImage } from '@/sanity/image'
import type { Image as SanityImageType } from 'sanity'
 
interface SanityImageProps {
  image: SanityImageType & { alt?: string }
  width?: number
  height?: number
  className?: string
  priority?: boolean
}
 
export function SanityImage({
  image,
  width = 800,
  height = 450,
  className,
  priority = false,
}: SanityImageProps) {
  const imageUrl = urlForImage(image)?.url()
  if (!imageUrl) return null
 
  return (
    <Image
      src={imageUrl}
      alt={image.alt || ''}
      width={width}
      height={height}
      className={className}
      priority={priority}
    />
  )
}

الخطوة 10: بناء صفحات المدونة

صفحة قائمة المدونة

// src/app/blog/page.tsx
import Link from 'next/link'
import { client } from '@/sanity/client'
import { postsQuery } from '@/sanity/queries'
import { SanityImage } from '@/components/sanity-image'
 
export const revalidate = 60
 
export default async function BlogPage() {
  const posts = await client.fetch(postsQuery)
 
  return (
    <main className="mx-auto max-w-6xl px-4 py-16">
      <h1 className="mb-12 text-4xl font-bold">مدونة DevJournal</h1>
 
      <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
        {posts.map((post: any) => (
          <article
            key={post._id}
            className="group overflow-hidden rounded-xl border bg-white shadow-sm transition-shadow hover:shadow-md"
          >
            <Link href={`/blog/${post.slug.current}`}>
              {post.mainImage && (
                <SanityImage
                  image={post.mainImage}
                  width={600}
                  height={340}
                  className="aspect-video w-full object-cover transition-transform group-hover:scale-105"
                />
              )}
              <div className="p-6">
                <h2 className="mb-2 text-xl font-semibold">{post.title}</h2>
                <p className="mb-4 line-clamp-2 text-gray-600">{post.excerpt}</p>
                <div className="flex items-center gap-3 text-sm text-gray-500">
                  <span>{post.author?.name}</span>
                </div>
              </div>
            </Link>
          </article>
        ))}
      </div>
    </main>
  )
}

صفحة المقالة الفردية

// src/app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { client } from '@/sanity/client'
import { postBySlugQuery, postSlugsQuery } from '@/sanity/queries'
import { SanityImage } from '@/components/sanity-image'
import { PortableTextRenderer } from '@/components/portable-text'
import type { Metadata } from 'next'
 
interface PageProps {
  params: Promise<{ slug: string }>
}
 
export async function generateStaticParams() {
  const slugs = await client.fetch(postSlugsQuery)
  return slugs.map((slug: string) => ({ slug }))
}
 
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const { slug } = await params
  const post = await client.fetch(postBySlugQuery, { slug })
  if (!post) return { title: 'المقالة غير موجودة' }
 
  return {
    title: post.seo?.metaTitle || post.title,
    description: post.seo?.metaDescription || post.excerpt,
  }
}
 
export default async function PostPage({ params }: PageProps) {
  const { slug } = await params
  const post = await client.fetch(postBySlugQuery, { slug })
  if (!post) notFound()
 
  return (
    <article className="mx-auto max-w-3xl px-4 py-16">
      <header className="mb-12">
        <h1 className="mb-4 text-4xl font-bold leading-tight lg:text-5xl">
          {post.title}
        </h1>
        {post.excerpt && (
          <p className="mb-6 text-xl text-gray-600">{post.excerpt}</p>
        )}
      </header>
 
      {post.mainImage && (
        <SanityImage
          image={post.mainImage}
          className="mb-12 w-full rounded-xl"
          priority
        />
      )}
 
      <div className="prose prose-lg max-w-none">
        <PortableTextRenderer value={post.body} />
      </div>
    </article>
  )
}

الخطوة 11: عرض Portable Text

يستخدم Sanity Portable Text — صيغة نص غني تمنحك تحكماً كاملاً في العرض. أنشئ عارضاً مخصصاً:

// src/components/portable-text.tsx
import {
  PortableText,
  type PortableTextComponents,
} from '@portabletext/react'
import { SanityImage } from './sanity-image'
 
const components: PortableTextComponents = {
  types: {
    image: ({ value }) => (
      <figure className="my-8">
        <SanityImage image={value} className="w-full rounded-lg" />
        {value.caption && (
          <figcaption className="mt-2 text-center text-sm text-gray-500">
            {value.caption}
          </figcaption>
        )}
      </figure>
    ),
    code: ({ value }) => (
      <pre className="my-6 overflow-x-auto rounded-lg bg-gray-900 p-4">
        <code className={`language-${value.language || 'text'}`}>
          {value.code}
        </code>
      </pre>
    ),
  },
  marks: {
    link: ({ children, value }) => (
      <a
        href={value?.href}
        rel="noreferrer noopener"
        className="text-blue-600 underline hover:text-blue-800"
      >
        {children}
      </a>
    ),
    code: ({ children }) => (
      <code className="rounded bg-gray-100 px-1.5 py-0.5 text-sm">
        {children}
      </code>
    ),
  },
  block: {
    h2: ({ children }) => (
      <h2 className="mb-4 mt-12 text-3xl font-bold">{children}</h2>
    ),
    h3: ({ children }) => (
      <h3 className="mb-3 mt-8 text-2xl font-semibold">{children}</h3>
    ),
    blockquote: ({ children }) => (
      <blockquote className="my-6 border-l-4 border-blue-500 pl-4 italic text-gray-700">
        {children}
      </blockquote>
    ),
  },
}
 
export function PortableTextRenderer({ value }: { value: any }) {
  if (!value) return null
  return <PortableText value={value} components={components} />
}

الخطوة 12: إضافة المعاينة المباشرة

المعاينة المباشرة هي واحدة من أبرز ميزات Sanity. تتيح لمحرري المحتوى رؤية تغييراتهم في الوقت الفعلي قبل النشر.

إنشاء مزود المعاينة

// src/components/preview-provider.tsx
'use client'
 
import { LiveQueryProvider } from 'next-sanity/preview'
import { client } from '@/sanity/client'
 
export default function PreviewProvider({
  children,
  token,
}: {
  children: React.ReactNode
  token: string
}) {
  return (
    <LiveQueryProvider client={client} token={token}>
      {children}
    </LiveQueryProvider>
  )
}

إنشاء مسار المعاينة

// src/app/api/preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const slug = searchParams.get('slug')
  const secret = searchParams.get('secret')
 
  if (secret !== process.env.SANITY_PREVIEW_SECRET) {
    return new Response('رمز غير صالح', { status: 401 })
  }
 
  const draft = await draftMode()
  draft.enable()
  redirect(slug ? `/blog/${slug}` : '/blog')
}

مسار الخروج من المعاينة

// src/app/api/exit-preview/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
 
export async function GET() {
  const draft = await draftMode()
  draft.disable()
  redirect('/blog')
}

الخطوة 13: إعادة التحقق عند الطلب باستخدام Webhooks

بدلاً من إعادة التحقق بمؤقت، قم بإعداد webhook بحيث يقوم Sanity بتشغيل إعادة التحقق عند تغيير المحتوى:

// src/app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
 
export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-sanity-webhook-secret')
 
  if (secret !== process.env.SANITY_WEBHOOK_SECRET) {
    return NextResponse.json({ message: 'غير مصرح' }, { status: 401 })
  }
 
  const body = await request.json()
 
  switch (body._type) {
    case 'post':
      revalidatePath('/blog')
      if (body.slug?.current) {
        revalidatePath(`/blog/${body.slug.current}`)
      }
      break
    case 'author':
    case 'category':
      revalidatePath('/blog')
      break
    default:
      revalidatePath('/')
  }
 
  return NextResponse.json({ revalidated: true, now: Date.now() })
}

لإعداد الـ webhook في Sanity:

  1. اذهب إلى sanity.io/manage واختر مشروعك
  2. انتقل إلى API ثم Webhooks
  3. انقر Create Webhook
  4. اضبط عنوان URL على https://your-domain.com/api/revalidate
  5. أضف رأساً مخصصاً: x-sanity-webhook-secret بقيمة سرك
  6. حدد أنواع المستندات للتشغيل عليها (post, author, category)
  7. اختر أحداث Create و Update و Delete

الخطوة 14: أنواع TypeScript

أضف أنواع TypeScript للحصول على أمان كامل للأنواع:

// src/sanity/types.ts
import type { Image, Slug, PortableTextBlock } from 'sanity'
 
export interface Author {
  _id: string
  name: string
  slug: Slug
  image?: Image
  bio?: PortableTextBlock[]
}
 
export interface Category {
  _id: string
  title: string
  slug: Slug
  description?: string
  postCount?: number
}
 
export interface Post {
  _id: string
  title: string
  slug: Slug
  author: Author
  mainImage?: Image & { alt?: string }
  categories?: Category[]
  publishedAt: string
  excerpt?: string
  body?: PortableTextBlock[]
  seo?: {
    metaTitle?: string
    metaDescription?: string
    ogImage?: Image
  }
}

الخطوة 15: النشر في بيئة الإنتاج

نشر Next.js على Vercel

git add .
git commit -m "feat: complete devjournal with Sanity v3"
git push origin main
 
npx vercel

اضبط متغيرات البيئة في Vercel:

  • NEXT_PUBLIC_SANITY_PROJECT_ID
  • NEXT_PUBLIC_SANITY_DATASET
  • SANITY_API_READ_TOKEN
  • SANITY_PREVIEW_SECRET
  • SANITY_WEBHOOK_SECRET

تكوين CORS في Sanity

اذهب إلى sanity.io/manage وأضف نطاق الإنتاج إلى أصول CORS:

  • https://your-domain.com (مع السماح ببيانات الاعتماد)
  • http://localhost:3000 (للتطوير)

اختبار التطبيق

  1. الوصول إلى Studio: قم بزيارة /studio وتحقق من إمكانية إنشاء المؤلفين والتصنيفات والمقالات
  2. قائمة المدونة: قم بزيارة /blog وتأكد من عرض المقالات مع الصور والبيانات الوصفية
  3. المقالة الفردية: انقر على مقالة وتحقق من عرض Portable Text بشكل صحيح
  4. تحسين الصور: تحقق من تحميل الصور بتنسيق WebP بأحجام متجاوبة
  5. المعاينة المباشرة: فعّل وضع المسودة وتحقق من ظهور التغييرات في الوقت الفعلي
  6. إعادة التحقق: انشر تغييراً في Studio وتحقق من تحديث الموقع

استكشاف الأخطاء وإصلاحها

أخطاء "المشروع غير موجود" أو CORS

تأكد من صحة معرف مشروعك وأنك أضفت localhost:3000 إلى أصول CORS في لوحة تحكم Sanity.

الصور لا تظهر

تحقق من أن مجموعة بيانات Sanity مضبوطة على عام لأصول الصور، أو أنك تمرر الرمز المميز الصحيح لمجموعات البيانات الخاصة.

Studio يعرض صفحة فارغة

تحقق من وحدة التحكم في المتصفح. المشاكل الشائعة تشمل أنواع مخططات مفقودة أو تكوين إضافات غير صحيح.

استعلام GROQ يعيد فارغاً

استخدم أداة Vision في Studio لاختبار الاستعلامات. الأخطاء الشائعة تشمل مرشحات _type المفقودة أو بناء جملة المراجع غير الصحيح.

الخطوات التالية

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

الخلاصة

لقد بنيت موقعاً كاملاً يعتمد على المحتوى باستخدام Sanity v3 و Next.js App Router. يمنحك هذا المزيج Studio CMS قابل للتخصيص بالكامل، و لغة استعلام GROQ القوية، و معاينة مباشرة في الوقت الفعلي، و خط أنابيب صور محسّن — كل ذلك مع الحفاظ على أمان الأنواع الكامل مع TypeScript.

بنية مخزن محتوى Sanity تعني أن محتواك متاح دائماً عبر API، مما يسهل إعادة استخدامه عبر المواقع وتطبيقات الجوال والمنصات الأخرى. نهج Studio المدمج يعني أن محرري المحتوى يحصلون على تجربة تحرير متقنة بدون الحاجة إلى تطبيق منفصل.

النقاط الرئيسية من هذا البرنامج التعليمي:

  • مخططات Sanity تعتمد على الكود ومحددة الأنواع بالكامل
  • استعلامات GROQ تمنحك تحكماً دقيقاً في جلب البيانات
  • Portable Text يوفر محتوى غنياً مع عرض مخصص
  • المعاينة المباشرة وإعادة التحقق عند الطلب تنشئ سير عمل تحرير سلس
  • خط أنابيب الصور يتعامل مع التحسين تلقائياً

هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على تنسيق TEIF والمواصفات التقنية للفاتورة الإلكترونية في تونس.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

بناء موقع محتوى متكامل باستخدام Payload CMS 3 و Next.js

تعلّم كيفية بناء موقع محتوى كامل الميزات باستخدام Payload CMS 3 الذي يعمل مباشرة داخل Next.js App Router. يغطي هذا الدرس المجموعات، محرر النصوص الغنية، رفع الوسائط، المصادقة، والنشر في بيئة الإنتاج.

30 د قراءة·

بناء تطبيق Full-Stack باستخدام Strapi 5 و Next.js 15 App Router

تعلّم كيفية بناء تطبيق متكامل باستخدام Strapi 5 كنظام إدارة محتوى headless و Next.js 15 App Router للواجهة الأمامية. يغطي هذا الدليل نمذجة المحتوى، واجهات REST API، والعرض من جانب الخادم، والنشر في بيئة الإنتاج.

30 د قراءة·