How to use Netlify Visual Editor with Astro

by Tomas Bankauskas

The Netlify Visual Editor is a user-friendly tool for editing structured visual content. It’s compatible with various frameworks and Headless CMS, enabling real-time editing without requiring extensive technical knowledge. Learn how to use it with the Astro web framework.

#TL;DR

To explore Netlify Visual Editor’s capabilities, we will set up and customize a blog using an Astro template. Enabling visual editing allows content editors to work with structured content in a visual way.

Explore the example site and repo

Preview the deployed site and reference the repository for the code via the following links:

Also, you can follow the steps below to create this example from the beginning.

#Prerequisites

To follow along with all of the steps of this guide, you’ll need:

#Tech stack overview

  • Astro as the site framework
  • File-based content (Git CMS)
  • Netlify Visual Editor

Netlify Visual Editor is compatible with various web frameworks, such as Next.js, Nuxt, and 11ty. We’re using Astro as an example.

#Steps required to add Visual Editor

These steps are required for optimal Visual Editor experience.

  1. Install Visual Editor development dependencies
  2. Create Visual Editor config (stackbit.config.ts)
  3. Create content models to define the structure of the content
  4. Annotate project components for inline editing

#Getting started with Astro

For this guide, we will be using the Astro Starter Kit: Blog. This template includes several hardcoded pages, such as Home and About, as well as a collection of blog posts. Our goal is to make the blog posts editable using the Visual Editor. To get started with the Astro Starter Kit Blog, run the following command:

Terminal window
npm create astro@latest -- --template blog
Create an Astro project with TypeScript Strict parameters.

#Adjust the template

By default, Astro uses port 4321 for its development server, while Visual Editor operates on port 3000. To change the port for Astro, add the following code to your astro.config.mjs file:

export default {
server: {
port: 3000,
},
};

As we are working with structured content (post collection), the hardcoded pages are unnecessary to include. Please delete the about.astro and index.astro in the pages directory. Move index.astro from the blog directory to the pages directory, and adjust the components paths. This way, our project structure aligns with the example repository, and we only have pages that can be edited in the Visual Editor.

#Install Netlify Visual Editor CLI

Run the following command to install the Netlify Visual Editor CLI globally using npm:

Terminal window
npm install -g @stackbit/cli

#Installing required dependencies for Visual Editor

Once you have the template in your local environment, let’s install the necessary dependencies for the Netlify Visual Editor. The Astro Starter Kit Blog template uses markdown files to store blog post content (Git CMS). To enable the Visual Editor to work with file-based content, we need to install the following development dependencies:

Terminal window
npm install -D @stackbit/types @stackbit/cms-git

#Create Visual Editor config

The next step is to add the Netlify Visual Editor config. Create a file at the root of your project named stackbit.config.ts and add the following code:

stackbit.config.ts
import { defineStackbitConfig } from "@stackbit/types";
import { GitContentSource } from "@stackbit/cms-git";
export default defineStackbitConfig({
stackbitVersion: "~0.6.0",
ssgName: "custom",
nodeVersion: "18",
devCommand: "node_modules/.bin/astro dev --port {PORT} --hostname 127.0.0.1",
experimental: {
ssg: {
name: "Astro",
logPatterns: {
up: ["is ready", "astro"],
},
directRoutes: {
"socket.io": "socket.io",
},
passthrough: ["/vite-hmr/**"],
},
},
contentSources: [
new GitContentSource({
rootPath: __dirname,
contentDirs: ["src/content/blog"],
models: [],
assetsConfig: {
referenceType: "static",
staticDir: "src/content",
uploadDir: "_images",
publicPath: "/src/content/",
},
}),
],
});

#Netlify Visual Editor config explained

Let’s break down each section of the Visual Editor configuration.

#Imports

import { defineStackbitConfig } from "@stackbit/types";
import { GitContentSource } from "@stackbit/cms-git";
  • defineStackbitConfig: this function is used to define the Netlify Visual Editor configuration. It ensures the configuration follows Visual Editor’s type structure.
  • GitContentSource: this import from @stackbit/cms-git enables using a Git-based content source. This way, Netlify Visual Editor can read content directly from files in the project’s repository.

#Configuration details

