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

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 v3 | Strapi 5 | Payload CMS 3 | Contentful |
|---|---|---|---|---|
| البنية | مخزن محتوى سحابي | خادم مستضاف ذاتياً | داخل Next.js | SaaS سحابي |
| لغة الاستعلام | GROQ (مخصصة) | REST / GraphQL | REST / GraphQL | REST / GraphQL |
| Studio | تطبيق React قابل للتخصيص | لوحة إدارة | إدارة Next.js | لوحة سحابية |
| الوقت الفعلي | مدمج | يتطلب إعداد | يتطلب إعداد | Webhooks فقط |
| خط أنابيب الصور | CDN مدمج + تحويلات | يتطلب إضافة | محول رفع | CDN مدمج |
| التسعير | مستوى مجاني سخي | مجاني (مستضاف ذاتياً) | مجاني (مستضاف ذاتياً) | مستوى مجاني محدود |
| Portable Text | صيغة نص غني أصلية | Blocks / Markdown | محرر Lexical | Rich 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:
- اذهب إلى sanity.io/manage واختر مشروعك
- انتقل إلى API ثم Webhooks
- انقر Create Webhook
- اضبط عنوان URL على
https://your-domain.com/api/revalidate - أضف رأساً مخصصاً:
x-sanity-webhook-secretبقيمة سرك - حدد أنواع المستندات للتشغيل عليها (post, author, category)
- اختر أحداث 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_IDNEXT_PUBLIC_SANITY_DATASETSANITY_API_READ_TOKENSANITY_PREVIEW_SECRETSANITY_WEBHOOK_SECRET
تكوين CORS في Sanity
اذهب إلى sanity.io/manage وأضف نطاق الإنتاج إلى أصول CORS:
https://your-domain.com(مع السماح ببيانات الاعتماد)http://localhost:3000(للتطوير)
اختبار التطبيق
- الوصول إلى Studio: قم بزيارة
/studioوتحقق من إمكانية إنشاء المؤلفين والتصنيفات والمقالات - قائمة المدونة: قم بزيارة
/blogوتأكد من عرض المقالات مع الصور والبيانات الوصفية - المقالة الفردية: انقر على مقالة وتحقق من عرض Portable Text بشكل صحيح
- تحسين الصور: تحقق من تحميل الصور بتنسيق WebP بأحجام متجاوبة
- المعاينة المباشرة: فعّل وضع المسودة وتحقق من ظهور التغييرات في الوقت الفعلي
- إعادة التحقق: انشر تغييراً في 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 يوفر محتوى غنياً مع عرض مخصص
- المعاينة المباشرة وإعادة التحقق عند الطلب تنشئ سير عمل تحرير سلس
- خط أنابيب الصور يتعامل مع التحسين تلقائياً
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

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

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

Medusa.js 2.0 — بناء متجر إلكتروني Headless مع Next.js (2026)
تعلم كيفية بناء متجر إلكتروني كامل باستخدام Medusa.js 2.0 و Next.js. من كتالوج المنتجات إلى الدفع، هذا الدليل يغطي كل شيء بـ TypeScript.