בלוקים והערות מותאמים אישית עם Portable Text ב-Sanity

עורך Sanity הסטנדרטי מתקשה לעיתים קרובות עם תוכן מורכב: אתם זקוקים ל-custom blocks, annotations וקישורים פנימיים, בעוד שמנהלי תוכן נתקלים במגבלות. אנחנו מגדירים את Portable Text לצרכים שלכם, ומספקים פרויקט מפתח — מסכמת הנתונים ועד לרינדור ב-React. הצוות שלנו מבטיח גמישות ואמינות, כך שתוכלו להרחיב תוכן ללא עקיפות ועם תמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
בלוקים והערות מותאמים אישית עם Portable Text ב-Sanity
בינוני
~2-3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1049
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

הגדרת 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) תמונות עם כיתוב נמוכה (בלוק סטנדרטי)
ווידג'ט מותאם אישית הטמעת תוכן צד שלישי גבוהה (שדה + רכיב)

תהליך

  1. ניתוח — קבע אילו בלוקים העורכים צריכים (קריאות תיבה, טבלאות, קוד, הטמעות).
  2. עיצוב — צור סכמת blockContent ורכיבים מותאמים אישית.
  3. יישום — הגדר הערות ובלוקים, כתוב את ה-renderer.
  4. בדיקות — ודא עיבוד של כל סוגי התוכן, תקינות הקישורים.
  5. פריסה — דחוף שינויים והדריך עורכים.

לוחות זמנים משוערים

הגדרת סכמה ו-renderer בסיסיים אורכת 1 עד 2 ימים. אם נדרשים יותר בלוקים מותאמים אישית או אינטגרציה עם APIs אחרים, לוח הזמנים גדל. עלויות טיפוסיות נעות בין $500–$1500 בהתאם למורכבות.

מה כלול

  • סכמת Portable Text מוגדרת עם בלוקים והערות מותאמים אישית
  • רכיב renderer עבור React/Next.js עם כיסוי טיפוסים מלא
  • תיעוד למנהלי תוכן
  • אחריות תמיכה למשך שבועיים לאחר המסירה

הניסיון שלנו: למעלה מ-5 שנות עבודה עם Sanity ו-50+ פרויקטים על הפלטפורמה. אנו מכירים את כל המלכודות, משאילתות N+1 ועד hydration בצד הלקוח.

קבל ייעוץ ממהנדס שהגדיר Portable Text לעשרות צוותי עריכה. צור קשר כדי לדון בפרויקט שלך — נעריך את ההיקף ונציע את הפתרון האופטימלי. הזמן הגדרת Portable Text ממומחים ושחרר את העורכים שלך ממגבלות.