Using `walkjs` to fetch and attach image metadata in Sanity queries

6 replies
Last updated: Jun 7, 2024
Is there an easy way to get the blurhash from the image metadata without having to do anything in groq?
Ideally I'd like it to just always pull down so I don't have to edit every query
AI Update

Unfortunately, there isn't a built-in way to automatically include blurhash in every query without explicitly requesting it in GROQ. However, there are a few approaches that can help minimize the manual work:

The Schema-Level Solution

The good news is that blurhash is included by default when you upload images to Sanity. According to the image metadata documentation, you don't need to explicitly configure it in your schema - it's automatically extracted and stored alongside your image assets.

This means the data is already there in your Content Lake, you just need to query for it.

Making GROQ Queries Easier

While you still need to explicitly request the blurhash in GROQ, you can make this less repetitive:

1. Create a GROQ fragment/helper:

// Define once in a constants file
const IMAGE_WITH_METADATA = `{
  asset->{
    _id,
    url,
    metadata {
      dimensions,
      lqip,
      blurHash
    }
  }
}`

// Use in queries
const query = `*[_type == "post"] {
  title,
  mainImage ${IMAGE_WITH_METADATA}
}`

2. Use a helper function with your SDK:

// Wrapper that automatically adds image metadata
function queryWithImageMetadata(baseQuery) {
  // Parse and inject image metadata projections
  // This is more complex but possible
}

3. Consider using sanity-codegen or TypeGen:

With proper TypeScript types, you can create reusable projection types that make it easier to consistently query the same fields.

Why No Auto-Include?

GROQ's philosophy is explicit projections - you only get what you ask for. This keeps payloads small and queries performant. While it means a bit more typing, it prevents accidentally fetching large amounts of metadata you might not need in every context.

The tradeoff is that you need to be explicit in your projections, but the benefit is that your schema configuration ensures the data is always available when you do query for it.

No, you’d need to explicitly query for it each time.
Hey User! Yes, in the query every time but my team has in the past used utilities that use
walkjs
to traverse the returned data, grab the image refs and re-query the extra metadata, and reattach them to the data to avoid super long queries, highly recommend these!
user D
Do you have a code example?
import { WalkBuilder, deepCopy } from "walkjs"


export const resolveLinks = async (inputData, maxDepth = 5) => {
  const store = new Map()

  const replaceNode = (node, id) => {
    const doc = store.get(id)
    if (["link"].includes(node.key)) {
      const values = {
        slug:
          doc._type === "home"
            ? { _type: "slug", current: "/" }
            : {
                _type: "slug",
                current:
                  "/" +
                  (doc.slug?.current
                    ? doc.slug.fullUrl || doc.slug.current
                    : `${doc._type}`),
              },
        label: node.parent?.val?.label,
        docType: doc._type,
      }

      const _key = node.val._key || node.parent.val._key || doc._key
      if (_key) {
        values._key = _key
      }
      Object.keys(node.parent.val).forEach(key => delete node.parent.val[key])
      Object.keys(values).forEach(key => {
        node.parent.val[key] = values[key]
      })
    } else {
      Object.keys(node.val).forEach(key => delete node.val[key])
      Object.keys(doc).forEach(key => {
        const value = doc[key]
        node.val[key] = typeof value === "object" ? deepCopy(value) : value
      })
    }
  }

  const iterate = async nodes => {
    const ids = new Map()

    new WalkBuilder()
      .withGlobalFilter(a => a.val && a.val._type === "reference")
      .withSimpleCallback(node => {
        const refId = node.val._ref
        if (typeof refId !== "string") {
          throw new TypeError("node.val is not set")
        }

        if (!refId.startsWith("image-")) {
          if (!store.has(refId)) {
            // unresolved, add it to the list
            ids.set(refId, node)
          } else {
            // already resolved, can be replaced immediately
            replaceNode(node, refId)
          }
        }
      })
      .walk(nodes)

    if (ids.size) {
      // fetch all references at once
      const documents = await sanityClient.fetch(
        `*[_id in [${[...ids.keys()].map(id => `'${id}'`).join(",")}]]{...}`
      )
      documents.forEach(element => {
        store.set(element._id, element)
      })

      // replace them
      ids.forEach((node, id) => {
        replaceNode(node, id)
      })

      if (!--maxDepth) {
        console.error("Sanity autoresolver max depth reached")
        return
      }

      // iterate threw newly fetched nodes
      await iterate(nodes)
    }
  }

  await iterate(inputData)
}
Awesome, thanks 🙏
To break it down:
1. Go thru the response of a query, find anything with _ref
2. Add to a list
3. Query eveything on the list
4. Attach returned reference data to object

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?