Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 100 additions & 127 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,41 +121,36 @@

```ts
// sanity.config.ts
import {
defineArrayMember,
defineConfig,
defineField,
defineType,
} from "sanity";
import {defineArrayMember, defineConfig, defineField, defineType} from 'sanity'

export default defineConfig({
name: "default",
projectId: "abc123",
dataset: "production",
name: 'default',
projectId: 'abc123',
dataset: 'production',
schema: {
types: [
defineType({
name: "post",
type: "document",
name: 'post',
type: 'document',
fields: [
defineField({ name: "title", type: "string" }),
defineField({name: 'title', type: 'string'}),
defineField({
name: "content",
type: "array",
name: 'content',
type: 'array',
of: [
defineArrayMember({ type: "block" }),
defineArrayMember({type: 'block'}),
defineArrayMember({
type: "image",
options: { hotspot: true },
fields: [defineField({ name: "alt", type: "string" })],
type: 'image',
options: {hotspot: true},
fields: [defineField({name: 'alt', type: 'string'})],
}),
],
}),
],
}),
],
},
});
})
```

#### Before: hand-typing handlers
Expand All @@ -164,26 +159,24 @@

```tsx
// app/[slug]/page.tsx
import { createClient } from "@sanity/client";
import { createImageUrlBuilder } from "@sanity/image-url";
import { PortableText } from "@portabletext/react";
import { defineQuery } from "groq";
import {createClient} from '@sanity/client'
import {createImageUrlBuilder} from '@sanity/image-url'
import {PortableText} from '@portabletext/react'
import {defineQuery} from 'groq'

const client = createClient({
projectId: "abc123",
dataset: "production",
projectId: 'abc123',
dataset: 'production',
useCdn: true,
apiVersion: "2026-05-04",
});
const builder = createImageUrlBuilder(client);
apiVersion: '2026-05-04',
})
const builder = createImageUrlBuilder(client)

export default async function Page({ slug }: { slug: string }) {
const query = defineQuery(
`*[_type == "post" && slug.current == $slug][0]{title,content}`
);
const data = await client.fetch(query, { slug });
export default async function Page({slug}: {slug: string}) {
const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`)
const data = await client.fetch(query, {slug})

if (!data) return notFound();
if (!data) return notFound()

return (
<article>
Expand All @@ -196,37 +189,35 @@
}: {
value: {
asset?: {
_ref: string;
_type: "reference";
_weak?: boolean;
};
_ref: string
_type: 'reference'
_weak?: boolean
}
hotspot?: {
_type: "sanity.imageHotspot";
x?: number;
y?: number;
height?: number;
width?: number;
};
_type: 'sanity.imageHotspot'
x?: number
y?: number
height?: number
width?: number
}
crop?: {
_type: "sanity.imageCrop";
top?: number;
bottom?: number;
left?: number;
right?: number;
};
alt?: string;
_type: "image";
_key: string;
};
}) => (
<img src={builder.image(value).url()} alt={value.alt || ""} />
),
_type: 'sanity.imageCrop'
top?: number
bottom?: number
left?: number
right?: number
}
alt?: string
_type: 'image'
_key: string
}
}) => <img src={builder.image(value).url()} alt={value.alt || ''} />,
},
}}
value={data.content}
/>
</article>
);
)
}
```

Expand All @@ -236,26 +227,24 @@

```tsx
// app/[slug]/page.tsx
import { createClient } from "@sanity/client";
import { createImageUrlBuilder } from "@sanity/image-url";
import { PortableText } from "@portabletext/react";
import { defineQuery } from "groq";
import {createClient} from '@sanity/client'
import {createImageUrlBuilder} from '@sanity/image-url'
import {PortableText} from '@portabletext/react'
import {defineQuery} from 'groq'

const client = createClient({
projectId: "abc123",
dataset: "production",
projectId: 'abc123',
dataset: 'production',
useCdn: true,
apiVersion: "2026-05-04",
});
const builder = createImageUrlBuilder(client);
apiVersion: '2026-05-04',
})
const builder = createImageUrlBuilder(client)

