Images and Portable Text in Astro
Render Sanity images with CDN transforms and rich text with Portable Text in your Astro components.
Rendering images
Sanity's asset pipeline serves images from a global CDN with on-demand transforms, automatic format optimization, and metadata like dimensions, color palettes, and blur hashes. Use @sanity/image-url to generate URLs with the transforms you need.
Create an image URL builder
import { createImageUrlBuilder } from '@sanity/image-url'
import { sanityClient } from 'sanity:client'
const builder = createImageUrlBuilder(sanityClient)
export function urlFor(source: any) {
return builder.image(source)
}Use it in components
---
import { urlFor } from '../lib/image'
interface Props {
image: any
alt: string
width?: number
}
const { image, alt, width = 800 } = Astro.props
const url = urlFor(image).width(width).auto('format').url()
---
<img src={url} alt={alt} width={width} loading="lazy" />The auto('format') option serves modern formats like AVIF or WebP based on browser support, and falls back to the original format for others.
Rendering Portable Text
Sanity stores rich text as Portable Text, a JSON-based format. The community-maintained astro-portabletext library handles rendering in Astro.
Basic rendering
---
import { PortableText } from 'astro-portabletext'
interface Props {
value: any[]
}
const { value } = Astro.props
---
<PortableText value={value} />Custom block types and annotations
If your schema includes custom block types (like code blocks, images, or YouTube embeds), map them to Astro components:
---
import { PortableText as PortableTextInternal } from 'astro-portabletext'
import CodeBlock from './CodeBlock.astro'
import SanityImage from './SanityImage.astro'
import YouTubeEmbed from './YouTubeEmbed.astro'
import InternalLink from './InternalLink.astro'
interface Props {
value: any[]
}
const { value } = Astro.props
const components = {
type: {
code: CodeBlock,
image: SanityImage,
youtube: YouTubeEmbed,
},
mark: {
internalLink: InternalLink,
},
}
---
<PortableTextInternal value={value} components={components} />Each component receives the block data as props. For the full API, see the astro-portabletext documentation.
Using Astro's Image component
Astro's built-in <Image> component is designed for local assets and third-party image URLs that are not part of the Sanity pipeline. For images stored in Sanity, use @sanity/image-url instead: it handles format negotiation (AVIF/WebP), resizing, and cropping at the CDN level, with no build-time processing required. If you do use <Image> with Sanity CDN URLs, add the Sanity CDN to your allowed image domains:
// astro.config.mjs
export default defineConfig({
image: {
domains: ['cdn.sanity.io'],
},
// ...
})Note that passing a Sanity CDN URL to Astro's <Image> component results in double-processing: Sanity's CDN applies its transforms first, then Astro's Sharp pipeline processes the result again at build time. This wastes build time and can degrade image quality.
Example: code block component
---
interface Props {
node: {
code: string
language?: string
filename?: string
}
}
const { node } = Astro.props
---
{node.filename && <div class="code-filename">{node.filename}</div>}
<pre><code class={`language-${node.language || 'text'}`}>{node.code}</code></pre>Example: internal link annotation
---
interface Props {
node: {
slug?: { current: string }
_type?: string
}
}
const { node } = Astro.props
const href = node.slug ? `/${node.slug.current}` : '#'
---
<a href={href}><slot /></a>Querying images and rich text together
A typical query for a content page includes both image references and Portable Text body content:
*[_type == "post" && slug.current == $slug][0]{
title,
"mainImage": mainImage{
asset->,
alt,
caption
},
body[]{
...,
_type == "image" => {
asset->,
alt,
caption
}
},
"author": author->{
name,
"imageUrl": image.asset->url
}
}The asset-> dereference on images gives you the full asset document, including dimensions and metadata. The body[]{ ... } projection with the conditional image expansion ensures inline images in Portable Text also include their asset data.