Discussion on getting an array of images from Sanity for a gallery page in Next.js, with tips on code formatting and content strategy.

34 replies
Last updated: Jan 5, 2023
Hello Happy New Year! I am trying to get an array of URL's from Sanity but either get null or object.

export const getStaticProps = async () => {
  const query = groq`*[_type == "gallery" ]{
    "url": imgUrl.asset->url,
    title,
    description,
    projectLink,
    codeLink,
    tags
  }`
  const data = await client.fetch(query)
  return {
    props: { data }
  }
}
AI Update

Happy New Year! The issue you're experiencing is likely because imgUrl is either not an image type field, or you're trying to reference it incorrectly. Let me help you fix this.

The problem is in how you're dereferencing the asset. When you have an image field in Sanity, it's stored as a reference object, and you need to properly traverse it to get the URL.

Your query actually looks correct syntactically! So the issue is likely one of these:

1. The field name might be different Check your schema - is the image field actually called imgUrl? It might be image, mainImage, or something else.

2. Some documents might not have images If some gallery documents don't have an image, you'll get null for those. You can filter them out:

const query = groq`*[_type == "gallery" && defined(imgUrl.asset)]{
  "url": imgUrl.asset->url,
  title,
  description,
  projectLink,
  codeLink,
  tags
}`

3. You need the full image URL If you're getting an object instead of a string, you might need to use Sanity's image URL builder. Here's a better approach:

import imageUrlBuilder from '@sanity/image-url'

const builder = imageUrlBuilder(client)

export const getStaticProps = async () => {
  const query = groq`*[_type == "gallery" && defined(imgUrl)]{
    imgUrl,
    title,
    description,
    projectLink,
    codeLink,
    tags
  }`
  const data = await client.fetch(query)
  
  // Transform the data to include proper URLs
  const dataWithUrls = data.map(item => ({
    ...item,
    url: item.imgUrl ? builder.image(item.imgUrl).url() : null
  }))
  
  return {
    props: { data: dataWithUrls }
  }
}

The image URL builder approach is the recommended way because it gives you access to all of Sanity's image transformation capabilities and handles the asset reference properly.

If you just want an array of URLs (not the full objects), you can simplify it:

const query = groq`*[_type == "gallery" && defined(imgUrl.asset)].imgUrl.asset->url`

This will return just an array of URL strings directly. The key is using .imgUrl.asset->url at the end of the projection to extract just those values into a flat array.

In the request you are asking for a single image url not an array.
I'm not positive but try <nameOfArray>[] -> {"url": ...
Post the gallery schema here and I can give you a better idea.
I don't think that you saw all of the code my bad here you go
import groq from "groq";import imageUrlBuilder from "@sanity/image-url";
import client from "../client";

import React, { useState } from 'react';

import Masonry, {ResponsiveMasonry} from "react-responsive-masonry"
import styles from "../styles/Gallery.module.css";
import { setRevalidateHeaders } from "next/dist/server/send-payload";
import { data } from "jquery";


// get the image url from the sanity asset
function urlFor(source) {
return imageUrlBuilder(client).image(source);
}
//end get the image url

//query get sanity data
export const getStaticProps = async () => {
const query = groq`*[_type == "gallery" ]{
"url": imgUrl.asset->url,
title,
description,
projectLink,
codeLink,
tags
}`
const data = await client.fetch(query)
return {
props: { data }
}
}
// end get data exported as 'data'



