Sanity CMS
Sanity is the CMS powering the Dyson Inspired blog template. It treats content as structured data with a real-time collaborative Studio, GROQ query language, and flexible schemas. Use it to manage blog posts, authors, and media without touching code. Your editors will thank you.
Last updated: 2026-03-29
CMS Managed Content
The following content types are fully managed through Sanity Studio. No code changes required. Editors can update these directly from the dashboard.
| Content | CMS Managed |
|---|---|
| Products (title, price, images, stock) | ✓ Yes |
| Blog posts | ✓ Yes |
| FAQs | ✓ Yes |
| Banner / announcements | ✓ Yes |
| Team members | ✓ Yes |
Setting Up Sanity
Sanity separates content management (Studio) from content delivery (your frontend). Start by creating a Sanity project, which generates a project ID and dataset. Install the Sanity client to fetch content in your Astro application. The Studio can be hosted separately or embedded in your project.
// Install Sanity CLI and client
npm install -g @sanity/cli
npm install @sanity/client @sanity/image-url
// Initialize Sanity project (creates studio)
sanity init
// Configure Sanity client
// src/lib/sanity.js
import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
export const client = createClient({
projectId: 'your-project-id', // Find in sanity.json
dataset: 'production',
useCdn: true, // Enable CDN for faster reads
apiVersion: '2024-01-01',
token: import.meta.env.SANITY_API_TOKEN // Optional: for authenticated requests
});
// Helper for optimized image URLs
const builder = imageUrlBuilder(client);
export function urlFor(source) {
return builder.image(source);
}
// Example: Fetch all blog posts
export async function getPosts() {
return await client.fetch(`
*[_type == "post"] | order(publishedAt desc) {
_id,
title,
slug,
publishedAt,
excerpt,
mainImage,
author->{
name,
image
}
}
`);
}Defining Content Schemas
Schemas define the structure of your content. Create document types for posts, pages, products, or any custom content. Sanity provides field types for text, numbers, images, references, arrays, and more. Schemas enable Studio's editor interface and validate content structure.
// Define a blog post schema
// schemas/post.js
export default {
name: 'post',
title: 'Blog Post',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
validation: Rule => Rule.required().max(100)
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96
},
validation: Rule => Rule.required()
},
{
name: 'publishedAt',
title: 'Published Date',
type: 'datetime',
initialValue: () => new Date().toISOString()
},
{
name: 'excerpt',
title: 'Excerpt',
type: 'text',
rows: 4,
validation: Rule => Rule.max(200)
},
{
name: 'mainImage',
title: 'Main Image',
type: 'image',
options: {
hotspot: true // Enable crop/hotspot
},
fields: [
{
name: 'alt',
title: 'Alt Text',
type: 'string',
validation: Rule => Rule.required()
}
]
},
{
name: 'body',
title: 'Body Content',
type: 'array',
of: [
{
type: 'block',
styles: [
{ title: 'Normal', value: 'normal' },
{ title: 'H2', value: 'h2' },
{ title: 'H3', value: 'h3' },
{ title: 'Quote', value: 'blockquote' }
],
marks: {
decorators: [
{ title: 'Strong', value: 'strong' },
{ title: 'Emphasis', value: 'em' },
{ title: 'Code', value: 'code' }
]
}
},
{
type: 'image',
options: { hotspot: true }
}
]
},
{
name: 'author',
title: 'Author',
type: 'reference',
to: [{ type: 'author' }]
},
{
name: 'categories',
title: 'Categories',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'category' }] }]
}
],
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage'
},
prepare({ title, author, media }) {
return {
title,
subtitle: author && `by ${author}`,
media
};
}
}
};Querying with GROQ
GROQ (Graph-Relational Object Queries) is Sanity's powerful query language designed for content. It lets you filter, sort, project fields, and traverse references with intuitive syntax. GROQ queries are expressive and return exactly the data you need.
// Fetch all published posts
const posts = await client.fetch(`
*[_type == "post" && publishedAt < now()] | order(publishedAt desc)
`);
// Fetch single post by slug with author details
const post = await client.fetch(`
*[_type == "post" && slug.current == $slug][0] {
title,
publishedAt,
excerpt,
mainImage,
body,
author->{
name,
bio,
image
},
categories[]->{
title,
slug
}
}
`, { slug: 'my-post-slug' });
// Search posts by keyword
const searchResults = await client.fetch(`
*[_type == "post" && [title, excerpt] match $keyword] {
title,
slug,
excerpt
}
`, { keyword: 'javascript*' });
// Fetch posts with pagination
const page = 1;
const perPage = 10;
const start = (page - 1) * perPage;
const end = start + perPage;
const paginatedPosts = await client.fetch(`
*[_type == "post"] | order(publishedAt desc) [$start...$end] {
_id,
title,
slug,
excerpt,
mainImage
}
`, { start, end });
// Count total posts for pagination
const totalPosts = await client.fetch(`
count(*[_type == "post"])
`);
// Fetch related posts by category
const relatedPosts = await client.fetch(`
*[_type == "post" && references($categoryId) && _id != $currentPostId] [0...3] {
title,
slug,
excerpt
}
`, { categoryId, currentPostId });Integrating with Astro
Fetch Sanity content at build time for static pages or at request time for dynamic content. Use Astro's getStaticPaths for generating pages from CMS data. Optimize images with Sanity's image pipeline for responsive, performant delivery.
// Blog listing page
// src/pages/blog.astro
---
import { getPosts, urlFor } from '../lib/sanity';
import Layout from '../layouts/Layout.astro';
const posts = await getPosts();
---
<Layout title="Blog">
<div class="max-w-4xl mx-auto px-4 py-12">
<h1 class="text-4xl font-bold mb-8">Blog Posts</h1>
<div class="grid gap-8">
{posts.map(post => (
<article class="border rounded-lg overflow-hidden hover:shadow-lg transition">
{post.mainImage && (
<img
src={urlFor(post.mainImage)
.width(800)
.height(400)
.fit('crop')
.auto('format')
.url()}
alt={post.mainImage.alt || post.title}
class="w-full h-64 object-cover"
/>
)}
<div class="p-6">
<h2 class="text-2xl font-semibold mb-2">
<a href={`/blog/${post.slug.current}`} class="hover:text-blue-600">
{post.title}
</a>
</h2>
<time class="text-sm text-gray-500 block mb-4">
{new Date(post.publishedAt).toLocaleDateString()}
</time>
<p class="text-gray-700 mb-4">{post.excerpt}</p>
{post.author && (
<div class="flex items-center gap-2">
{post.author.image && (
<img
src={urlFor(post.author.image).width(40).height(40).url()}
alt={post.author.name}
class="w-10 h-10 rounded-full"
/>
)}
<span class="text-sm text-gray-600">{post.author.name}</span>
</div>
)}
</div>
</article>
))}
</div>
</div>
</Layout>
// Dynamic blog post pages
// src/pages/blog/[slug].astro
---
import { client, urlFor } from '../../lib/sanity';
import Layout from '../../layouts/Layout.astro';
import PortableText from '../../components/PortableText.astro';
export async function getStaticPaths() {
const posts = await client.fetch(`
*[_type == "post"] { slug }
`);
return posts.map(post => ({
params: { slug: post.slug.current }
}));
}
const { slug } = Astro.params;
const post = await client.fetch(`
*[_type == "post" && slug.current == $slug][0] {
title,
publishedAt,
mainImage,
body,
author->{ name, image, bio }
}
`, { slug });
if (!post) {
return Astro.redirect('/404');
}
---
<Layout title={post.title}>
<article class="max-w-3xl mx-auto px-4 py-12">
<h1 class="text-5xl font-bold mb-4">{post.title}</h1>
<time class="block text-gray-600 mb-8">
{new Date(post.publishedAt).toLocaleDateString()}
</time>
{post.mainImage && (
<img
src={urlFor(post.mainImage).width(1200).url()}
alt={post.title}
class="w-full rounded-lg mb-8"
/>
)}
<div class="prose prose-lg max-w-none">
<PortableText value={post.body} />
</div>
</article>
</Layout>