export default defineStackbitConfig({
stackbitVersion: "~0.6.0",
ssgName: "custom",
nodeVersion: "18",
devCommand: "node_modules/.bin/astro dev --port {PORT} --hostname 127.0.0.1",
});
  • stackbitVersion: specifies the Visual Editor version.
  • ssgName: allows Visual Editor to recognize the web framework used in your project. We set it to “custom” since Astro currently isn’t natively supported as a standard SSG in Visual Editor.
  • nodeVersion: specifies the Node.js version.
  • devCommand: this command (astro dev) runs the development server, which is essential for Visual Editor’s live preview feature. PORT is dynamically replaced with the correct port Visual Editor uses.

#Experimental section

experimental: {
ssg: {
name: "Astro",
logPatterns: {
up: ["is ready", "astro"],
},
directRoutes: {
"socket.io": "socket.io",
},
passthrough: ["/vite-hmr/**"],
},
},

This section enables Visual Editor’s experimental support for Astro:

  • name: specifies Astro as the SSG we are using.
  • logPatterns: helps Visual Editor to recognize when the server is ready by checking for specific log patterns in the console output, like “is ready” and “astro”.
  • directRoutes: configures Visual Editor to route socket.io traffic correctly.
  • passthrough: specifies paths (/vite-hmr/**) to bypass Visual Editor’s routing and allow direct access, particularly for Hot Module Replacement (HMR) when using Vite.

#Content sources

contentSources: [
new GitContentSource({
rootPath: __dirname,
contentDirs: ["src/content/blog"],
models: [],
assetsConfig: {
referenceType: "static",
staticDir: "src/content",
uploadDir: "_images",
publicPath: "/src/content/",
},
}),
];
  • GitContentSource: defines a content source.
    • rootPath: sets the root path of the content to the current directory (__dirname).
    • contentDirs: specifies where the content is located.
    • models: the array of the content model definitions. This is currently empty, but we will update it later.
    • assetsConfig: configures asset handling:
      • referenceType: set to “static”, meaning images and other assets are stored statically.
      • staticDir: sets the directory for static content as src/content.
      • uploadDir: specifies _images as the directory where assets are uploaded.
      • publicPath: the public path to access static content, set to /src/content/.

#Run Visual Editor locally

To start the Visual Editor locally, run the Astro development server by using the command npm run dev. In a new terminal window, run the Visual Editor with the command stackbit dev. This provides you with your own Netlify Visual Editor URL. To be directed to Netlify’s Visual Editor for your new project, open the URL and register or sign in.

#Creating post content model

The next step is to create a content model. The Astro blog kit has a content collection for blog posts, which means we need to create a content model specifically for posts. To ensure a post is editable in the Visual Editor, the post content must be represented as a structured data object sourced from a content source.

We define the post content model within the models array in the Visual Editor’s configuration. For this, we create an object named post with the type set to page and specify the urlPath and filePath properties. urlPath represents the blog post URL path, and filePath indicates where new posts will be saved.

stackbit.config.ts
/* ... */
export default defineStackbitConfig({
/* ... */
contentSources: [
new GitContentSource({
rootPath: __dirname,
contentDirs: ["src/content/blog"],
models: [{
name: "post",
type: "page",
urlPath: "/blog/{slug}",
filePath: "src/content/blog/{slug}.md",
};],
assetsConfig: {
referenceType: "static",
staticDir: "src/content",
uploadDir: "_images",
publicPath: "/src/content/",
},
}),
],
});

To ensure that the Visual Editor recognizes which content files correspond to the post model, we add type: post to the front matter of all existing post files located in src/content/blog.

Now, if you run the Visual Editor locally, you should see the posts appear in the top navigator and in the sidebar.

Posts in top navigator

#Post fields property

Netlify Visual Editor Reference

In the Visual Editor reference documentation, you can find all properties available within a model definition.

The next step is to make the post front matter editable in the Visual Editor. To achieve this, we need to define the fields property in our post model. Let’s start by reviewing the blog post collection schema definition:

src/content/config.ts
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
}),
});

Ensure that the fields defined in the schema are listed as objects in the fields property of the post model. Let’s start with the post title.

The post title is a simple text field, so we set its type to string. The name of the field should match the one defined in the collection schema, so we set it to title. Since the post title is required, we include the required property and set it to true. To enhance the user experience, we can also provide an initial value by adding the default property and setting it to “Post Title”:

