🎯 Exemplos recomendados
Balanced sample collections from various categories for you to explore
Exemplos Sanity CMS em Tempo Real
Exemplos abrangentes do Sanity cobrindo modelagem de conteúdo, integração de API e padrões de implantação
💻 Modelagem de Conteúdo Sanity javascript
🟡 intermediate
⭐⭐⭐
Definições completas de content types, configuração de schema e modelagem de documentos para vários casos de uso
⏱️ 35 min
🏷️ sanity, content modeling, cms
Prerequisites:
Sanity basics, JavaScript, Schema concepts, Real-time content management
// Sanity Content Modeling Examples
// File: schemas/blogPost.js
export default {
name: 'blogPost',
title: 'Blog Post',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
validation: (Rule) => Rule.required().min(10).max(100),
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 200,
},
},
{
name: 'excerpt',
title: 'Excerpt',
type: 'text',
rows: 3,
validation: (Rule) => Rule.max(300),
},
{
name: 'content',
title: 'Content',
type: 'array',
of: [
{ type: 'block' },
{ type: 'image', options: { hotspot: true } },
{ type: 'file' },
{ type: 'reference', to: [{ type: 'author' }, { type: 'category' }, { type: 'tag' }] },
],
},
{
name: 'featuredImage',
title: 'Featured Image',
type: 'image',
options: { hotspot: true },
fields: [
{
name: 'alt',
title: 'Alternative Text',
type: 'string',
},
{
name: 'caption',
title: 'Caption',
type: 'text',
},
],
},
{
name: 'gallery',
title: 'Gallery',
type: 'array',
of: [
{
type: 'image',
options: { hotspot: true },
},
],
},
{
name: 'author',
title: 'Author',
type: 'reference',
to: [{ type: 'author' }],
},
{
name: 'category',
title: 'Category',
type: 'reference',
to: [{ type: 'category' }],
},
{
name: 'tags',
title: 'Tags',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'tag' }] }],
},
{
name: 'seo',
title: 'SEO',
type: 'seo',
fields: [
{
name: 'title',
title: 'Meta Title',
type: 'string',
description: 'Recommended: 50-60 characters',
validation: (Rule) => Rule.max(60),
},
{
name: 'description',
title: 'Meta Description',
type: 'text',
rows: 3,
description: 'Recommended: 50-160 characters',
validation: (Rule) => Rule.max(160),
},
{
name: 'keywords',
title: 'Keywords',
type: 'array',
of: [{ type: 'string' }],
},
{
name: 'image',
title: 'Social Share Image',
type: 'image',
description: 'Recommended: 1200x630px',
},
],
},
{
name: 'publishedAt',
title: 'Published Date',
type: 'datetime',
initialValue: () => new Date().toISOString(),
},
{
name: 'readingTime',
title: 'Reading Time',
type: 'number',
description: 'Reading time in minutes',
validation: (Rule) => Rule.min(1).positive(),
},
],
orderings: [
{
name: 'publishedAtDesc',
title: 'Published Date (Newest First)',
by: [{ field: 'publishedAt', direction: 'desc' }],
},
{
name: 'titleAsc',
title: 'Title (A-Z)',
by: [{ field: 'title', direction: 'asc' }],
},
],
preview: {
select: {
title: 'title',
slug: 'slug',
excerpt: 'excerpt',
featuredImage: 'featuredImage',
},
prepare: (selection) => ({
...selection,
url: `/blog/${selection.slug?.current}`,
}),
},
},
};
// File: schemas/product.js
export default {
name: 'product',
title: 'Product',
type: 'document',
fields: [
{
name: 'name',
title: 'Product Name',
type: 'string',
validation: (Rule) => Rule.required(),
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'name',
maxLength: 200,
},
},
{
name: 'description',
title: 'Description',
type: 'text',
rows: 3,
},
{
name: 'shortDescription',
title: 'Short Description',
type: 'text',
rows: 2,
},
{
name: 'sku',
title: 'SKU',
type: 'string',
validation: (Rule) => Rule.required().regex(/^[A-Z0-9-]+$/),
},
{
name: 'price',
title: 'Price',
type: 'number',
validation: (Rule) => Rule.required().positive(),
},
{
name: 'comparePrice',
title: 'Compare Price',
type: 'number',
},
{
name: 'currency',
title: 'Currency',
type: 'string',
options: {
list: [
{ title: 'USD', value: 'USD' },
{ title: 'EUR', value: 'EUR' },
{ title: 'GBP', value: 'GBP' },
{ title: 'JPY', value: 'JPY' },
],
defaultValue: 'USD',
},
},
{
name: 'images',
title: 'Product Images',
type: 'array',
of: [
{
type: 'image',
options: { hotspot: true },
},
],
},
{
name: 'variants',
title: 'Product Variants',
type: 'array',
of: [
{
type: 'object',
fields: [
{
name: 'name',
title: 'Variant Name',
type: 'string',
},
{
name: 'sku',
title: 'SKU',
type: 'string',
},
{
name: 'price',
title: 'Price',
type: 'number',
validation: (Rule) => Rule.positive(),
},
{
name: 'color',
title: 'Color',
type: 'string',
},
{
name: 'size',
title: 'Size',
type: 'string',
},
{
name: 'image',
title: 'Variant Image',
type: 'image',
},
{
name: 'inventory',
title: 'Inventory',
type: 'number',
validation: (Rule) => Rule.min(0),
},
],
},
],
},
{
name: 'category',
title: 'Category',
type: 'reference',
to: [{ type: 'category' }],
validation: (Rule) => Rule.required(),
},
{
'name': 'tags',
'title': 'Tags',
'type': 'array',
'of': [{ 'type': 'reference', to: [{ 'type': 'tag' }] }],
},
{
name: 'attributes',
title: 'Product Attributes',
type: 'array',
of: [
{
type: 'object',
fields: [
{
name: 'name',
title: 'Attribute Name',
type: 'string',
},
{
name: 'value',
title: 'Attribute Value',
type: 'string',
},
],
},
],
},
{
name: 'inventory',
title: 'Inventory Information',
type: 'object',
fields: [
{
name: 'quantity',
title: 'Quantity',
type: 'number',
validation: (Rule) => Rule.min(0),
},
{
name: 'trackQuantity',
title: 'Track Quantity',
type: 'boolean',
initialValue: true,
},
{
name: 'allowBackorder',
title: 'Allow Backorder',
type: 'boolean',
initialValue: false,
},
{
name: 'lowStockThreshold',
title: 'Low Stock Threshold',
type: 'number',
},
],
},
{
name: 'shipping',
title: 'Shipping Information',
type: 'object',
fields: [
{
name: 'weight',
title: 'Weight (kg)',
type: 'number',
},
{
name: 'dimensions',
title: 'Dimensions (L×W×H)',
type: 'string',
},
{
name: 'requiresShipping',
title: 'Requires Shipping',
type: 'boolean',
initialValue: true,
},
],
},
{
name: 'isActive',
title: 'Active',
type: 'boolean',
initialValue: true,
},
],
orderings: [
{
name: 'nameAsc',
title: 'Name (A-Z)',
by: [{ field: 'name', direction: 'asc' }],
},
{
name: 'priceAsc',
title: 'Price (Low to High)',
by: [{ field: 'price', direction: 'asc' }],
},
{
name: 'priceDesc',
title: 'Price (High to Low)',
by: [{ field: 'price', 'direction: 'desc' }],
},
],
};
// File: schemas/author.js
export default {
name: 'author',
title: 'Author',
type: 'document',
fields: [
{
name: 'name',
title: 'Name',
type: 'string',
validation: (Rule) => Rule.required(),
},
{
name: 'email',
title: 'Email',
type: 'email',
validation: (Rule) => Rule.required(),
},
{
name: 'bio',
title: 'Bio',
type: 'text',
rows: 3,
},
{
name: 'avatar',
title: 'Avatar',
type: 'image',
options: {
hotspot: true,
},
fields: [
{
name: 'alt',
title: 'Alternative Text',
type: 'string',
},
],
},
{
name: 'social',
title: 'Social Media',
type: 'object',
fields: [
{
name: 'twitter',
title: 'Twitter',
type: 'url',
},
{
name: 'github',
title: 'GitHub',
type: 'url',
},
{
name: 'linkedin',
title: 'LinkedIn',
type: 'url',
},
{
name: 'website',
title: 'Website',
type: 'url',
},
],
},
{
name: 'role',
title: 'Role',
type: 'string',
},
{
name: 'expertise',
title: 'Areas of Expertise',
type: 'array',
of: [{ type: 'string' }],
},
],
};
// File: schemas/category.js
export default {
name: 'category',
title: 'Category',
type: 'document',
fields: [
{
name: 'name',
title: 'Name',
type: 'string',
validation: (Rule) => Rule.required(),
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'name',
},
},
{
name: 'description',
title: 'Description',
type: 'text',
rows: 2,
},
{
name: 'color',
title: 'Color',
type: 'color',
options: {
disableAlpha: true,
},
},
{
name: 'icon',
title: 'Icon',
type: 'image',
},
{
name: 'parent',
title: 'Parent Category',
type: 'reference',
to: [{ type: 'category' }],
},
{
name: 'seo',
title: 'SEO',
type: 'seo',
fields: [
{
name: 'title',
title: 'Meta Title',
type: 'string',
},
{
name: 'description',
title: 'Meta Description',
type: 'text',
},
],
},
],
},
};
// File: schemas/tag.js
export default {
name: 'tag',
title: 'Tag',
type: 'document',
fields: [
{
name: 'name',
title: 'Name',
type: 'string',
validation: (Rule) => Rule.required(),
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'name',
},
},
{
name: 'description',
title: 'Description',
type: 'text',
rows: 2,
},
{
name: 'color',
title: 'Color',
type: 'color',
options: {
disableAlpha: true,
},
},
],
};
// File: schemas/settings.js
export default {
name: 'settings',
title: 'Site Settings',
type: 'document',
fields: [
{
name: 'siteName',
title: 'Site Name',
type: 'string',
validation: (Rule) => Rule.required(),
},
{
name: 'siteUrl',
title: 'Site URL',
type: 'url',
validation: (Rule) => Rule.required(),
},
{
name: 'description',
title: 'Site Description',
type: 'text',
rows: 3,
},
{
name: 'logo',
title: 'Logo',
type: 'image',
options: { hotspot: true },
fields: [
{
name: 'darkModeLogo',
title: 'Dark Mode Logo',
type: 'image',
},
],
},
{
name: 'favicon',
title: 'Favicon',
type: 'image',
description: 'Recommended: 32×32 or 16×16 pixels',
},
{
name: 'ogImage',
title: 'Default Social Share Image',
type: 'image',
description: 'Recommended: 1200×630 pixels',
},
{
name: 'contact',
title: 'Contact Information',
type: 'object',
fields: [
{
name: 'email',
title: 'Email',
type: 'email',
},
{
name: 'phone',
title: 'Phone',
type: 'string',
},
{
name: 'address',
title: 'Address',
type: 'object',
fields: [
{
name: 'street',
title: 'Street',
type: 'text',
},
{
name: 'city',
title: 'city',
type: 'string',
},
{
name: 'country',
title: 'Country',
type: 'string',
},
{
name: 'postalCode',
title: 'Postal Code',
type: 'string',
},
],
},
],
},
{
name: 'social',
title: 'Social Media Links',
type: 'object',
fields: [
{
name: 'twitter',
title: 'Twitter',
type: 'url',
},
{
name: 'facebook',
title: 'Facebook',
type: 'url',
},
{
name: 'instagram',
title: 'Instagram',
type: 'url',
},
{
name: 'youtube',
title: 'YouTube',
type: 'url',
},
{
name: 'linkedin',
title: 'LinkedIn',
type: 'url',
},
],
},
{
name: 'analytics',
title: 'Analytics Settings',
type: 'object',
fields: [
{
name: 'googleAnalytics',
title: 'Google Analytics',
type: 'object',
fields: [
{
name: 'trackingId',
title: 'Tracking ID',
type: 'string',
},
{
name: 'enableEnhancedMeasurement',
title: 'Enable Enhanced Measurement',
type: 'boolean',
initialValue: true,
},
],
},
{
name: 'facebookPixel',
title: 'Facebook Pixel',
type: 'object',
fields: [
{
name: 'pixelId',
title: 'Pixel ID',
type: 'string',
},
{
name: 'enableAdvancedMatching',
title: 'Enable Advanced Matching',
type: 'boolean',
initialValue: true,
},
],
},
],
},
{
name: 'seo',
title: 'Global SEO Settings',
type: 'object',
fields: [
{
name: 'metaRobots',
title: 'Meta Robots',
type: 'string',
initialValue: 'index,follow',
},
{
name: 'googleSearchConsole',
title: 'Google Search Console Verification',
type: 'string',
description: 'Enter your verification code',
},
{
name: 'bingSiteAuth',
title: 'Bing Site Verification',
type: 'string',
description: 'Enter your verification code',
},
],
},
],
},
};
// File: plugins/visionPlugin.js
export const visionPlugin = {
name: 'sanity-plugin-media-library',
options: {
selectable: true,
sources: [
{
name: 'unsplash',
provider: 'unsplash',
},
{
name: 'pexels',
provider: 'pexels',
},
],
},
};
// File: plugins/tableOfContentsPlugin.js
export const tableOfContentsPlugin = {
name: '@sanity/plugin/table-of-contents',
options: {
minLevel: 2,
maxLevel: 4,
},
};
// File: plugins/altTextPlugin.js
export const altTextPlugin = {
name: '@sanity/plugin-asset-source-unsplash',
options: {
mode: 'auto',
},
};
💻 Integração de API Sanity javascript
🔴 complex
⭐⭐⭐⭐
Exemplos completos de integração de API incluindo consultas GROQ, assinaturas em tempo real, configuração de cliente e implementação frontend
⏱️ 40 min
🏷️ sanity, api, integration, frontend
Prerequisites:
Sanity basics, GROQ, React hooks, JavaScript ES6+, Real-time updates
// Sanity API Integration Examples
// File: lib/sanityClient.js - API Client Setup
import { createClient, createClient, groq } from '@sanity/client';
import { visionTool } from '@sanity/vision';
// Create clients
export const sanityClient = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
apiVersion: '2023-10-16',
useCdn: true,
perspective: 'published',
stega: {
studioUrl: process.env.NEXT_PUBLIC_SANITY_STUDIO_URL,
token: process.env.SANITY_STUDIO_TOKEN,
},
browser: {
projectApiVersion: '2023-10-16',
},
});
export const sanityConfig = {
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
projectId: process.env.npm_package_config_SANITY_PROJECT_ID,
apiVersion: '2023-10-16',
useCdn: process.env.NODE_ENV === 'production',
};
export const client = createClient(sanityConfig);
// Vision tool for image optimization
export const builder = imageUrlBuilder({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITIZE_DATASET || 'production',
apiVersion: '2023-10-16',
});
// File: lib/groqQueries.js - Predefined GROQ Queries
export const groqQueries = {
// Blog posts with population
getBlogPosts: groq`
*[_type == "blogPost"] {
_id,
title,
slug,
excerpt,
content,
publishedAt,
readingTime,
"author": author->{
_id,
name,
email,
avatar,
bio,
role,
},
"category": category->{
_id,
name,
slug,
color,
"parent": parent->{
name,
slug,
},
},
"tags": tags[]->{
_id,
name,
slug,
color,
},
featuredImage,
gallery,
seo {
metaTitle,
metaDescription,
keywords,
},
} | order(publishedAt desc)
`,
getBlogPostBySlug: (slug) => groq`
*[_type == "blogPost" && slug == ${slug}] {
_id,
title,
slug,
content,
publishedAt,
readingTime,
"author": author->{
_id,
name,
email,
avatar,
bio,
social,
},
"category": category->{
_id,
name,
slug,
description,
color,
},
"tags": tags[]->{
_id,
name,
slug,
},
featuredImage,
gallery,
seo {
metaTitle,
metaDescription,
keywords,
image,
},
}
`,
getRelatedBlogPosts: (categoryId, currentPostId, limit = 3) => groq`
*[_type == "blogPost" && _id != ${currentPostId}] {
_id,
title,
slug,
excerpt,
publishedAt,
"category": category->{
_id,
name,
slug,
},
featuredImage,
}[${limit}] | order(publishedAt desc)
`,
getFeaturedBlogPosts: (limit = 5) => groq`
*[_type == "blogPost" && isFeatured == true] {
_id,
title,
slug,
excerpt,
publishedAt,
"author": author->{
name,
avatar,
},
featuredImage,
}[${limit}] | order(publishedAt desc)
`,
getBlogPostsByCategory: (categoryId, limit = 10) => groq`
*[_type == "blogPost" && category._ref == ${categoryId}` {
_id,
title,
slug,
excerpt,
publishedAt,
"author": author->{
name,
avatar,
},
featuredImage,
}[${limit}] | order(publishedAt desc)
`,
// Products with population
getProducts: groq`
*[_type == "product"] {
_id,
name,
slug,
shortDescription,
description,
sku,
price,
comparePrice,
currency,
images,
variants,
category,
"brand": brand->{
_id,
name,
logo,
website,
},
tags,
attributes,
isActive,
} | order(name asc)
`,
getProductBySlug: (slug) => groq`
*[_type == "product" && slug == ${slug}] {
_id,
name,
slug,
description,
shortDescription,
sku,
price,
comparePrice,
currency,
images,
variants,
inventory,
shipping,
category,
"brand": brand->{
_id,
name,
logo,
website,
},
tags,
attributes,
isActive,
}
`,
getProductsByCategory: (categoryId) => groq`
*[_type == "product" && category._ref == ${categoryId}` {
_id,
name,
slug,
shortDescription,
price,
currency,
images,
category,
isActive,
} | order(name asc)
`,
// Categories
getCategories: groq`
*[_type == "category"] {
_id,
name,
slug,
description,
color,
icon,
"parent": parent->{
name,
slug,
},
count: count(*[_type == "blogPost" && references(^.^\.category._ref) == ${_id}]),
} | order(name asc)
`,
// Authors
getAuthors: groq`
*[_type == "author"] {
_id,
name,
email,
bio,
avatar,
social,
role,
expertise,
postCount: count(*[_type == "blogPost" && references(^.^\.author._ref) == ${_id}]),
}
`,
// Tags
getTags: groq`
*[_type == "tag"] {
_id,
name,
slug,
description,
color,
postCount: count(*[_type == "blogPost" && references(^.^\.tags[]._ref) == ${_id}]),
}
`,
// Settings
getSettings: groq`
*[_type == "settings"] {
siteName,
siteUrl,
description,
logo,
favicon,
ogImage,
contact,
social,
analytics,
seo,
}
`,
};
// File: hooks/useSanity.js - React Hooks
import { useState, useEffect, useCallback } from 'react';
import { sanityClient } from '../lib/sanityClient';
export function useSanityQuery(query, dependencies = []) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const result = await sanityClient.fetch(query);
setData(result);
} catch (err) {
setError(err);
console.error('Sanity query error:', err);
} finally {
setLoading(false);
}
}, [query, sanityClient]);
useEffect(() => {
fetchData();
}, [fetchData, ...dependencies]);
const refetch = useCallback(() => {
return fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
}
export function useSanitySubscription(query, callback, dependencies = []) {
const [subscription, setSubscription] = useState(null);
useEffect(() => {
const newSubscription = sanityClient.subscribe(query, callback);
setSubscription(newSubscription);
return () => {
newSubscription.unsubscribe();
};
}, [query, callback, sanityClient, ...dependencies]);
return subscription;
}
export function useSanityClient() {
return sanityClient;
}
// File: services/sanity-service.js - Service Layer
export class SanityService {
constructor(client = sanityClient) {
this.client = client;
}
// Blog posts
async getBlogPosts(params = {}) {
const { limit = 10, offset = 0, category, tag, featured } = params;
let query = groqQueries.getBlogPosts;
if (offset > 0) {
query = `${query}[${offset}...${offset + limit}]`;
} else if (limit) {
query = `${query}[${limit}]`;
}
if (category) {
query = query.replace(
'*',
`*[_type == "blogPost" && category._ref == ${category}`
);
}
if (tag) {
query = query.replace(
'*',
`*[_type == "blogPost" && ${tag} in tags[]->slug]`
);
}
if (featured) {
query = query.replace(
'*',
`*[_type == "BlogPost" && isFeatured == true`
);
}
const result = await this.client.fetch(query);
return {
posts: result,
total: result.length,
};
}
async getBlogPostBySlug(slug, preview = false) {
const client = preview ? contentfulPreviewClient : this.client;
const query = groqQueries.getBlogPostBySlug(slug);
const result = await client.fetch(query);
return result[0] || null;
}
async getRelatedBlogPosts(categoryId, currentPostId, limit = 3) {
const query = groqQueries.getRelatedBlogPosts(categoryId, currentPostId, limit);
const result = await this.client.fetch(query);
return result;
}
async createBlogPost(postData) {
const mutations = [
groq`
mutation {
createBlogPost {
_id,
_createdAt,
_rev,
_type
}
}
`,
];
const mutation = mutations[0];
const result = await this.client.mutate(mutation);
return result;
}
async updateBlogPost(postId, patch) {
const mutations = [
groq`
mutation {
updateBlogPost(id: ${postId}, patch: ${JSON.stringify(patch)}) {
_id,
_rev
}
}
`,
];
const mutation = mutations[0];
const result = await this.client.mutate(mutation);
return result;
}
async deleteBlogPost(postId) {
const mutations = [
groq`
mutation {
deleteBlogPost(id: ${postId}) {
id,
deletedAt,
status: 'deleted'
}
}
`,
];
const mutation = mutations[0];
const result = await this.client.mutate(mutation);
return result;
}
// Products
async getProducts(params = {}) {
const { limit = 20, offset = 0, category, brand, search } = params;
let query = groqQueries.getProducts;
if (offset > 0) {
query = `${query}[${offset}...${offset + limit}]`;
} else if (limit) {
query = `${query}[${limit}]`;
}
if (category) {
query = query.replace(
'*',
`*[_type == "product" && category._ref == ${category}`
);
}
if (brand) {
query = query.replace(
'*',
`*[_type == "product" && brand._ref == ${brand}`
);
}
if (search) {
query = query.replace(
'*',
`*[_type == "product" && name match ${search}* || shortDescription match ${search}*`
);
}
const result = await this.client.fetch(query);
return {
products: result,
total: result.length,
};
}
async getProductBySlug(slug) {
const query = groqQueries.getProductBySlug(slug);
const result = await this.client.fetch(query);
return result[0] || null;
}
async searchAll(searchTerm, options = {}) {
const { limit = 50, types = ['blogPost', 'product', 'category', 'tag'] } = options;
const searchQueries = types.map(type => {
switch (type) {
case 'blogPost':
`*[_type == "blogPost" && (title match ${searchTerm}* || content match ${searchTerm}*)] {
_id,
title,
slug,
excerpt,
featuredImage,
}`;
case 'product':
`*[_type == "product" && (name match ${searchTerm}* || description match ${searchTerm}*)] {
_id,
name,
slug,
price,
currency,
images[0],
}`;
case 'category':
`*[_type == "category" && name match ${searchTerm}*` {
_id,
name,
slug,
description,
color,
}`;
case 'tag':
`*[_type == "tag" && name match ${searchTerm}*` {
_id,
name,
slug,
}`;
default:
null;
}
}).filter(Boolean);
const allResults = await Promise.all(
searchQueries.map(query =>
query ? this.client.fetch(query) : []
)
);
const flatResults = allResults.flat();
return {
items: flatResults,
total: flatResults.length,
searchTerm,
};
}
// Categories
async getCategories() {
const query = groqQueries.getCategories;
const result = await this.client.fetch(query);
return result;
}
// Authors
async getAuthors() {
const query = groqQueries.getAuthors;
const result = await this.client.client.fetch(query);
return result;
}
// Settings
async getSettings() {
const query = groqQueries.getSettings;
const result = await this.client.fetch(query);
return result[0];
}
// Utility methods
async publishDocument(docId) {
const mutation = groq`
mutation {
publish(id: ${docId}) {
status: "published",
_id,
_rev
}
}
`;
const result = await this.client.mutate(mutation);
return result;
}
async unpublishDocument(docId) {
const mutation = groq`
mutation {
unPublish(id: ${docId}) {
status: "draft",
_id,
_rev
}
}
`;
const result = await this.client.mutate(mutation);
return result;
}
async duplicateDocument(docId, overrides = {}) {
const mutation = groq`
mutation {
duplicate(
id: ${docId}
${Object.entries(overrides).map(([key, value]) =>
${key}: ${JSON.stringify(value)}`
).join(',')}
) {
_id,
_rev
}
}
`;
const result = await this.client.mutate(mutation);
return result;
}
}
// File: components/BlogPostCard.js - Frontend Component
import React from 'react';
import imageUrlBuilder from '@sanity/image-url';
import { PortableText } from '@portabletext/react';
import Link from 'next/link';
import { format } from 'date-fns';
import { groq } from 'next-sanity';
import { client } from '../lib/sanityClient';
const BlogPostCard = ({ post, className = '' }) => {
if (!post) return null;
const {
title,
slug,
excerpt,
publishedAt,
readingTime,
author,
category,
tags,
featuredImage,
} = post;
const authorName = author?.name;
const categoryName = category?.name;
const imageUrl = featuredImage
? imageUrlBuilder
.image(featuredImage)
.width(400)
.height(250)
.format('webp')
.url()
: null;
const postUrl = `/blog/${slug?.current}`;
return (
<article className={`blog-post-card ${className}`}>
{imageUrl && (
<div className="blog-post-card__image">
<Link href={postUrl}>
<Image
src={imageUrl}
alt={title}
width={400}
height={250}
className="object-cover w-full h-full"
style={{
transition: 'transform 0.2s',
}}
whileHover={{ scale: 1.05 }}
/>
</Link>
</div>
)}
<div className="blog-post-card__content">
{categoryName && (
<span
className="blog-post-card__category"
style={{
backgroundColor: category?.color || '#0070f3',
}}
>
{categoryName}
</span>
)}
<h2 className="blog-post-card__title">
<Link href={postUrl}>
{title}
</Link>
</h2>
{excerpt && (
<PortableText
value={excerpt}
components={{
em: ({ children }) => <em>{children}</em>,
a: ({ href, children }) => <Link href={href}>{children}</Link>,
strong: ({ children }) => <strong>{children}</strong>,
code: ({ children }) => (
<code className="bg-gray-100 px-1 py-0.5 text-sm">...contentful-container{
}
}}
className="blog-post-card__excerpt"
/>
)}
<div className="blog-post-card__meta">
{authorName && (
<span className="blog-post-card__author">
By {authorName}
</span>
)}
{publishedAt && (
<time className="blog-post__date">
{format(new Date(publishedAt), 'MMM d, yyyy')}
</time>
)}
{readingTime && (
<span className="blog-post__reading-time">
{readingTime} min read
</span>
)}
</div>
{tags && tags.length > 0 && (
<div className="blog-post-card__tags">
{tags.slice(0, 3).map((tag) => (
<span
key={tag._id} || tag.slug?.current}
className="blog-post-card__tag"
style={{
backgroundColor: tag.color || '#e5e7eb',
}}
>
{tag.name}
</span>
))}
{tags.length > 3 && (
<span className="blog-post__more-tags">
+{tags.length - 3} more
</span>
)}
</div>
)}
</div>
</article>
);
};
export default BlogPostCard;
// File: hooks/useRealtimeBlogPosts.js - Real-time Updates
import { useState, useEffect } from 'react';
import { useSanitySubscription } from './useSanity';
import { groq } from 'next-sanity';
export function useRealtimeBlogPosts() {
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// Set up real-time subscription
const subscription = useSanitySubscription(
groq`
*[_type == "blogPost"] {
_id,
_type,
title,
slug,
publishedAt,
status
}
`,
(update) => {
if (update.type === 'create') {
// New post added
setPosts(prev => [update.result, ...prev]);
} else if (update.type === 'update') {
// Post updated
setPosts(prev =>
prev.map(post =>
post._id === update.result._id ? update.result : post
)
);
} else if (update.type === 'delete') {
// Post deleted
setPosts(prev => prev.filter(post => post._id !== update.result._id));
}
},
[posts]
);
// Initial load
useEffect(() => {
const loadPosts = async () => {
try {
setIsLoading(true);
const sanityService = new SanityService();
const { posts: initialPosts } = await sanityService.getBlogPosts({
limit: 10,
});
setPosts(initialPosts);
setIsLoading(false);
} catch (error) {
console.error('Failed to load initial blog posts:', error);
setIsLoading(false);
}
};
loadPosts();
}, []);
return { posts, isLoading, subscription };
}
💻 Atualizações em Tempo Real e Webhooks do Sanity javascript
🔴 complex
⭐⭐⭐⭐
Atualizações de conteúdo em tempo real, configuração de webhooks e fluxos de trabalho automatizados
⏱️ 30 min
🏷️ sanity, webhooks, real-time
Prerequisites:
Sanity basics, Webhook concepts, Event-driven architecture, Node.js
// Sanity Real-time Updates & Webhooks Examples
// File: lib/sanity-webhook-server.js
const express = require('express');
const crypto = require('crypto');
const { createClient, groq } = require('@sanity/client');
class SanityWebhookServer {
constructor() {
this.app = express();
this.client = createClient({
projectId: process.env.SANITY_PROJECT_ID,
dataset: process.env.SANITY_DOCUMENT_DATASET,
token: process.env.SANITY_WEBHOOK_SECRET,
});
this.setupRoutes();
}
setupRoutes() {
this.app.use(express.json());
// Webhook verification endpoint
this.app.post('/api/sanity/webhook', (req, res) => {
const signature = req.headers['sanity-webhook-signature'];
const body = JSON.stringify(req.body);
if (!this.verifyWebhook(body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
this.processWebhook(req.body);
res.status(200).json({ success: true });
});
// Content change notification endpoint
this.app.post('/api/sanity/content-change', async (req, res) => {
const { type, id, fields } = req.body;
// Send notification to connected clients
this.broadcastContentChange(type, id, fields);
res.status(200).json({ success: true });
});
}
verifyWebhook(body, signature) {
const webhookSecret = process.env.SANITY_WEBHOOK_SECRET;
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(body)
.digest('hex');
return signature === expectedSignature;
}
processWebhook(payload) {
const { type, ids } = payload;
switch (type) {
case 'create':
this.handleContentCreate(ids);
break;
case 'update':
this.handleContentUpdate(ids);
break;
case 'delete':
this.handleContentDelete(ids);
break;
case 'publish':
this.handleContentPublish(ids);
break;
case 'unpublish':
this.handleContentUnpublish(ids);
break;
}
}
handleContentCreate(ids) {
console.log(`New content created: ${ids.join(', ')}`);
this.triggerWebhook('content.created', { ids });
}
handleContentUpdate(ids) {
console.log(`Content updated: ${ids.join(', ')}`);
this.triggerWebhook('content.updated', { ids });
}
handleContentDelete(ids) {
console.log(`Content deleted: ${ids.join(', ')}`);
this.triggerWebhook('content.deleted', { ids });
}
handleContentPublish(ids) {
console.log(`Content published: ${ids.join(', ')}`);
this.triggerWebhook('content.published', { ids });
}
handleContentUnpublish(ids) {
console.log(`Content unpublished: ${ids.join(', ')}`);
this.triggerWebhook('content.unpublished', { ids });
}
triggerWebhook(event, data) {
// Send webhook to configured endpoints
this.sendToSlack(event, data);
this.sendToDiscord(event, data);
}
async sendToSlack(event, data) {
if (!process.env.SLACK_WEBHOOK_URL) {
return;
}
const message = this.formatSlackMessage(event, data);
try {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: message }),
});
} catch (error) {
console.error('Failed to send Slack notification:', error);
}
}
async sendToDiscord(event, data) {
if (!process.env.DISCORD_WEBHOOK_URL) {
return;
}
const message = this.formatDiscordMessage(event, data);
try {
await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ embed: message }),
});
} catch (error) {
console.error('Failed to send Discord notification:', error);
}
}
formatSlackMessage(event, data) {
const eventEmojis = {
'content.created': '📝',
'content.updated': '✏�',
'content.deleted': '🗑️',
'content.published': '✅',
'content.unpublished': '📝',
};
return ${eventEmojis[event]} Sanity ${event.replace('_', ' ').toUpperCase()}
` +
`Content IDs: ${data.ids?.join(', ')}`;
}
formatDiscordMessage(event, data) {
const eventEmojis = {
'content.created': '📝',
'content.updated': '✏�',
'content.deleted': '🗑️',
'content.published': '✅',
'content.unpublished': '📝',
};
return {
title: `Sanity Content ${event.replace('_', ' ).toUpperCase()}`,
description: `Content IDs: ${data.ids?.join(', ')}`,
color: this.getDiscordColor(event),
};
}
getDiscordColor(event) {
const colors = {
'content.created': '#22c55e0',
'content.updated': '#f59e0b',
'content.deleted': '#ef4444',
'content.published': '#22c55e0',
'content.unpublish': '#f59e0b',
};
return colors[event] || '#808080';
}
broadcastContentChange(type, ids, fields) {
// This would typically use WebSockets or Server-Sent Events
console.log(`Broadcasting content change: ${type}`, ids, fields);
}
start(port = 3001) {
this.app.listen(port, () => {
console.log(`Sanity webhook server running on port ${port}`);
});
}
}
// File: scripts/deploy-sanity.js - Deployment Script
const { exec } = require('child_process');
class SanityDeployer {
constructor() {
this.projectId = process.env.SANITY_PROJECT_ID;
this.dataset = process.env.SANITY_DATASET;
this.token = process.env.SANITY_TOKEN;
}
async deploy() {
try {
console.log('Starting Sanity deployment...');
// 1. Validate configuration
await this.validateConfig();
// 2. Backup current data
await this.backupData();
// 3. Deploy schema
await this.deploySchema();
// 4. Import content
await this.importContent();
// 5. Run migrations
await this.runMigrations();
// 6. Deploy hooks
await this.deployHooks();
// 7. Run tests
await this.runTests();
console.log('Sanity deployment completed successfully!');
} catch (error) {
console.error('Sanity deployment failed:', error);
throw error;
}
}
validateConfig() {
console.log('Validating Sanity configuration...');
if (!this.projectId) {
throw new Error('SANITY_PROJECT_ID is required');
}
if (!this.token) {
throw new Error('SANITY_TOKEN is required');
}
console.log(`Project ID: ${this.projectId}`);
console.log(`Dataset: ${this.dataset}`);
console.log('Configuration validated ✓');
}
async backupData() {
console.log('Backing up current data...');
const backupDir = './backups';
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${backupDir}/sanity-backup-${timestamp}.json`;
exec(`mkdir -p ${backupDir}`);
const backupCommand = `npx sanity export ${this.dataset} --out ${backupPath}`;
console.log(`Running: ${backupCommand}`);
return new Promise((resolve, reject) => {
exec(backupCommand, { cwd: process.cwd() }, (error, stdout, stderr) => {
if (error) {
console.error('Backup failed:', error);
return reject(error);
}
console.log('Backup completed:', backupPath);
resolve(backupPath);
});
});
}
async deploySchema() {
console.log('Deploying schema...');
const schemaDir = './schemas';
const deployCommand = `npx sanity deploy --project ${this.projectId} --no-verify ${schemaDir}`;
console.log(`Running: ${deployCommand}`);
return new Promise((resolve, reject) => {
exec(deployCommand, { cwd: process.cwd() }, (error, stdout, stderr) => {
if (error) {
console.error('Schema deployment failed:', error);
return reject(error);
}
console.log('Schema deployment completed');
resolve();
});
});
}
async importContent() {
console.log('Importing content...');
const importDir = './data/content';
const importCommand = `npx sanity import ${this.dataset} --replace ${importDir}`;
console.log(`Running: ${importCommand}`);
return new Promise((resolve, reject) => {
exec(importCommand, { cwd: process.cwd() }, (error, stdout, stderr) => {
if (error) {
console.error('Content import failed:', error);
return reject(error);
}
console.log('Content import completed');
resolve();
});
});
}
async runMigrations() {
console.log('Running migrations...');
const migrationsDir = './migrations';
const migrateCommand = `npx sanity migration ${migrationsDir}`;
console.log(`Running: ${migrateCommand}`);
return new Promise((resolve, reject) => {
exec(migrateCommand, { cwd: process.cwd() }, (error, stdout, stderr) => {
if (error) {
console.error('Migrations failed:', error);
return reject(error);
}
console.log('Migrations completed');
resolve();
});
});
}
async deployHooks() {
console.log('Deploying hooks...');
const hooksDir = './hooks';
const deployCommand = `npx sanity hook deploy ${hooksDir}`;
console.log(`Running: ${deployCommand}`);
return new Promise((resolve, reject) => {
exec(deployCommand, { cwd: process.cwd() }, (error, stdout, stderr) => {
if (error) {
console.error('Hooks deployment failed:', error);
return reject(error);
}
console.log('Hooks deployment completed');
resolve();
});
}
async runTests() {
console.log('Running tests...');
const testCommand = 'npm run test:sanity';
console.log(`Running: ${testCommand}`);
return new Promise((resolve, reject) => {
exec(testCommand, { cwd: process.cwd() }, (error, stdout, stderr) => {
if (error) {
console.error('Tests failed:', error);
return reject(error);
}
console.log('Tests passed');
resolve();
});
}
async rollback() {
console.log('Rolling back deployment...');
const backupDir = './backups';
const rollbackCommand = `npx import ${this.dataset} --replace ${backupDir}`;
return new Promise((resolve, reject) => {
exec(rollbackCommand, { cwd: process.cwd() }, (error, stdout, stderr) => {
if (error) {
console.error('Rollback failed:', error);
return reject(error);
}
console.log('Rollback completed');
resolve();
});
}
}
// File: lib/webhook-client.js - Webhook Client for Frontend
class SanityWebhookClient {
constructor(webhookUrl, secret) {
this.webhookUrl = webhookUrl;
this.secret = secret;
}
async sendWebhook(event, data = {}) {
const payload = {
event,
timestamp: new Date().toISOString(),
...data,
};
const signature = this.generateSignature(JSON.stringify(payload));
try {
const response = await fetch(this.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Webhook request failed: ${response.status}`);
}
return response.json();
} catch (error) {
console.error('Webhook error:', error);
throw error;
}
}
generateSignature(payload) {
return crypto
.createHmac('sha256', this.secret)
.update(payload)
.digest('hex');
}
}
// Usage example:
const webhookClient = new SanityWebhookClient(
process.env.NEXT_PUBLIC_SANITY_WEBHOOK_URL,
process.env.SANITY_WEBHOOK_SECRET
);
// Send webhook notification
await webhookClient.sendWebhook('content.updated', {
ids: ['abc123', 'def456'],
changes: ['title', 'content'],
});