export default async function Page({ slug }: { slug: string }) {
const query = defineQuery(
`*[_type == "post" && slug.current == $slug][0]{title,content}`
);
const data = await client.fetch(query, { slug });
export default async function Page({slug}: {slug: string}) {
const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`)
const data = await client.fetch(query, {slug})

if (!data) return notFound();
if (!data) return notFound()

return (
<article>
Expand All @@ -264,15 +253,13 @@
components={{
types: {
// value is fully typed from the query result, no annotation needed
image: ({ value }) => (
<img src={builder.image(value).url()} alt={value.alt || ""} />
),
image: ({value}) => <img src={builder.image(value).url()} alt={value.alt || ''} />,
},
}}
value={data.content}
/>
</article>
);
)
}
```

Expand All @@ -282,41 +269,37 @@

```tsx
// app/[slug]/page.tsx
import { createClient } from "@sanity/client";
import { createImageUrlBuilder } from "@sanity/image-url";
import { PortableText, type InferComponents } from "@portabletext/react";
import { defineQuery } from "groq";
import {createClient} from '@sanity/client'
import {createImageUrlBuilder} from '@sanity/image-url'
import {PortableText, type InferComponents} from '@portabletext/react'
import {defineQuery} from 'groq'

const client = createClient({
projectId: "abc123",
dataset: "production",
projectId: 'abc123',
dataset: 'production',
useCdn: true,
apiVersion: "2026-05-04",
});
const builder = createImageUrlBuilder(client);
apiVersion: '2026-05-04',
})
const builder = createImageUrlBuilder(client)

export default async function Page({ slug }: { slug: string }) {
const query = defineQuery(
`*[_type == "post" && slug.current == $slug][0]{title,content}`
);
const data = await client.fetch(query, { slug });
export default async function Page({slug}: {slug: string}) {
const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`)
const data = await client.fetch(query, {slug})

if (!data) return notFound();
if (!data) return notFound()

const components = {
types: {
image: ({ value }) => (
<img src={builder.image(value).url()} alt={value.alt || ""} />
),
image: ({value}) => <img src={builder.image(value).url()} alt={value.alt || ''} />,
},
} satisfies InferComponents<typeof data.content>;
} satisfies InferComponents<typeof data.content>

return (
<article>
<h1>{data.title}</h1>
<PortableText components={components} value={data.content} />
</article>
);
)
}
```

Expand All @@ -326,56 +309,46 @@

```tsx
// app/[slug]/page.tsx
import { createClient, type SanityQueries } from "@sanity/client";
import { createImageUrlBuilder } from "@sanity/image-url";
import {
PortableText,
type InferStrictComponents,
type InferValue,
} from "@portabletext/react";
import { defineQuery } from "groq";
import {createClient, type SanityQueries} from '@sanity/client'
import {createImageUrlBuilder} from '@sanity/image-url'
import {PortableText, type InferStrictComponents, type InferValue} from '@portabletext/react'
import {defineQuery} from 'groq'

const client = createClient({
projectId: "abc123",
dataset: "production",
projectId: 'abc123',
dataset: 'production',
useCdn: true,
apiVersion: "2026-05-04",
});
const builder = createImageUrlBuilder(client);
apiVersion: '2026-05-04',
})
const builder = createImageUrlBuilder(client)

// Array value type for every Portable Text item shape across all registered queries.
type PortableTextValue = InferValue<SanityQueries[keyof SanityQueries]>;
type PortableTextValue = InferValue<SanityQueries[keyof SanityQueries]>

function CustomPortableText({ value }: { value: PortableTextValue }) {
function CustomPortableText({value}: {value: PortableTextValue}) {
const components = {
types: {
image: ({ value }) => (
<img src={builder.image(value).url()} alt={value.alt || ""} />
),
image: ({value}) => <img src={builder.image(value).url()} alt={value.alt || ''} />,
},
} satisfies InferStrictComponents<PortableTextValue>;
} satisfies InferStrictComponents<PortableTextValue>
// ^ TypeScript errors when the schema gains a custom type, mark, or list
// style without a matching handler defined here

return <PortableText components={components} value={value} />;
return <PortableText components={components} value={value} />
}

export default async function Page({ slug }: { slug: string }) {
const query = defineQuery(
`*[_type == "post" && slug.current == $slug][0]{title,content}`
);
const data = await client.fetch(query, { slug });
export default async function Page({slug}: {slug: string}) {
const query = defineQuery(`*[_type == "post" && slug.current == $slug][0]{title,content}`)
const data = await client.fetch(query, {slug})

if (!data) return notFound();
if (!data) return notFound()

return (
<article>
<h1>{data.title}</h1>
{Array.isArray(data.content) && (
<CustomPortableText value={data.content} />
)}
{Array.isArray(data.content) && <CustomPortableText value={data.content} />}
</article>
);
)
}
```

Expand Down
Loading