// the page magic
const Gallery = ({ data }) => {

// create an array of images to use for prev and next
const images = data.map(({ url }) => [url])
console.log(images)
// end create an array

const [info, setInfo] = useState({img: '', i: 0})

const viewImage = (img, i)=>{
setInfo({img, i})
}

const imgAction = (action) => {
let i = info.i

if(action === 'next-img'){
setInfo({img: images[i + 1], i: i + 1})

}
if(action === 'prev-img'){
setInfo({img: images[i - 1], i: i - 1})

}
if(!action){
setInfo({img: '', i: 0})
}
}

return (
<>
{info.img &&
<div style={{
width: '100%',
height: '100vh',
background: 'black',
position: 'fixed',
top: '0',
left: '0',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
}}>
<button onClick={()=> imgAction()} style={{position: 'absolute', top: '20px', right: '20px'}}>X</button>
<button onClick={()=> imgAction('prev-img')} style={{marginRight: '10px' }}>Previous</button>
<img src={info.img} style={{width: 'auto', maxWidth: '90%', maxHeight: '90%'}}/>
<button onClick={()=> imgAction('next-img')} style={{marginLeft: '10px' }}>Next</button>
</div>
}
<ResponsiveMasonry
columnsCountBreakPoints={{350: 1, 750: 2, 900: 3}}
>
<Masonry gutter="20px">
{ data.map((p, i)=>(

<img
key={i}
src={p.url}
alt={p.title}
className={styles.pic}
onClick={()=> viewImage(p.url, i)}
/>
))}
</Masonry>
</ResponsiveMasonry>
</>
)
}

export default Gallery
user T
are you using React or NextJS?
Do you have gallery document type? Or where is the gallery setup in your schemas?
This tells me you’re using next, but there are some inconsistencies in your code, I would tackle πŸ˜‰
1) You are querying for a document called gallery, but only query for one imgUrl. Without knowing more about your setup this will only work (see comment πŸ‘‡)
export const getStaticProps = async () => {
  const query = groq`*[_type == "gallery" ]{
    "url": imgUrl.asset->url,
// This would work for getting image urls directly
    "imageUrls": images[].image.asset->url,

    title,
    description,
    projectLink,
    codeLink,
    tags
  }`
  const data = await client.fetch(query)
  return {
    props: { data },
  }
}
I would recommend using the Image functionality of
NextJs and Sanity to get responsive and optimised images.
2) using the
optimised image functionality (
image-url
plus
<Image/>
) where you combine these 2 instructions from NextJs and Sanity
3) This is a more friendly tip, but very important in my opinion:
Using verbose vars in your code will help you maintain it and make scaling way easier πŸ™‚. So instead of this:
const [info, setInfo] = useState({ img: '', i: 0 })
you could write this:

const [displayedImage, setDisplayedImage] = useState({ imageUrl: '', imageIndex: 0 })
Following good practices like this will make your code better searchable and readable for both machines and people
🀝 (believe me, using Machine Learning and AI in code like Github Copilot shows how important this is πŸ˜‰ )
I will wait for you to provide some answers, if you need more help
That's great! I really appreciate the advice and it is always welcome. I am positive that there is a more efficient way to code this page so any help is great. I would like to have a galley page that I can use in several projects. You're the best!:sanity_with_3_hearts:
So then one Gallery with the same images across multiple other pages? then that will work in a way. Where do you store the other data for the pages?
No it would be a page that uses data (images and content) stored in Sanity. The page would work the same given the schema and names all don't change. That is easy though. The project id in sanity would really be the only change to the Next and Sanity apps. Images for other pages that are a part of the content are stored in the body (portable text). Ultimately yes all data is in Sanity. I also am working on a dynamic menu for restaurants so that updates would be easy which has it's own schema / images in the same site.
I was asking about your schema setup πŸ˜‰ I mean HOW will the content in sanity look like and how will it translate into a front-end. I have no idea how your code looks like, this is why I ask.There are many roads that lead to rome
πŸ›£οΈ
As a small tipp number 2:
You need to plan your content strategy early on (structure your content), or it will bite you in the
πŸ‘ when you scale up and then need to mutate all you data. Our Carrie Hanes is a pro in all things content and I think this might help you longterm
Ah ok. Here's the gallery.js:
export default {
    name: 'gallery',
    title: 'Gallery',
    type: 'document',
    fields: [
      {
        name: 'title',
        title: 'Title',
        type: 'string',
      },

      {
        name: 'description',
        title: 'Description',
        type: 'string',
      },
      {
        name: 'projectLink',
        title: 'Project Link',
        type: 'string',
      },
      {
        name: 'codeLink',
        title: 'Code Link',
        type: 'string',
      },
      {
        name: 'imgUrl',
        title: 'ImageUrl',
        type: 'image',
        options: {
          hotspot: true,
        },
      },

      {
        name: 'categories',
        title: 'Tags',
       type:'array',
       of: [
         {
          type: 'reference', to: {type: 'category'}
         }
       ]
      },

    ],
  };
