Skip to content

Block Content Component

The BlockContent component renders Sanity’s portable text (block content) into HTML. Supports common block types including paragraphs, headings, lists, and text formatting.

  • ✅ Paragraphs and headings (H1-H6)
  • ✅ Text marks (bold, italic, code, underline)
  • ✅ Blockquotes
  • ✅ Styled with Tailwind classes
  • ✅ XSS-safe rendering
  • ✅ Prose typography support
  • ✅ Custom CSS classes
  • ✅ Lightweight (~1 KB)
---
import BlockContent from '@rejected-media/podcast-framework-core/components/BlockContent.astro';
const episode = await getEpisode(Astro.params.slug);
---
<BlockContent blocks={episode.showNotes} />

Type: any[] (Sanity portable text blocks) Default: undefined

Array of Sanity block content. Component returns null if not provided.

<BlockContent blocks={episode.showNotes} />

Type: string Default: undefined

Custom CSS classes to apply to the container.

<BlockContent
blocks={episode.showNotes}
class="prose-lg max-w-4xl"
/>
---
import { getEpisode } from '@rejected-media/podcast-framework-core';
import BlockContent from '@rejected-media/podcast-framework-core/components/BlockContent.astro';
import BaseLayout from '@rejected-media/podcast-framework-core/layouts/BaseLayout.astro';
const episode = await getEpisode(Astro.params.slug);
---
<BaseLayout title={episode.title}>
<article class="max-w-4xl mx-auto px-4 py-12">
<h1 class="text-4xl font-bold mb-4">{episode.title}</h1>
<p class="text-lg text-gray-600 mb-8">
{episode.description}
</p>
<!-- Spotify player -->
{episode.spotifyLink && (
<iframe src={getSpotifyEmbedUrl(episode.spotifyLink)} />
)}
<!-- Show Notes -->
{episode.showNotes && (
<div class="mt-12">
<h2 class="text-2xl font-bold mb-6">Show Notes</h2>
<BlockContent
blocks={episode.showNotes}
class="prose prose-lg max-w-none"
/>
</div>
)}
<!-- About the Episode -->
{episode.about && (
<div class="mt-12">
<h2 class="text-2xl font-bold mb-6">About</h2>
<BlockContent
blocks={episode.about}
class="prose max-w-none"
/>
</div>
)}
</article>
</BaseLayout>

Sanity Input:

{
_type: 'block',
style: 'normal',
children: [
{ text: 'This is a paragraph.' }
]
}

Rendered HTML:

<p class="text-lg text-gray-700 leading-relaxed mb-6">
This is a paragraph.
</p>

Sanity Input:

{
_type: 'block',
style: 'h1',
children: [
{ text: 'Heading Text' }
]
}

Rendered HTML:

<!-- H1 -->
<h1 class="text-3xl font-bold mb-4">Heading Text</h1>
<!-- H2 -->
<h2 class="text-2xl font-bold mb-3">Heading Text</h2>
<!-- H3 -->
<h3 class="text-xl font-semibold mb-2">Heading Text</h3>
<!-- H4 -->
<h4 class="text-lg font-semibold mb-2">Heading Text</h4>
<!-- H5 -->
<h5 class="font-semibold mb-2">Heading Text</h5>
<!-- H6 -->
<h6 class="font-semibold mb-2 text-sm">Heading Text</h6>

Bold:

{
text: 'bold text',
marks: ['strong']
}

<strong>bold text</strong>

Italic:

{
text: 'italic text',
marks: ['em']
}

<em>italic text</em>

Code:

{
text: 'code text',
marks: ['code']
}

<code class="bg-gray-100 px-1 py-0.5 rounded">code text</code>

Underline:

{
text: 'underlined text',
marks: ['underline']
}

<u>underlined text</u>

Sanity Input:

{
_type: 'block',
style: 'blockquote',
children: [
{ text: 'This is a quote.' }
]
}

Rendered HTML:

<blockquote class="border-l-4 border-gray-300 pl-4 italic my-4">
This is a quote.
</blockquote>

The component applies these Tailwind classes:

/* Container */
.prose .max-w-none
/* Paragraph */
.text-lg .text-gray-700 .leading-relaxed .mb-6
/* Headings */
.text-3xl .font-bold .mb-4 /* H1 */
.text-2xl .font-bold .mb-3 /* H2 */
.text-xl .font-semibold .mb-2 /* H3 */
/* Code */
.bg-gray-100 .px-1 .py-0.5 .rounded
/* Blockquote */
.border-l-4 .border-gray-300 .pl-4 .italic .my-4

Override with Tailwind prose classes:

<BlockContent
blocks={showNotes}
class="prose prose-lg prose-blue max-w-none"
/>

