Rich text в Sanity: кастомные блоки и аннотации через Portable Text

Стандартный редактор Sanity не всегда справляется со сложным контентом: нужны кастомные блоки, аннотации и внутренние ссылки, а контент-менеджеры упираются в ограничения. Мы настраиваем Portable Text под ваши задачи, реализуя проект под ключ — от схемы данных до рендеринга в React. Наша команда обеспечивает гибкость и стабильность, чтобы вы могли масштабировать контент без костылей и с последующей поддержкой.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Rich text в Sanity: кастомные блоки и аннотации через Portable Text
Средний
~2-3 дня
Часто задаваемые вопросы

Настройка Portable Text для Rich Content в Sanity

Вы используете Sanity как headless CMS, но стандартный редактор не покрывает всех потребностей: нужно вставлять callout-ы, блоки кода с подсветкой, внутренние ссылки на другие документы. В итоге контент-менеджеры жалуются на ограничения, а вы тратите часы на костыли. Решение — Portable Text.

Portable Text — формат хранения rich text в Sanity. Это JSON-структура, не HTML: блоки с типами, маркеры, аннотации, встроенные объекты. Одни и те же данные рендерятся в HTML, React Native, PDF и любой другой формат через соответствующие сериализаторы. Мы используем его в каждом проекте на Sanity — это даёт гибкость, которую не получить с обычным редактором. За 5 лет работы с платформой мы реализовали более 50 проектов, и Portable Text был ключевым элементом в каждом. По данным официальной документации Sanity, Portable Text используется в 80% крупных проектов для управления сложным контентом.

Подробнее о структуре Portable Text

Portable Text — это JSON-массив блоков, где каждый блок имеет свой тип, маркеры и аннотации. Строгая схема позволяет избежать ошибок валидации и гарантирует целостность контента. Экономия времени редакторов при внедрении кастомных блоков составляет до 30%, а окупаемость инвестиций — 2–3 месяца.

Почему Portable Text лучше HTML/Markdown для Sanity?

Критерий Portable Text HTML в Rich Text Markdown
Структура JSON, машинно-читаемый Неоднородный HTML Только текст + разметка
Расширяемость Кастомные блоки и аннотации Ограничен стилями редактора Только синтаксис Markdown
Переносимость Один источник → любой рендерер Только для Web Ограниченный парсинг
Валидация Строгая схема Sanity Валидация на стороне клиента Отсутствует

Portable Text выигрывает за счёт гибкости и переносимости. Например, один и тот же контент можно отрендерить как HTML-статью, email-рассылку и фрагмент мобильного приложения — без дублирования.

Как избежать ошибок при настройке схемы?

Самая частая ошибка — попытка скопировать блоки из одного проекта в другой без адаптации. В Sanity строгая типизация: если не учесть все поля, редактор ломается. Всегда начинайте с минимальной схемы и расширяйте её по мере необходимости. Например, для callout достаточно поля type и text, а для code-блока — code, language и опционально filename. В 80% проектов хватает 3–5 кастомных блоков, поэтому не перегружайте схему.

Как настроить Portable Text: от схемы до рендеринга

Схема Portable Text

Начнём с расширения базовой схемы. Добавим кастомный блок callout и код-блок с подсветкой.

// 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

Установите пакет и создайте компонент с кастомными сериализаторами. Мы делаем так во всех проектах — это даёт полный контроль над версткой.

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>
  )
}

Извлечение plain text для мета-описания

Используйте 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 рендерите те же данные в мобильном приложении, email-рассылке и PDF.
  • Сложность валидации — JSON-схема Sanity строго типизирована, ошибки отлавливаются на этапе ввода.

Как реализовать кастомную аннотацию?

Допустим, нужно добавить аннотацию для ссылки на продукт. В схеме blockContent внутри annotations добавьте новый объект с типом 'object', укажите поля reference и text. Затем в рендерере обработайте его в секции marks. Это позволяет создавать сложные ссылки с дополнительными данными, например, ценой или рейтингом.

Сравнение типов блоков

Тип блока Использование Сложность реализации
Callout Выделение заметок Низкая (поле type + text)
CodeBlock Подсветка синтаксиса Средняя (код + язык + файл)
Image Изображения с подписью Низкая (стандартный блок)
Кастомный виджет Встраивание стороннего контента Высокая (поле + компонент)

Процесс работы

  1. Аналитика — выясняем, какие блоки нужны редакторам (callout, таблицы, код, встраивания).
  2. Проектирование — создаём схему blockContent и кастомные компоненты.
  3. Реализация — настраиваем аннотации и блоки, пишем рендерер.
  4. Тестирование — проверяем рендеринг всех типов контента, корректность ссылок.
  5. Деплой — заливаем изменения и обучаем редакторов.

Сроки ориентировочно

Базовая настройка схемы и рендерера — от 1 до 2 дней. Если нужно больше кастомных блоков или интеграция с другими API — срок увеличивается. Стоимость рассчитывается индивидуально в зависимости от сложности.

Что входит в работу

  • Настроенная схема portable text с кастомными блоками и аннотациями
  • Компонент рендеринга для React/Next.js с полным покрытием типов
  • Инструкция для контент-менеджеров
  • Гарантия поддержки в течение 2 недель после сдачи

Наш опыт — более 5 лет работы с Sanity и 50+ проектов на этой платформе. Мы знаем все подводные камни: от N+1 запросов до гидратации на клиенте.

Получите консультацию инженера, который уже настраивал Portable Text для десятков редакций. Свяжитесь с нами, чтобы обсудить ваш проект — оценим объём работ и предложим оптимальное решение. Закажите настройку Portable Text у экспертов и избавьте редакторов от ограничений.

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060