Using Sanity History API for Page Version Navigation

17 replies
Last updated: May 20, 2021
Anyone here worked with historical data in Sanity? We want to give our users a way to navigate historical versions of our pages, so the plan is to use the history-api to do some clever queries and display a dropdown menu where the users can select different revisions of the page. If anyone has done something similar I would greatly appreciate some tips and pointers 🙂 For example it doesn't seem like it possible to use the history-api from the
@sanity/client
?
AI Update

Great question! Building a historical version navigator is definitely doable with Sanity's History API. Here's what you need to know based on my experience and the docs:

Using the History API

You're correct that the History API isn't directly exposed through @sanity/client. Instead, you'll need to make direct HTTP requests to the history endpoints. The main endpoint you'll want to use is:

https://${projectId}.apicdn.sanity.io/v1/data/history/${dataset}/documents/${docId}?time=${time}

This lets you fetch a document as it existed at a specific point in time, which is perfect for your dropdown navigation use case.

Implementation Approach

Here's a practical approach for building your version dropdown:

  1. Fetch transaction history to get a list of all revisions:
const historyUrl = `https://${projectId}.apicdn.sanity.io/v1/data/history/${dataset}/transactions/${docId}`;
const response = await fetch(historyUrl, {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});
  1. Parse the transactions to build your dropdown menu - each transaction gives you a timestamp and change information

  2. Fetch specific versions when users select from the dropdown using the time parameter shown above

Important Considerations

History retention limits - Make sure you're aware of what your plan offers:

  • Free: 3 days
  • Growth: 90 days
  • Enterprise: 365 days or custom

Input sanitization - Since you're building user-facing features, be sure to properly validate and sanitize document IDs and timestamps to prevent security issues. The History API documentation specifically calls this out.

Using client.request() - While the History API isn't built into the standard client methods, you can use the lower-level client.request() method to make authenticated requests to the history endpoints without manually handling tokens:

const history = await client.request({
  url: `/data/history/${dataset}/documents/${docId}?time=${timestamp}`,
  withCredentials: true
});

This approach keeps your authentication handled by the client while still accessing the history endpoints.

Alternative: Document Revisions

If you need simpler access to recent changes, the document history feature in the Studio UI shows revisions, and you can programmatically restore documents using client.createOrUpdate() with data from historical versions.

Hope this helps! Let me know how it goes with your implementation.

Show original thread
17 replies
Hi Daniel! You’re right that our js-client don’t have support for the history API. This is how the Studio are fetching the history https://github.com/sanity-io/sanity/blob/next/packages/@sanity/desk-tool/src/panes/documentPane/documentHistory/history/controller.ts#L256
Thanks
user P
, thats very helpfull 🙌
No problem 🙂
We've built a solution where our end users can browse document history, and it works really nicely thanks to the history-api 🎉
Right now we are fetching document-history server-side (nextjs) using urls built like this: ``https://${projectId}.
apicdn.sanity.io/v1/data/history/${dataset}/documents/${docId}?time=${time} `` .
time
and
docId
is taken directly from the request-url (for example
<http://localhost:3000/historikk/0caeeb00-deb0-4310-bb04-17ffb2ccd165/2021-05-12T12:09:23.000Z>
). Does this leave us open to some kind of vulnerability? Should we for example sanitise this input, validate it, cache it, or worry about ddos attacks etc..? Other things we should consider?
Thankful for any input on this
🙂
Code here:

https://github.com/navikt/dp-faktasider-frontend/blob/08795ca914ae4f649c419b04e14bc832e9b1f5a1/src/pages/historikk/%5B...slug%5D.tsx#L34
https://github.com/navikt/dp-faktasider-frontend/blob/08795ca914ae4f649c419b04e14bc832e9b1f5a1/src/components/historikk/api/historikkFetcher.ts#L18
https://github.com/navikt/dp-faktasider-frontend/blob/08795ca914ae4f649c419b04e14bc832e9b1f5a1/src/components/historikk/api/revisionsFetcher.ts#L16
Hi
user Q
!
I would always sanitise user input. If you’re taking the docId and time directly without sanitising it it might leave you vulnerable to path traversal attacks. Without having looked to much on the code, and I assume the
historikkFetcher
is using a client/token with elevated privileges, it could be possible to construct a query that fetches a document you shouldn’t have access to
Thanks
user P
🙂
I'm new to sanitising user input, what is the correct way to do this? Any libraries/javascript functionality that can sanitise this in a meaningful way? Or checking the id with a regex like
/[^a-z0-9-]/.test(docId)
and time with another suitable regex?
Any other threats we should consider? By allowing any time as input, are we more vulnerable against ddos-attacks for example?
Any suggestion as to how we should sanitise these inputs? Any libraries that can sanitise this in a meaningful way? Or checking the id with a regex like
/[^a-z0-9-]/.test(docId)
and time with another suitable regex?
Any other threats we should consider? By allowing any time as input, are we more vulnerable against ddos-attacks for example?
What does your IDs look like? 🙂
What does your IDs look like? 🙂
For time you can use
date-fns
to validate date times 🙂
/[^a-z0-9-]/.test(docId)
will match everything that isn’t:
a-z
,
0-9
or
-
, I assume you maybe meant without the negative op(
^
) ?
These are Sanity-document IDs, so I thought I'd test if they used some illegal characters and in that case throw an error or something 🙂 But would maybe a simple
encodeURIComponent(id/time)
do the trick?
Right! The id’s generated by the studio are uuids,
const uuidMatcher = /^[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+$/
would return true on a valid id
with
encodeURIComponent
you should decode on receiving end, it’s not for sanitising input in that way.ie,
encodeURIComponent("+") => %2B
so if the
time
field contains a plus it won’t be valid when you pass it to the api client 🙂
with
encodeURIComponent
you should decode on receiving end, it’s not for sanitising input in that way.ie,
encodeURIComponent("+") => %2B
so if the
time
field contains a plus it won’t be valid when you pass it to the api client 🙂
https://date-fns.org/v2.21.3/docs/isValid Can be used for validating the time field, or you can do
function isValidDate(time) { try { new Date(time); return true } catch(err) { return false; }}
Perfect, thank you 🙂 🙌

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?