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

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

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

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

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

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

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

Часто задаваемые вопросы

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

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1414
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1285
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    982
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1241
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    982
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    994

Настройка 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 у экспертов и избавьте редакторов от ограничений.