Astro / Sanity Tutorials ⭐️ November 02, 2024

Sanity & Astro Visual Editor Tutorial

Sanity Astro Visual Editor YouTube Thumbnail

Introduction

If you're a developer building websites for your clients, especially Astro websites using Sanity as the content management system then you will know as well as I do that the only real way to learn how to use this stuff is to dive deep in to article rabbit holes, reading and reading, jumping from tab to tab, article to article trying to piece together what you need.

At Island Web Design we build over 10 websites a month for our clients using Sanity and Astro and we keep up to date with all the latest happenings in the Astro and Sanity eco system and to save you the time and effort of having to dive deep into articles, I have put together this tutorial to show you exactly how to implement visual editing in your Astro Sanity website so your clients can click to edit any part of the site just like a real website builder.

I don't know if you've heard the Wordpress news but it seems they might be going out of business anyway 😳

So in this article I am going to show you how to implement full visual editing functionality in your Astro and Sanity website using the Sanity Presentation Tool and the Astro Netlify Adapter so your clients can easily update and manage their website themselves and not have to pay someone each time they make an update or so your team can work on your content without requiring a developer.

I will, show you how to make your site SSR (Server Side Rendered) using the Astro Netlify Adapter then I will show you how to use a load query function along with the Sanity Presentation tool to create a preview link for your site, showing only draft content so your client can preview and make changes, and test them, without publishing to their production link.

There are loads of different articles about this kind of stuff online but there is no clear method, no straight forward approach, so I have made one, I hope everyone finds this helpful and before we start, remember, if you want all this free value + more in your inbox each week then you can sign up for our free newsletter on our website.

Setting up your project

I am going to assume you already have your Astro Sanity set up, if you want to know how we fully set up these Astro Sanity projects, and how we build out the site to the point you see us starting at here then you can check that video out on our channel, I show you how we set up the Sanity Studio, how we set up the Astro project and how we render all the content in to our front end: Full video tutorial on our YouTube channel.

The idea of this blog is for you to have it as a guide as you follow the video.

Start your servers

Start by typing cd in your terminal and navigating into your project folder.

cd web

Now start your Astro server

npm run dev

Next open a new tab in your terminal and cd in to your studio folder.

cd studio

You can start that server as well.

npm run dev

Add Server Side Rendering To Your Astro Site

The first thing we need to do is make our site server side rendered, Astro is traditionally for building static sites, but if we build a static site, we will need a 40 second build step every time we make changes in our Sanity Studio and want to deploy them to our site, the only way to make the updates between Sanity and Astro instant is to make your site server rendered.

Astro and Netlify literally have a package to help you:

npx astro add netlify

The package will add the correct code in your Astro Config file for you, you can find the full set up guide here: Astro x Netlify SSR Adapter

Your Astro Config file should look like the one below:

astro.config.mjs
import { fileURLToPath } from 'url'
import path, { dirname } from 'path'
import { defineConfig } from 'astro/config'
import sanityIntegration from '@sanity/astro'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
import netlify from '@astrojs/netlify'
import react from '@astrojs/react'

// https://astro.build/config
export default defineConfig({
	vite: {
		resolve: {
			alias: {
				'@/': `${path.resolve(__dirname, 'src')}/`
			}
		},
		css: {
			preprocessorOptions: {
				scss: {
					// path to your scss variables
					additionalData: `@import "@/styles/variables.scss";`
				}
			}
		}
	},
	image: {
		remotePatterns: [
			{
				protocol: 'https'
			}
		]
	},
	integrations: [
		sanityIntegration({
			projectId: 'YOURPROJECTID',
			dataset: 'production',
			apiVersion: '2023-02-08',
			// perspective: 'published',
			useCdn: false,
			stega: {
				studioUrl: 'STUDIO URL'
			}
		}),
		react()
	],
	output: 'server',
	adapter: netlify()
})

Add React To Your Astro Site

