Astro กับ Sanity CMS: Headless CMS
#astro13 เม.ย. 2569
Astro กับ Sanity CMS: Headless CMS
Sanity เป็น headless CMS ที่ทรงพลังและยืดหยุ่น ด้วย real-time collaboration, structured content, และ GROQ query language ทำให้ Sanity เป็นตัวเลือกยอดนิยมสำหรับ content-heavy websites ที่สร้างด้วย Astro
ทำไมต้องใช้ Sanity?
- Real-time Editing: แก้ไข content แบบ real-time
- Structured Content: กำหนด schema ของ content ได้เอง
- GROQ: Query language ที่ทรงพลังและยืดหยุ่น
- Portable Text: Rich text format ที่ portable
- CDN: Content delivery ที่เร็วทั่วโลก
การติดตั้ง
# สร้าง Sanity project
npm create sanity@latest
# ติดตั้ง Sanity client
npm install @sanity/client @sanity/image-url
กำหนด Schema ใน Sanity
สร้าง sanity/schemas/post.ts:
import { defineField, defineType } from 'sanity';
export const postType = defineType({
name: 'post',
title: 'บทความ',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'หัวข้อ',
type: 'string',
validation: (rule) => rule.required().min(5).max(100),
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: { source: 'title' },
validation: (rule) => rule.required(),
}),
defineField({
name: 'excerpt',
title: 'บทสรุป',
type: 'text',
rows: 3,
}),
defineField({
name: 'mainImage',
title: 'รูปภาพหลัก',
type: 'image',
options: { hotspot: true },
fields: [
defineField({
name: 'alt',
title: 'Alt Text',
type: 'string',
}),
],
}),
defineField({
name: 'body',
title: 'เนื้อหา',
type: 'array',
of: [
{ type: 'block' },
{
type: 'image',
options: { hotspot: true },
},
{
type: 'code',
options: {
language: 'typescript',
languageAlternatives: [
{ title: 'TypeScript', value: 'typescript' },
{ title: 'JavaScript', value: 'javascript' },
{ title: 'Bash', value: 'bash' },
],
},
},
],
}),
defineField({
name: 'author',
title: 'ผู้เขียน',
type: 'reference',
to: [{ type: 'author' }],
}),
defineField({
name: 'categories',
title: 'หมวดหมู่',
type: 'array',
of: [{ type: 'reference', to: [{ type: 'category' }] }],
}),
defineField({
name: 'publishedAt',
title: 'วันที่เผยแพร่',
type: 'datetime',
}),
],
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage',
},
prepare({ title, author, media }) {
return {
title,
subtitle: author ? `โดย ${author}` : 'ไม่ระบุผู้เขียน',
media,
};
},
},
});
สร้าง Sanity Client
// src/lib/sanity.ts
import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
import type { SanityImageSource } from '@sanity/image-url/lib/types/types';
export const sanityClient = createClient({
projectId: import.meta.env.SANITY_PROJECT_ID,
dataset: import.meta.env.SANITY_DATASET ?? 'production',
apiVersion: '2024-01-01',
useCdn: true,
});
const builder = imageUrlBuilder(sanityClient);
export function urlFor(source: SanityImageSource) {
return builder.image(source);
}
// GROQ Queries
export const queries = {
allPosts: `*[_type == "post" && defined(slug.current)] | order(publishedAt desc) {
_id,
title,
slug,
excerpt,
mainImage,
publishedAt,
"author": author->{ name, image },
"categories": categories[]->{ title, slug }
}`,
postBySlug: `*[_type == "post" && slug.current == $slug][0] {
_id,
title,
slug,
excerpt,
mainImage,
body,
publishedAt,
"author": author->{ name, image, bio },
"categories": categories[]->{ title, slug }
}`,
allSlugs: `*[_type == "post" && defined(slug.current)][].slug.current`,
};
ดึงข้อมูลใน Astro
---
// src/pages/blog/index.astro
import { sanityClient, queries, urlFor } from '../../lib/sanity';
interface Post {
_id: string;
title: string;
slug: { current: string };
excerpt: string;
mainImage: any;
publishedAt: string;
author: { name: string; image: any };
}
const posts = await sanityClient.fetch<Post[]>(queries.allPosts);
---
<html lang="th">
<body>
<h1>บทความทั้งหมด</h1>
<div class="posts-grid">
{posts.map((post) => (
<article>
{post.mainImage && (
<img
src={urlFor(post.mainImage).width(400).height(250).url()}
alt={post.mainImage.alt ?? post.title}
/>
)}
<h2>
<a href={`/blog/${post.slug.current}`}>{post.title}</a>
</h2>
<p>{post.excerpt}</p>
<p>โดย {post.author?.name}</p>
</article>
))}
</div>
</body>
</html>
Dynamic Routes
---
// src/pages/blog/[slug].astro
import { sanityClient, queries, urlFor } from '../../lib/sanity';
import { PortableText } from '@portabletext/react';
export async function getStaticPaths() {
const slugs = await sanityClient.fetch<string[]>(queries.allSlugs);
return slugs.map((slug) => ({ params: { slug } }));
}
const { slug } = Astro.params;
const post = await sanityClient.fetch(queries.postBySlug, { slug });
if (!post) {
return Astro.redirect('/404');
}
---
<html lang="th">
<body>
<article>
<h1>{post.title}</h1>
{post.mainImage && (
<img
src={urlFor(post.mainImage).width(1200).height(630).url()}
alt={post.mainImage.alt}
/>
)}
<!-- Render Portable Text -->
<div class="content">
<!-- ใช้ @portabletext/react หรือ render เอง --
</div>
</article>
</body>
</html>
Live Preview
// src/lib/sanityPreview.ts
import { createClient } from '@sanity/client';
export const previewClient = createClient({
projectId: import.meta.env.SANITY_PROJECT_ID,
dataset: import.meta.env.SANITY_DATASET ?? 'production',
apiVersion: '2024-01-01',
useCdn: false,
token: import.meta.env.SANITY_API_TOKEN,
perspective: 'previewDrafts',
});
สรุป
Sanity CMS กับ Astro เป็นคู่ที่ยอดเยี่ยมสำหรับ content-heavy websites ด้วย GROQ query language ที่ทรงพลัง, structured content schema, และ real-time editing ทำให้ทีม content สามารถจัดการเนื้อหาได้อย่างมีประสิทธิภาพ ในขณะที่ Astro ดูแล performance และ SEO