Web Development
Coding
Technology
April 10, 2025
Content Management with Sanity: From ‘A Studio’ to ‘A Nice Studio’
How and why I customized my Sanity Studio—adding a preview pane, dynamic slug routing, SSR cookie-based session handling, and API routes to improve content editing.

Steven
Software Engineer
Context Before the Code
I recently went on a deep dive customizing my Sanity Studio for a headless CMS project. What started as a quick attempt to add a draft content preview feature turned into a hands-on learning experience with Sanity, Next.js, CORS, and session management.
Along the way, I was able to implement some recently learned concepts like custom API routes and cookie-based session handling to build a flexible, secure preview system—giving content creators a smoother editing workflow. All while ensuring that drafted content and published content from the studio remains separated on the public-facing frontend.
In-Page Links for Quick Navigation
- 1. From ‘A Studio’ to ‘A Nice Studio’
- 2. Implementing the Feature
- 3. Needing a Creative Touch
- 4. Touch Ups
- 5. Baking in Functionality; with Cookies
- 6. Key Challenges; Infrastructure and Security
- 7. Can I Get a Toggle With That?
- 8. Troubleshooting Tips
- 9. Why This Solution Works for Me
- 10. Why It’s Worth Talking About
- 11. Final Words
- 12. Disclaimer
🛠️ From ‘A Studio’ to ‘A Nice Studio’
When I was preparing to add the Headless CMS project that sends data to my Tsundoku blog as a piece on my portfolio site, I wrote up a nice how and why along with what it is and why it’s nice. I was happy when I finished after an hour or so of writing, because I remembered most of the implementation details and why I liked Sanity so much off the top of my head. It really is a nice option if you’re looking into a headless CMS; and here is a link to the post if you want to read more about it.
After I was done, it was time to take a few pictures of the studio and how it’s used for the portfolio piece. This is when I realized though; the app used to manage my studio is powerful and very useful, but also a little boring. It’s just a big black menu with content boxes I created to push data to the front-end.
When I was originally making the studio, I wanted to add a content viewer so that someone working with the content could see changes being made to the site before submitting the changes to the live site. I didn’t get to implement it though because there were other priorities that I needed to attend to, so the feature got shelfed. But look at this studio - its functional, but pretty basic.
This is what a Sanity Studio initial install looks like - minus the content.
So now revisiting the project and giving it some proper attention as separate from the front-end, I decided to implement that feature now because it would be pretty nice to have in the future when making more blog posts (like this), for the photos I would use on the portfolio piece, and as a nice proof of concept showing the customization that you can perform on the studio itself.
🔧 Implementing the Feature
As I said - upon initialization, Sanity’s studio is pretty bare bones. It structures your content and the studio application itself based on rules that you define on how to structure it through config, structure, and schema files.
Aside from the content, the studio itself is typically only installed with tools like a “Structure” component that allows you to customize the editorial interface, and a “Vision” component that allows you to query your content using GROQ - Sanity’s proprietary querying language; but these components can be replaced or built upon as needed. In the case you are reading about, I built on the “Structure” component.
To implement my preview window, I decided to extend the basic structure tool with a custom dual-purpose tool that can either view the content input window or a preview window that fetches the published and drafted content to show in that preview window.
The preview page portion of the custom structure was made by adding the sanity-plugin-iframe-pane npm package to the studio project. It allows me to add an “iFrame” window with specified content in it, where I will link a route to my front-end. To give the “structure tool” a secondary toggle for the window, I added a “view” option to the structure file that toggles the side panel — where input fields for a piece of content can be edited.
Sample of a "Structure" module to export into the sanity.config.ts in the root of the project.
So now I can toggle the content window between the input fields for the content I am producing - or a view of the page that corresponds to the slug field of the article I am working on. The latter will fetch data from the website and display fully rendered data within the studio.
This allows a studio user to open a content menu to change the input of blog content, a window to view the changes on the blog in real time, or open two windows side by side to change content and view the content side by side.
This is the toggle to switch between content input mode (the left) and preview mode (the right).
🧩 Needing a Creative Touch
There was a problem though - I did not want to simply show the published content of the preview window, I wanted to show drafted and published content with drafted changes on top if there were any. This required me to think of a way to tell the server serving my site if I was viewing content on the production blog site, or from within the preview window in the studio.
By default, Sanity only makes content in the dataset available if the content has been officially published. That means I had to create an API key to allow for me to read all content - including drafted content - from the studio, then add that key as an environment variable to the website project, and establish a way for a cookie to be distributed to a client to flag a session as “draft mode” or “prod mode.”
I decided to assign data to cookies handed to the client via an accessible API route that I would use for a “draft mode.” I was able to read the cookie data on the server by using cookies from next/headers. No accessing those sweet sweet drafts until they are officially published. The following code runs on all page loads and checks for my delicious “draft mode” cookie;
This is the logic that checks every page load to determine if drafted content should be shown.
📝 Touch Ups
With this, I could use the presence of the custom cookie on the headers sent to the server to securely decide whether to include the token for draft content when querying Sanity - because Next.js supports SSR (server-side rendering), and assign the cookie to sessions calling the site from the custom API route - which the preview window would do by default. Phew.
Cool, now I can mark client-to-server connection sessions as “draft mode” or “production mode” using cookies - and I can make an API route in my Next.js structure to ping from the front-end and specifically say that I am entering “draft mode” to see all the unpublished content. Now I can set up the preview window to access the API route by default to obtain that cookie and show the drafted content.
As a final touch - I set the preview window to exclusively preview by configuring the preview pane to dynamically construct an iFrame URL when opened based on the slug of the article being edited.
🍪 Baking in Functionality; with Cookies
Nice, so now - I have a way to assign a cookie to a client if they ping an API which would allow the server to include the token in GROQ queries including drafted content only if the request originates from a preview-enabled session.
This is the logic to assign a cookie to a session if the request has come through the custom API.
That was a lot for learners, so put simply; when I open content to edit it in my studio, there are two options to view the content - the first option and the default is to view the content input menu, and the second is a preview window that when clicked will pre-fill a URL to that article we are editing specifically.
From there, we can see all of the drafted changes on the article, and we can navigate the site to see everywhere the changes take effect, like the home screen, the allPosts page, etc.
For an extra layer of protection and session persistence, I used NextRequest and NextResponse from next/server in middleware. This allows the middleware to securely read/set cookies, ensuring that once 'draft mode' is activated, the user's session remains in 'draft mode' until explicitly exited by clearing the cookie.
This is the simple middleware I use to persist session data.
🧱 Key Challenges; Infrastructure and Security
All I had to do from here was add a couple CORS configurations to allow requests between the studio and the deployed frontend domain, commit everything in Git (commit early and commit often folks,) deploy the site to Vercel and the studio to Sanity respectively, add the API key to the Vercel environment hosting my project, and now I have a custom pipeline built in which I can manage the lifecycle of all my content from the studio.
My content creators and I can make content in the studio, access content on the frontend, then conditionally choose if the frontend sees the published content exclusively - or if it shows drafted changes and unpublished content with the published content; which I choose the latter from the preview content window in the studio. All handled with API routes and cookies. Delicious.
This is how I like to personify my programs. This API gets custom preview cookies while all of the static routes expected to be used through production gets production cookies.
🔘 Can I Get a Toggle With That?
I would argue that good design and good engineering require us to carefully balance user control with intentional constraints. A tailored experience always keeps in mind, “What does the user want, what do they need, and what are they using this for?”
Well, I’m using this “draft mode” tool to view drafts; but what if I don’t want to see drafts anymore, and I only want to see production material? This could quickly become a significant UX issue if scaled up—imagine implementing this studio workflow at a company with 20 editors or more.
To improve the experience a bit - I created an API route that enables the frontend to easily exit “draft mode,” which ended up being a nice little QoL consideration for future me and other content creators. Now we can all toggle between “draft mode” and “production mode.” You’re welcome, everyone 😌
The following code is a simple API to exit draft mode;
Simple API to ping when a content creator wants to exit draft mode.
🔨 Troubleshooting Tips
If you want to add this feature yourself, some basic troubleshooting advice; If you don’t want to (or don’t need to) use Next.js, you can absolutely implement preview mode in your studio in much easier ways than the cookie/API tag-team I used.
Next.js’s support for SSR improves SEO and UX, but it definitely adds complexity in more than a few places—especially when dealing with cookies, preview sessions, and CORS between the studio and frontend. That being said, I chose Next, and I’d choose it again. (Or maybe SvelteKit—but that’s a whole other article.)
If you’re gonna use Next, be prepared to configure your CORS settings carefully to allow secure communication between the studio and the frontend, without opening up unnecessary access.
Keep in mind, a lot of your errors will be dealing with SSR, cookies, and CORS specifically because of Next.js’s Server Side Rendering. And lastly—don’t forget to swap out any [localhost] preview URLs for your actual domain when you push to production 😅
✅ Why This Solution Works for Me
This is awesome because it allows me to manage my content effectively and efficiently by previewing what changes to the site will look like before publishing them. This approach also separates published content from drafted content so visitors won’t ever see the drafted content while I can see it from the studio preview. No drafts leaking into production.
It also improves the experience for content creators by broadening their capabilities while reducing the need for multiple on-screen tools and windows; no swapping back and forth to preview changes, and no publishing 100 times to get the spacing and characters the way you want them on the page.
It’s worth noting; there are plenty of ways to implement a feature like this, with varying degrees of security and features depending on your requirements and use case.
I chose this method because I wanted a way to isolate sessions with or without preview permissions granted by cookies, so content creators could have a second window open to view the production site while working within their workspace to view drafted content. That is my workflow preference as a creator for the site 🙃
Content menu on the left, the draft previews in the middle, and a scaling window on the right where I can look at styles, borders, mobile views, etc.
✍️ Why It’s Worth Talking About
I chose to write this article to showcase how customizable a Sanity Studio can be. I created a completely custom iFrame with dynamic pathing and logic for cookie distribution and conditional environment variable usage based on session data. This setup allows me to preview site-wide changes to content before publishing while keeping the public-facing site free of any drafted content.
The API route implementation with conditional cookie designation was a custom design decision—I didn’t find it in the docs anywhere. It was technical enough to write an edge case I didn’t expect when I just wanted to add a simple feature.
I had quite a few issues interpreting Sanity’s docs for this niche use case—specifically, how to preview drafted content without it leaking into production, while still allowing navigation between pages during preview mode.
Writing out the entire process like this also helps me dissect the problem more clearly and outline each step I took to solve it. Hopefully - the more problems I solve and reflect on like this, the better I’ll get at recognizing patterns, abstracting solutions, and tackling more challenges in the future; even if they are a little more difficult than previous ones that I have worked with.
I don't wanna be like this guy; thinking about pieces that don't have any purpose and won't be of any use to me while I am trying to solve a problem.
👋 Final Words
Sanity is an extremely powerful and versatile tool, especially in the context of a Headless CMS. It was made in 2018 to work as an API-first content platform to support all kinds of front-end applications on all kinds of operating systems and devices.
Needing a little bit of extra care when working with an SSR framework like Next.js is kind of expected honestly, and in no way is a deal breaker for me; it’s totally understandable. I will be using Sanity as my go-to Headless CMS solution anytime I need one, and I would recommend it to anyone looking for one as well.
If you want to read about a few of the differences between a traditional and headless content management system, I wrote a bit about it in this blog post along with some more info about Sanity.
That article is about the entire Tsundoku project and is a good resource if you want to learn more about full-stack JavaScript/TypeScript development or Next.js as well. Thanks for reading - hope it was worth your time!
When I have a hard time thinking of an image to use - I just use coffee. Coffee is always appropriate.
⚠️Disclaimer⚠️
Given the time and effort I’ve invested in building this studio and refactoring the blog, I’ve made the repositories for these projects private. They now represent part of my personal intellectual property—which is why I’ve intentionally obscured implementation details in screenshots and chosen my wording carefully throughout this article.
That said, I’m always happy to share code or dive into the details with anyone genuinely interested in working together 🤭
If you'd like to reach out, LinkedIn is probably the quickest way (though I don’t check it daily). You can also contact me through the forms on either my blog or portfolio site. Thanks again for reading and taking an interest!