Apply Sanity crop and hotspot to images

Fetch the full image objects instead of only the asset URL so the URL
builder can apply the crop and hotspot set in the Studio, crop the Open
Graph image around the hotspot, and add sizes to full-width blog images.

Closes #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
leanderandClaude Opus 5.5 committed 2026-10-30 23:42:21 +01:00
1 parent f647dd869d
commit 1a18096cf6
4 files changed
+27 -40

No files matched your search

+13 -10
View File
@@ -2,7 +2,7 @@ import Footer from "@/components/page-sections/footer"
import { generateSeoMetadata } from "@/components/seo"
import Back from "@/components/ui/back"
import { urlFor } from "@/lib/imageUrlBuilder"
import { type SanityImage, urlFor } from "@/lib/imageUrlBuilder"
import { getBlogPost } from "@/lib/sanity"
import { PortableText, PortableTextComponents } from "@portabletext/react"
@@ -20,11 +20,7 @@ interface BlogPageParams {
}>
}
interface ImageValue {
asset?: {
_id: string
url: string
}
type ImageValue = Partial<SanityImage> & {
alt?: string
caption?: string
}
@@ -67,7 +63,10 @@ export async function generateMetadata({
path: `/blog/${slug}`,
title: data.title,
description: data.excerpt,
imageURL: data.mainImage?.asset?.url,
// Cropped around the hotspot to the size link previews expect
imageURL: data.mainImage?.asset
? urlFor(data.mainImage).width(1200).height(630).url()
: undefined,
author: data.author?.name,
publishedTime: data._createdAt,
})
@@ -80,10 +79,13 @@ const components: PortableTextComponents = {
return (
<div className={styles.imageContainer}>
<Image
src={urlFor(value).url()}
// Only a width, so the image keeps its shape and just the crop
// applies. fit("max") avoids upscaling small images
src={urlFor(value).width(1200).fit("max").url()}
alt={value.alt || "Blog image"}
width={800}
height={400}
sizes="(max-width: 640px) 100vw, 640px"
style={{ width: "100%", height: "auto" }}
/>
{value.caption && (
@@ -144,10 +146,11 @@ const BlogPost = async ({ params }: BlogPageParams) => {
<header className={styles.header}>
{data.mainImage && (
<Image
src={urlFor(data.mainImage).url()}
src={urlFor(data.mainImage).width(1600).fit("max").url()}
alt={data.title}
width={800}
height={400}
sizes="(max-width: 850px) 100vw, 850px"
className={styles.headerImage}
/>
)}
@@ -158,7 +161,7 @@ const BlogPost = async ({ params }: BlogPageParams) => {
<Image
className="circular"
alt={data.author.name}
src={urlFor(data.author.image).width(50).height(50).url()}
src={urlFor(data.author.image).width(100).height(100).url()}
width={50}
height={50}
/>
+5 -12
View File
@@ -1,4 +1,4 @@
import { urlFor } from "@/lib/imageUrlBuilder"
import { type SanityImage, urlFor } from "@/lib/imageUrlBuilder"
import { getBoardMembers } from "@/lib/sanity"
import { getLocale, getTranslations } from "next-intl/server"
@@ -14,12 +14,7 @@ interface BoardMember {
no: string
en: string
}
image?: {
asset?: {
_id: string
url: string
}
}
image: SanityImage | null
bio?: {
no: string
en: string
@@ -44,11 +39,9 @@ const BoardMembers = async () => {
>
<Image
src={
member.image?.asset?.url
? urlFor(member.image.asset.url)
.width(512)
.height(512)
.url()
member.image?.asset
? // Cropped to a square around the hotspot
urlFor(member.image).width(512).height(512).url()
: "/person.png"
}
width={256}
+6
View File
@@ -1,5 +1,6 @@
import {
createImageUrlBuilder,
type SanityImageObject,
type SanityImageSource,
} from "@sanity/image-url"
@@ -7,6 +8,11 @@ import { sanityClient } from "./sanity"
const builder = createImageUrlBuilder(sanityClient)
// An image field as stored in Sanity: an asset reference plus the crop and
// hotspot chosen in the Studio. Pass the whole object to urlFor, not the
// asset URL, or the crop and hotspot are ignored
export type SanityImage = SanityImageObject
export function urlFor(source: SanityImageSource) {
return builder.image(source)
}
+3 -18
View File
@@ -17,19 +17,9 @@ export async function getBlogPost(slug: string) {
title,
author-> {
name,
image {
asset-> {
_id,
url
}
}
},
mainImage {
asset-> {
_id,
url
}
image
},
mainImage,
excerpt,
body
}
@@ -119,12 +109,7 @@ export async function getBoardMembers() {
no,
en
},
image {
asset->{
_id,
url
}
},
image,
bio {
no,
en