Prose Variants:

  • prose-sm - Smaller text
  • prose-lg - Larger text
  • prose-xl - Extra large text
  • prose-blue - Blue links
  • prose-slate - Slate color scheme
<BlockContent blocks={showNotes} />
<style is:global>
.prose p {
font-size: 1.125rem;
line-height: 1.75;
color: #374151;
}
.prose h2 {
color: #1e40af;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 0.5rem;
}
.prose blockquote {
border-left-color: #3b82f6;
background-color: #eff6ff;
padding: 1rem;
}
</style>

Configure rich text in your Sanity schema:

schemas/episode.ts
export default {
name: 'episode',
type: 'document',
fields: [
// ... other fields
{
name: 'showNotes',
title: 'Show Notes',
type: 'array',
of: [
{
type: 'block',
styles: [
{ title: 'Normal', value: 'normal' },
{ title: 'H1', value: 'h1' },
{ title: 'H2', value: 'h2' },
{ title: 'H3', value: 'h3' },
{ title: 'Quote', value: 'blockquote' }
],
marks: {
decorators: [
{ title: 'Strong', value: 'strong' },
{ title: 'Emphasis', value: 'em' },
{ title: 'Code', value: 'code' },
{ title: 'Underline', value: 'underline' }
]
}
}
]
}
]
};

List rendering is not yet implemented:

{
_type: 'list', // Not supported yet
items: [...]
}

Workaround: Use paragraphs with dashes:

- Item 1
- Item 2
- Item 3

Link marks are not yet implemented:

{
text: 'link text',
marks: ['link'] // Not supported yet
}

Workaround: Include full URLs in text:

Visit https://example.com for more info

Image blocks are not yet implemented:

{
_type: 'image', // Not supported yet
asset: {...}
}

Workaround: Use separate image fields in schema.

src/components/BlockContent.astro
---
// Copy framework component and extend
function renderBlock(block: any): string {
if (block._type === 'block') {
const text = block.children?.map((child: any) => {
let content = child.text || '';
// Existing marks...
if (child.marks?.includes('strong')) {
content = `<strong>${content}</strong>`;
}
// Add link support
if (child.marks?.includes('link')) {
const mark = block.markDefs?.find((m: any) => m._key === child.marks.find((m: string) => m.startsWith('link')));
if (mark?.href) {
content = `<a href="${mark.href}" class="text-blue-600 hover:underline">${content}</a>`;
}
}
return content;
}).join('') || '';
// ... rest of rendering
}
}
---
---
function renderBlock(block: any): string {
// ... existing code
if (block._type === 'list') {
const tag = block.style === 'number' ? 'ol' : 'ul';
const items = block.items?.map((item: any) => {
return `<li>${item.text}</li>`;
}).join('');
return `<${tag} class="list-disc ml-6 mb-4">${items}</${tag}>`;
}
return '';
}
---

The component renders semantic HTML:

<h1>Heading</h1>
<p>Paragraph</p>
<blockquote>Quote</blockquote>
<strong>Bold</strong>
<em>Italic</em>
  • Headings provide document structure
  • Blockquotes announced as quotations
  • Code announced as code blocks
  • Bundle Size: ~1 KB
  • JavaScript: None (renders at build time)
  • Render: Static HTML
  • XSS Protection: DOM-based rendering (safe)

Check that blocks array exists:

{episode.showNotes ? (
<BlockContent blocks={episode.showNotes} />
) : (
<p>No show notes available</p>
)}

Ensure Tailwind prose plugin is installed:

Terminal window
npm install @tailwindcss/typography
tailwind.config.mjs
export default {
plugins: [
require('@tailwindcss/typography'),
],
};

Verify marks are configured in Sanity schema:

marks: {
decorators: [
{ title: 'Strong', value: 'strong' },
{ title: 'Emphasis', value: 'em' },
{ title: 'Code', value: 'code' }
]
}

Add prose classes:

<!-- ❌ Missing prose classes -->
<BlockContent blocks={content} />
<!-- ✅ With prose classes -->
<BlockContent blocks={content} class="prose max-w-none" />
<BlockContent blocks={showNotes} class="prose prose-invert" />
<BlockContent blocks={showNotes} class="prose prose-blue" />
<style is:global>
.prose-blue h2 {
color: #3b82f6;
}
.prose-blue a {
color: #2563eb;
}
.prose-blue blockquote {
border-left-color: #60a5fa;
color: #1e40af;
}
</style>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<BlockContent
blocks={episode.showNotes}
class="prose max-w-none"
/>
<BlockContent
blocks={episode.transcript}
class="prose max-w-none"
/>
</div>