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