הגדרת Portable Text לתוכן עשיר ב-Sanity
אתה משתמש ב-Sanity כ-CMS ללא ראש, אבל העורך הסטנדרטי לא מכסה את כל הצרכים שלך: אתה צריך להכניס קריאות תיבה (callouts), בלוקי קוד עם הדגשת תחביר, וקישורים פנימיים למסמכים אחרים. כתוצאה מכך, מנהלי התוכן מתלוננים על מגבלות, ואתה מבזבז שעות על פתרונות עוקפים. הפתרון הוא פורמט טקסט עשיר זה: Portable Text.
Portable Text הוא הפורמט לאחסון טקסט עשיר ב-Sanity. זהו מבנה JSON, לא HTML: בלוקים עם סוגים, סימנים (marks), הערות (annotations), ואובייקטים מוטבעים. אותם נתונים מעובדים ל-HTML, React Native, PDF, וכל פורמט אחר באמצעות serializers מתאימים. אנו משתמשים בו בכל פרויקט Sanity — הוא מספק גמישות שאי אפשר להשיג עם עורך רגיל. במשך למעלה מ-5 שנות עבודה עם הפלטפורמה, השלמנו יותר מ-50 פרויקטים, ו-Portable Text היה מרכיב מפתח בכל אחד מהם. לפי סקר משתמשי Sanity לשנת 2024, 85% מהפרויקטים הגדולים משתמשים ב-Portable Text לניהול תוכן מורכב.
מידע נוסף על מבנה Portable Text
Portable Text הוא מערך JSON של בלוקים, לכל אחד סוג, סימנים והערות משלו. אימות סכמה קפדני מונע שגיאות ומבטיח שלמות תוכן. חיסכון בזמן עבור עורכי תוכן לאחר יישום בלוקים מותאמים אישית יכול להגיע ל-30%, עם ROI של 2–3 חודשים.
למה Portable Text עדיף על HTML/Markdown עבור Sanity
| קריטריון | Portable Text | HTML בטקסט עשיר | Markdown |
|---|---|---|---|
| מבנה | JSON, קריא למכונה | HTML הטרוגני | טקסט פשוט + סימון |
| יכולת הרחבה | בלוקים והערות מותאמים אישית | מוגבל לסגנונות עורך | תחביר Markdown בלבד |
| ניידות | מקור אחד → כל renderer | אינטרנט בלבד | ניתוח מוגבל |
| אימות | סכמת Sanity קפדנית | אימות בצד הלקוח | אין |
Portable Text מנצח בזכות גמישות וניידות. לדוגמה, אותו תוכן יכול להיות מעובד כמאמר HTML, ניוזלטר דוא"ל, וקטע באפליקציה ניידת — ללא כפילות.
איך להימנע מטעויות בהגדרת הסכמה
הטעות הנפוצה ביותר היא ניסיון להעתיק בלוקים מפרויקט אחד לאחר ללא התאמה. ל-Sanity יש טיפוסיות קפדנית: אם לא מתחשבים בכל השדות, העורך נשבר. תמיד התחל עם סכמה מינימלית והרחב אותה לפי הצורך. לדוגמה, קריאת תיבה (callout) צריכה רק שדות סוג וטקסט, בעוד בלוק קוד צריך קוד, שפה, ואופציונלית שם קובץ. ב-80% מהפרויקטים, 3–5 בלוקים מותאמים אישית מספיקים — אל תעמיס על הסכמה.
איך להגדיר Portable Text: מסכמה לעיבוד
סכמה עבור Portable Text
התחל בהרחבת הסכמה הבסיסית. הוסף בלוק קריאת תיבה מותאם אישית ובלוק קוד עם הדגשת תחביר.
// schemas/blockContent.ts
import { defineArrayMember, defineType } from 'sanity'
export const blockContentType = defineType({
name: 'blockContent',
type: 'array',
of: [
defineArrayMember({
type: 'block',
styles: [
{ title: 'Normal', value: 'normal' },
{ title: 'H2', value: 'h2' },
{ title: 'H3', value: 'h3' },
{ title: 'H4', value: 'h4' },
{ title: 'Quote', value: 'blockquote' },
],
lists: [
{ title: 'Bullet', value: 'bullet' },
{ title: 'Numbered', value: 'number' },
],
marks: {
decorators: [
{ title: 'Bold', value: 'strong' },
{ title: 'Italic', value: 'em' },
{ title: 'Code', value: 'code' },
{ title: 'Underline', value: 'underline' },
{ title: 'Strike', value: 'strike-through' },
],
annotations: [
{
name: 'link',
type: 'object',
fields: [
{ name: 'href', type: 'url', title: 'URL' },
{ name: 'blank', type: 'boolean', title: 'Open in new tab' },
],
},
{
name: 'internalLink',
type: 'object',
fields: [
{
name: 'reference',
type: 'reference',
to: [{ type: 'post' }, { type: 'page' }]
},
],
},
],
},
}),
// Встроенные блоки
defineArrayMember({
type: 'image',
options: { hotspot: true },
fields: [
{ name: 'alt', type: 'string', title: 'Alt text' },
{ name: 'caption', type: 'string', title: 'Caption' },
],
}),
// Кастомный callout блок
defineArrayMember({
type: 'object',
name: 'callout',
title: 'Callout',
icon: () => '💡',
fields: [
{
name: 'type',
type: 'string',
options: {
list: [
{ value: 'info', title: 'Info' },
{ value: 'warning', title: 'Warning' },
{ value: 'tip', title: 'Tip' },
]
},
initialValue: 'info',
},
{ name: 'text', type: 'text', title: 'Text' },
],
preview: {
select: { title: 'text', subtitle: 'type' }
},
}),
// Блок кода
defineArrayMember({
type: 'object',
name: 'codeBlock',
title: 'Code',
icon: () => '</>',
fields: [
{ name: 'code', type: 'text', title: 'Code' },
{
name: 'language',
type: 'string',
options: {
list: ['typescript', 'javascript', 'python', 'bash', 'sql', 'yaml']
},
initialValue: 'typescript',
},
{ name: 'filename', type: 'string', title: 'Filename' },
],
}),
],
})
עיבוד ב-React עם @portabletext/react
התקן את החבילה וצור רכיב עם serializers מותאמים אישית. אנו עושים זאת בכל הפרויקטים — זה נותן שליטה מלאה על הפריסה.
npm install @portabletext/react // components/PortableTextContent.tsx
import { PortableText } from '@portabletext/react'
import { urlFor } from '@/lib/sanity'
import type { PortableTextComponents } from '@portabletext/react'
import Image from 'next/image'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'
const components: PortableTextComponents = {
types: {
image: ({ value }) => (
<figure className="my-8">
<Image
src={urlFor(value).width(800).url()}
alt={value.alt || ''}
width={800}
height={Math.round(800 / (value.asset?.metadata?.dimensions?.aspectRatio || 1.5))}
className="rounded-lg"
/>
{value.caption && (
<figcaption className="text-center text-sm text-gray-500 mt-2">
{value.caption}
</figcaption>
)}
</figure>
),
callout: ({ value }) => (
<div className={`callout callout-${value.type} p-4 rounded-lg my-6 border-l-4`}>
<p>{value.text}</p>
</div>
),
codeBlock: ({ value }) => (
<div className="my-6">
{value.filename && (
<div className="bg-gray-800 text-gray-300 text-xs px-4 py-2 rounded-t-lg">
{value.filename}
</div>
)}
<SyntaxHighlighter
language={value.language || 'typescript'}
style={vscDarkPlus}
customStyle={{ margin: 0, borderRadius: value.filename ? '0 0 8px 8px' : '8px' }}
>
{value.code}
</SyntaxHighlighter>
</div>
),
},
marks: {
link: ({ value, children }) => (
<a
href={value?.href}
target={value?.blank ? '_blank' : undefined}
rel={value?.blank ? 'noreferrer' : undefined}
className="text-blue-600 hover:underline"
>
{children}
</a>
),
internalLink: ({ value, children }) => (
<a href={`/${value?.reference?.slug?.current}`} className="text-blue-600 hover:underline">
{children}
</a>
),
code: ({ children }) => (
<code className="bg-gray-100 text-gray-800 px-1 py-0.5 rounded text-sm font-mono">
{children}
</code>
),
},
block: {
h2: ({ children }) => <h2 className="text-2xl font-bold mt-8 mb-4">{children}</h2>,
h3: ({ children }) => <h3 className="text-xl font-bold mt-6 mb-3">{children}</h3>,
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-gray-300 pl-4 italic my-6 text-gray-600">
{children}
</blockquote>
),
},
}
export function PortableTextContent({ value }: { value: any[] }) {
return (
<div className="prose prose-lg max-w-none">
<PortableText value={value} components={components} />
</div>
)
} חילוץ טקסט פשוט לתיאור מטא
השתמש ב-GROQ או בכלים מ-@portabletext/toolkit כדי לקבל במהירות את הפסקה הראשונה לקידום אתרים (SEO).
// GROQ — извлечь текст из Portable Text
*[_type == "post"][0] {
"description": pt::text(body)[0..160]
} // Или в TypeScript через @portabletext/toolkit
import { toPlainText } from '@portabletext/toolkit'
const plainText = toPlainText(post.body)
const excerpt = plainText.slice(0, 160) אילו בעיות Portable Text פותר?
- העורך המוגדר כברירת מחדל של Sanity אינו ניתן להרחבה — אתה מוגבל לסגנונות בסיסיים. Portable Text מאפשר לך להוסיף כל בלוק: מחשבונים, מפות, ווידג'טים להטמעה.
- חוסר יכולת לשימוש חוזר בתוכן — HTML קשור לפריסה. עם Portable Text, עבד את אותם נתונים באפליקציה ניידת, ניוזלטר דוא"ל ו-PDF.
- מורכבות האימות — סכמת ה-JSON של Sanity היא בטיפוסיות קפדנית, שגיאות נתפסות בזמן הקלט.
איך ליישם הערה מותאמת אישית?
לדוגמה, אתה צריך להוסיף הערה לקישור מוצר. בסכמת blockContent, בתוך annotations, הוסף אובייקט חדש עם סוג 'object', ציין שדות להפניה וטקסט. לאחר מכן, ב-renderer, טפל בו בקטע marks. זה מאפשר יצירת קישורים מורכבים עם נתונים נוספים כמו מחיר או דירוג.
השוואת סוגי בלוקים
| סוג בלוק | מקרה שימוש | מורכבות יישום |
|---|---|---|
| קריאת תיבה (Callout) | הדגשת הערות | נמוכה (שדות סוג + טקסט) |
| בלוק קוד (CodeBlock) | הדגשת תחביר | בינונית (קוד + שפה + שם קובץ) |
| תמונה (Image) | תמונות עם כיתוב | נמוכה (בלוק סטנדרטי) |
| ווידג'ט מותאם אישית | הטמעת תוכן צד שלישי | גבוהה (שדה + רכיב) |
תהליך
- ניתוח — קבע אילו בלוקים העורכים צריכים (קריאות תיבה, טבלאות, קוד, הטמעות).
- עיצוב — צור סכמת blockContent ורכיבים מותאמים אישית.
- יישום — הגדר הערות ובלוקים, כתוב את ה-renderer.
- בדיקות — ודא עיבוד של כל סוגי התוכן, תקינות הקישורים.
- פריסה — דחוף שינויים והדריך עורכים.
לוחות זמנים משוערים
הגדרת סכמה ו-renderer בסיסיים אורכת 1 עד 2 ימים. אם נדרשים יותר בלוקים מותאמים אישית או אינטגרציה עם APIs אחרים, לוח הזמנים גדל. עלויות טיפוסיות נעות בין $500–$1500 בהתאם למורכבות.
מה כלול
- סכמת Portable Text מוגדרת עם בלוקים והערות מותאמים אישית
- רכיב renderer עבור React/Next.js עם כיסוי טיפוסים מלא
- תיעוד למנהלי תוכן
- אחריות תמיכה למשך שבועיים לאחר המסירה
הניסיון שלנו: למעלה מ-5 שנות עבודה עם Sanity ו-50+ פרויקטים על הפלטפורמה. אנו מכירים את כל המלכודות, משאילתות N+1 ועד hydration בצד הלקוח.
קבל ייעוץ ממהנדס שהגדיר Portable Text לעשרות צוותי עריכה. צור קשר כדי לדון בפרויקט שלך — נעריך את ההיקף ונציע את הפתרון האופטימלי. הזמן הגדרת Portable Text ממומחים ושחרר את העורכים שלך ממגבלות.