Tip number 3: use code formatting in Slack: (see left image)1. Short inline code snippets &lt;/&gt; like this
const = x
is useful (cmd+shift+c)2. If you need to share longer snippets use block code (option+shift+c)
3. if you need to share whole files: cmd+shift+enter
And you can edit your posted messages (see right image)
It is really hard to work through unformatted text/code+
What is
codeLink
? what is
projectLink
?And what else are you using for data in the pages (do you have a page document type in sanity? …
Those links are for a dev portfolio piece so perhaps a website or github. That part is really just a placeholder
okay, I will wait for you to write what you have setup. I cannot continue guessing πŸ™‚You show me how you set things up, and I will show you how to use the data. I am here to help, but if you dont read and answer my whole questions, I cannot help you
πŸ₯²
But now that i can read your code: You have a gallery with only one image?And I would not call it imageUrl, since you dont get an image url but a reference to an image, which in your frontend queried data will be an object.
I would set it up like this:
      {
        name: 'images',
        title: 'Images',
       type:'array',
       of: [
         {
          type: 'image', name: 'image',
            options: {
             hotspot: true,
            },
         }
       ]
      },
so the groq would need to change and that is where I get stuck. The code needs a 'typical' map to show each clickable image which opens the modal. It is because the images are needed in an array that I got so confused... I made the changes you suggested in the schema... now what on the query?//query get sanity data
export const getStaticProps = async () => {
  const query = groq`*[_type == "gallery" ]{
    "url": imgUrl.asset->url,
    title,
    description,
    projectLink,
    codeLink,
    tags
  }`
  const data = await client.fetch(query)
  return {
    props: { data }
  }
}
// end get data exported as 'data'
the use case here is say a tattoo artist wants to showcase some of their work on the gallery page with a larger version in a modal
no need for more than one image per 'gallery entry
A gallery is always a collection of images, you cannot map over an object which you now are trying to do.
ARRAY.map πŸ˜‰
collection = list = array, you are setting up an object though.
If you use this:
      {
        name: 'images',
        title: 'Images',
       type:'array',
       of: [
         {
          type: 'image', name: 'image',
            options: {
             hotspot: true,
            },
         }
       ]
      },
your query:

const query = groq`*[_type == "gallery" ]{
    images,
    title,
    description,
    projectLink,
    codeLink,
    tags
  }`
you need to then in your front-end use the
image-url functionality
urlFor(source)
when mapping over the images array:
// in the component where you pass down images in props with a configured urlFor image from @sanity/image-url

const images = props?.images.map((image, imageIndex) => {
  const imgUrl = urlFor(image.asset).width(300).url()
  return image['imageUrl']=imgUrl
})
using this you will have a whole image object with the imageUrl added to it. You can also skip constructing the new object and using it like to in the component:
src={urlFor(image.asset).width(300).url()}
Oh wow ok that makes sense! Much cleaner way of doing this. Perfect thank you so much!
I am still trying to get this to work as you showed using the entire image object which makes sense that is how I used it to begin with but I am getting TypeError: Cannot read properties of undefined (reading 'map')
does any of this have to do with using the app folder in Next?
Thats all to do with your code ;) you need to check that you are passing down data and where the bottle neck is.
?
Actually I just got it. :)
typo

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?