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

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка Portable Text для Rich Content Sanity
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы

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

Этапы разработки

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

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

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

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

Схема 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' },
      ],
    }),
  ],
})

Рендеринг через @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 — извлечь текст из 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 схемы с кастомными блоками и рендерером — 1–2 дня.