# Images and Portable Text in Astro

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

**src/lib/image.ts**

```typescript
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

**src/components/SanityImage.astro**

```typescript
---
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

**src/components/PortableText.astro**

```typescript
---
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:

**src/components/PortableText.astro**

```typescript
---
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**

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

**src/components/CodeBlock.astro**

```typescript
---
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

**src/components/InternalLink.astro**

```typescript
---
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:

**GROQ query**

```groq
*[_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.