stackbit.config.ts
/* ... */
export default defineStackbitConfig({
/* ... */
contentSources: [
new GitContentSource({
/* ... */
models: [{
name: "post",
type: "page",
urlPath: "/blog/{slug}",
filePath: "src/content/blog/{slug}.md",
fields: [
{ name: "title", type: "string", required: true, default: "Post Title" },
],
};],
/* ... */
}),
],
});

Similarly, we define the remaining fields: description, pubDate, updatedDate, and heroImage:

stackbit.config.ts
/* ... */
export default defineStackbitConfig({
/* ... */
contentSources: [
new GitContentSource({
/* ... */
models: [{
name: "post",
type: "page",
urlPath: "/blog/{slug}",
filePath: "src/content/blog/{slug}.md",
fields: [
{ name: "title", type: "string", required: true, default: "Post Title" },
{ name: "pubDate", type: "date", required: true },
{ name: "updatedDate", type: "date" },
{ name: "description", type: "string" },
{ name: "heroImage", type: "string" },
],
};],
/* ... */
}),
],
});

After these edits, you should see all these fields on post page in the Visual Editor sidebar.

Visual Editor single post sidebar

#Annotations

The Visual Editor allows users to make changes directly in the preview by clicking on editable content. To enable inline editing in a post, we need to add annotations to our project components: src/pages/index.astro, src/pages/blog/[...slug].astro, and src/layouts/BlogPost.astro.

Annotations are the data-sb-* data attributes that inform the Visual Editor how to link the content in the source with its corresponding location in the rendered page structure within the project preview. The two available data attributes are:

  • data-sb-object-id: scopes all descendant elements within the context of the given ID value, so that any field path mentioned in HTML descendants is automatically attributed to the document with the specified ID.
  • data-sb-field-path: provides a path (absolute or relative) from the root of the document, identifying the field to be edited in the content source.

To start, let’s add annotations to the posts on the blog feed page located at src/pages/index.astro.

#Annotating blog feed posts

We begin by adding the data-sb-object-id attribute to the post wrapper element, which in this case is the <a> tag. The value of the data-sb-object-id attribute corresponds to the path of the post file, relative to the root of the project, and including the file extension. We construct it using the Astro entry id, as follows: src/content/blog/${post.id}

Next, we add data-sb-field-path attributes to the elements responsible for rendering the post’s heroImage, title, and pubDate:

index.astro
<li>
<a href={`/blog/${post.slug}/`} data-sb-object-id={`src/content/blog/${post.id}`}>
<img width={720} height={360} src={post.data.heroImage} alt="" data-sb-field-path="heroImage" />
<h4 class="title" data-sb-field-path="title">
{post.data.title}
</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} data-sb-field-path="pubDate" />
</p>
</a>
</li>

Now, let’s add annotations to a single blog post in pages/blog/[...slug].astro.

#Annotating blog post

The blog post is rendered using the BlogPost component, which only receives post front matter properties {...post.data}. To construct the data-sb-object-id attribute, we also need to pass the Astro entry id:

<BlogPost {...post.data} id={post.id} />

Next, let’s modify the BlogPost component located at src/layouts/BlogPost.astro. First, we update the TypeScript Props to include the id property and extract the id property from the received props:

type Props = CollectionEntry<"blog">["data"] & {
id: CollectionEntry<"blog">["id"];
};
const { id, title, description, pubDate, updatedDate, heroImage } = Astro.props;

Afterward, we can add the data-sb-object-id and data-sb-field-path attributes, following the same approach as we did for the blog feed posts.

<article data-sb-object-id={`src/content/blog/${id}`}>
<div class="hero-image">
{heroImage && <img width={1020} height={510} src={heroImage} alt="" data-sb-field-path="heroImage" />}
</div>
<div class="prose">
<div class="title">
<div class="date">
<FormattedDate date={pubDate} data-sb-field-path="pubDate" />
{updatedDate && (
<div class="last-updated-on">
Last updated on <FormattedDate date={updatedDate} data-sb-field-path="updatedDate" />
</div>
)}
</div>
<h1 data-sb-field-path="title">{title}</h1>
<hr />
</div>
<div data-sb-field-path="markdown_content">
<slot />
</div>
</div>
</article>

After these edits, you should be able to edit inline all annotated fields on post page in the Visual Editor.

Visual Editor inline editing

#Apply this to your site!

To learn more about the Netlify Visual Editor, explore the documentation or the reference documentation. Try templates with Visual Editor, the Astro Sanity Starter, or the Next.js Content Ops Starter.