Next you need to add React to your project (I am going to assume you haven't already done this)

npx astro add react

Again the package should change your Astro Config file for you, it should look like the one below:

astro.config.mjs
import { fileURLToPath } from 'url'
import path, { dirname } from 'path'
import { defineConfig } from 'astro/config'
import sanityIntegration from '@sanity/astro'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
import netlify from '@astrojs/netlify'
import react from '@astrojs/react'

// https://astro.build/config
export default defineConfig({
	vite: {
		resolve: {
			alias: {
				'@/': `${path.resolve(__dirname, 'src')}/`
			}
		},
		css: {
			preprocessorOptions: {
				scss: {
					// path to your scss variables
					additionalData: `@import "@/styles/variables.scss";`
				}
			}
		}
	},
	image: {
		remotePatterns: [
			{
				protocol: 'https'
			}
		]
	},
	integrations: [
		sanityIntegration({
			projectId: 'YOURPROJECTID',
			dataset: 'production',
			apiVersion: '2023-02-08',
			// perspective: 'published',
			useCdn: false,
			stega: {
				studioUrl: 'STUDIO URL'
			}
		}),
		react()
	],
	output: 'server',
	adapter: netlify()
})

You won't see any visual differences in your site but as long as your server starts you can be sure it's all working.

Add Presentation Tool To Your Sanity Config

Now that's all done you can import your Presentation Tool in your Sanity config file, the Presentation Tool comes with the Sanity Package so you don't need to install it

import {presentationTool} from 'sanity/presentation'

Once you have imported your presentation tool you need to add it to your plugins array, above your structure tool, and add your preview url along with the preview query string.

Define your preview url

First at the top you need to define your preview url

const SANITY_STUDIO_PREVIEW_URL = process.env.SANITY_STUDIO_PREVIEW_URL || 'http://localhost:4321/'

Once you have defined your preview url (we will set up our env variables soon) you can add it to your presentation tool config like in the screenshot below, make sure you use back ticks and template strings.

sanity.config.js
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {schemaTypes} from './schemaTypes'
import {deskStructure} from './components/deskStructure'
import {codeInput} from '@sanity/code-input'
import {media} from 'sanity-plugin-media'
import {simplerColorInput} from 'sanity-plugin-simpler-color-input'

const SANITY_STUDIO_PREVIEW_URL = process.env.SANITY_STUDIO_PREVIEW_URL || 'http://localhost4321/'

export default defineConfig({
  name: 'default',
  title: 'island-studio',
  projectId: 'YOURPROJECTID',
  dataset: 'production',
  plugins: [

    structureTool({structure: deskStructure}),
    codeInput(),
    media(),
    simplerColorInput(),
  ],

  schema: {
    types: schemaTypes,
  },
})

Now you can add your preview query string to the url, this is how we will be able to tell whether the site is being viewed in preview mode or not, we check the url on each request to see if it contains the query string, and if it does we enable visual editing.

sanity.config.js
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
import {schemaTypes} from './schemaTypes'
import {deskStructure} from './components/deskStructure'
import {codeInput} from '@sanity/code-input'
import {media} from 'sanity-plugin-media'
import {simplerColorInput} from 'sanity-plugin-simpler-color-input'
import {presentationTool} from 'sanity/presentation'

const SANITY_STUDIO_PREVIEW_URL = process.env.SANITY_STUDIO_PREVIEW_URL || 'http://localhost:4321/'

export default defineConfig({
  name: 'default',
  title: 'island-studio',
  projectId: 'YOURPROJECTID',
  dataset: 'production',
  plugins: [
    presentationTool({previewUrl: `${SANITY_STUDIO_PREVIEW_URL}?preview=true`}),
    structureTool({structure: deskStructure}),
    codeInput(),
    media(),
    simplerColorInput(),
  ],

  schema: {
    types: schemaTypes,
  },
})

Now if you go back to your Sanity Studio and restart the server you will see your presentation tool tab, I change the title of my clients tabs, yours will say 'Presentation'.

test alt

Bonus tip: Here's how you can change the title of your presentation tool

sanity.config.js
 presentationTool({
      title: 'Visual Editor',
      icon: '🎨',
      previewUrl: `${SANITY_STUDIO_PREVIEW_URL}?preview=true`,
    }),

Creating a load-query

Next we need to create a loader, this is basically a Javascript file that will query our Sanity client and pull our content in either draft mode or production mode depending on whether visual editing is enabled or not (based on our query string), the loader will handle all of your visual editing functionality, including overlays (which are the blue outlines you see when you hover on an element in preview mode).

If you want to hear me talk more about what's happening here you can see that in the full tutorial video on our YouTube channel but basically, when you make a change in Sanity, but you don't hit publish, that's draft content, it becomes live content when you publish it, we want to be able to view the draft content in the visual editor so we can preview our site before we publish it, this is what the loader does, you then pass in true or false for 'enabled' to turn it on or off.

load-query.js
// src/lib/load-query.js
import { sanityClient } from 'sanity:client'

export async function loadQuery({ query, params, enabled }) {
	const visualEditingEnabled = import.meta.env.PUBLIC_SANITY_VISUAL_EDITING_ENABLED === enabled

	const token = import.meta.env.SANITY_API_READ_TOKEN
	if (visualEditingEnabled && !token) {
		throw new Error(
			'The `SANITY_API_READ_TOKEN` environment variable is required during Visual Editing.'
		)
	}
	const perspective = visualEditingEnabled ? 'previewDrafts' : 'published'
	const { result, resultSourceMap } = await sanityClient.fetch(query, params ?? {}, {
		filterResponse: false,
		perspective,
		resultSourceMap: visualEditingEnabled ? 'withKeyArraySelector' : false,
		stega: visualEditingEnabled,
		...(visualEditingEnabled ? { token } : {}),
		useCdn: !visualEditingEnabled
	})
	return {
		data: result,
		sourceMap: resultSourceMap,
		perspective
	}
}

As long as you make sure you fetch any query you want to use visual editing for, using the load query function rather than a normal getSanity api call. More information on loaders.

Using getCleanClassName to clean Stega strings

Next, we need a getCleanClassName.js file, the way that the overlays in the visual editor works is by adding invisible strings to your html elements, and these can interfere with your class names, making all your styles go messed up, this code will help with that. Here is what your getCleanClassName.js file should look like.

You just need to import the stegaClean function from the Sanity Client

import {stegaClean} from '@sanity/client/stega'

Then you can export and define your function and return statement as follows

export function getCleanClassName(element) {
return stegaClean(element)
}
getCleanClassName.js
import { stegaClean } from '@sanity/client/stega'

export function getCleanClassName(element) {
	return stegaClean(element)
}

Getting your Sanity Api Key

Next step is to get your api key, for this you can go to https://sanity.io/manage/ and go into your project settings, you can get your key from the API tab.

test alt

Create your .env file

Now you need somewhere to keep your api key, create a .env file at the root of your project and add all the following variables, you will also need to add these to Netlify later. You should have your SANITY_API_READ_TOKEN, PUBLIC_SANITY_VISUAL_EDITING_ENABLED, SANITY_STUDIO_PREVIEW_URL

test alt

Enabling visual editing and draft mode

Next is the enabled code, this is the code we will have at the top of each of our pages in Astro that will check to see if the url of the page contains the preview string or not, if it does it will pull draft content instead of published content.

Add the following code to the top of each of your Astro pages in your front matter, below your initial import statements

let enabled
const referrer = Astro.url.href
if (referrer.includes('preview')) {enabled = 'true'} else {enabled = 'false'}

Next we need to change all our queries to use our load-query function. Below is how your new query for your home page should look, although this can be applied to any query on your site.

index.astro
const { data: home } = await loadQuery({
	query: `*[_type == 'home'] {
    ...,
	header-> {
		...,
		"links": links[] {
       		...,
       	"link": link-> {
       	    ...
       	},
       	...@-> {
       	    ...,
       	},
   },
	},
	offer-> {
	    ...,
		button->,
		features[]->,
		videoSalesLetter-> {
		...,
        videoBlock[] {
			...,
			file {
				asset-> {
					url
				},
			},
		},
		form
		
    },
	
}`,
	params: {},
	enabled: enabled
})

As you can see we pass our query as the first argument, then we pass our params which are empty and then we pass whether visual editing is enabled or not from our enabled code we added at the top of each page. You can name your query in the 'data' prop.

Add Stega to Astro Config

Next we need to add 'stega' to our Astro config, as I said in the YouTube video, I'm not 100% sure what this does, but it's necessary.

astro.config.mjs
sanityIntegration({
			projectId: 'YOURPROJECTID',
			dataset: 'production',
			apiVersion: '2023-02-08',
			useCdn: false,
			stega: {
				studioUrl: 'http://localhost:3333/'
			}
		}),

Your stega url should be the url of your deployed studio.

Add Visual Editing to your Layout.astro

Once this is done you need to actually add the visual editing component to your Layout.astro file so we can have our overlays and everything else on our page.

Layout.astro
<body>
		<Header links={links} logo={headerLogo} />
		<slot />
		<Footer
			contactInformation={contactInformation}
			title={footerCta[0].title}
			button={footerCta[0].button}
			text={footerCta[0].text}
			image={footerCta[0].image}
			backgroundColor={footerCta[0].backgroundColor}
			links={footer[0].links}
		/>
		<VisualEditing enabled={visualEditingEnabled} zIndex={1000} />
	</body>

You can pass in whether or not it is enabled by adding this line at the top of your layout in the front matter.

const visualEditingEnabled = import.meta.env.PUBLIC_SANITY_VISUAL_EDITING_ENABLED == 'true'

You also need to pass the zIndex so it sits on top of everything else.

Make sure you also import it

import {VisualEditing} from '@sanity/astro/visual-editing'

Important - fixing Sanity integration

At this point I had to comment out my Sanity Integration in my Astro config file, along with the import statement for it and install it again

npx astro add @sanity/astro

After this you need to change the integrations array because the package adds it to your array as sanity() but it should be sanityIntegration() you can just copy and paste back in the same config you had before.

MAKE SURE YOUR IMPORT IS NOT A NAMED IMPORT, DO NOT USE CURLY BRACKETS IN YOUR IMPORT STATEMENT, THIS GETS ME EVERY TIME, HERE IS HOW YOUR ASTRO CONFIG SHOULD NOW LOOK

astro.config.mjs
import { fileURLToPath } from 'url'
import path, { dirname } from 'path'
import { defineConfig } from 'astro/config'
import sanityIntegration from '@sanity/astro'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
import netlify from '@astrojs/netlify'
import react from '@astrojs/react'

// https://astro.build/config
export default defineConfig({
	vite: {
		resolve: {
			alias: {
				'@/': `${path.resolve(__dirname, 'src')}/`
			}
		},
		css: {
			preprocessorOptions: {
				scss: {
					// path to your scss variables
					additionalData: `@import "@/styles/variables.scss";`
				}
			}
		}
	},
	image: {
		remotePatterns: [
			{
				protocol: 'https'
			}
		]
	},
	integrations: [
		sanityIntegration({
			projectId: 'YOURPROJECTID',
			dataset: 'production',
			apiVersion: '2023-02-08',
			// perspective: 'published',
			useCdn: false,
			stega: {
				studioUrl: 'STUDIOURL'
			}
		}),
		react()
	],
	output: 'server',
	adapter: netlify()
})

Once you have reinstalled the @sanity/astro package and re added it to your integrations array and made sure it is not a named import, you can start your server and your visual editing will work.

Fixing broken styles

You'll now notice that some of your styles are messed up in the presentation tool, mainly anything where the classes are set with Javascript, you can use your getCleanClassName function to fix your classes just like I do for my button here.

Button.astro
<a href={to} target={target}>
	<button class={getCleanClassName(variant)}>
		{buttonText}
	</button>
</a>

Once you have added the function to any classes that aren't working, everything should work, well everything except navigating between pages with the visual editor.

You can now change any other queries in your site to use the load-query function.

Navigating between pages in the Visual Editor

You need to add a piece of code to each page of your site that will append the query string to all of your links when visual editing is enabled

index.astro
<script>
	// Function to append preview parameter to URLs
	function appendPreviewToLinks() {
		const links = document.querySelectorAll('a')
		const urlParams = new URLSearchParams(window.location.search)
		if (urlParams.has('preview')) {
			links.forEach((link) => {
				const url = new URL(link.href)
				url.searchParams.set('preview', 'true')
				link.href = url.toString()
			})
		}
	}

	// Call the function to update links after the page loads
	if (typeof window !== 'undefined') {
		window.addEventListener('load', appendPreviewToLinks)
	}
</script>

As soon as you put this on each page of your site you will be able to navigate between pages with visual editing enabled, just switch off edit mode with the toggle at the top of the visual editor to use the links, if you don't turn this off the links will be click to edit rather than actual links that take you somewhere.

Adding .env variables to Netlify

Lastly, when publishing this site to Netlify you need to make sure that you add all your env variables in Netlify, you can do this in your build settings, you also need to change out any localhost urls for the actual urls or it won't work. How to add env variables in Netlify.

Deploy your Sanity Studio

Make sure you remember to deploy your Sanity Studio

sanity deploy

Now you can test your live site with visual editing enabled.