Astro กับ Contentful: Content Management
#astro13 เม.ย. 2569
Astro กับ Contentful: Content Management
Contentful เป็น headless CMS ระดับ enterprise ที่ได้รับความไว้วางใจจากบริษัทชั้นนำทั่วโลก ด้วย Content Delivery API ที่รวดเร็ว, rich content modeling, และ localization support ทำให้ Contentful เหมาะสำหรับ websites ขนาดใหญ่ที่ต้องการ content management ที่ครบครัน
ทำไมต้องใช้ Contentful?
- Enterprise-grade: ใช้งานโดยบริษัทชั้นนำ
- Rich Content Modeling: กำหนด content types ได้อย่างยืดหยุ่น
- Localization: รองรับหลายภาษาในตัว
- Webhooks: แจ้งเตือนเมื่อ content เปลี่ยนแปลง
- GraphQL API: รองรับทั้ง REST และ GraphQL
การติดตั้ง
npm install contentful contentful-management
ตั้งค่า environment variables:
# .env
CONTENTFUL_SPACE_ID=your-space-id
CONTENTFUL_ACCESS_TOKEN=your-access-token
CONTENTFUL_PREVIEW_TOKEN=your-preview-token
CONTENTFUL_MANAGEMENT_TOKEN=your-management-token
สร้าง Contentful Client
// src/lib/contentful.ts
import contentful from 'contentful';
export const contentfulClient = contentful.createClient({
space: import.meta.env.CONTENTFUL_SPACE_ID,
accessToken: import.meta.env.CONTENTFUL_ACCESS_TOKEN,
});
export const previewClient = contentful.createClient({
space: import.meta.env.CONTENTFUL_SPACE_ID,
accessToken: import.meta.env.CONTENTFUL_PREVIEW_TOKEN,
host: 'preview.contentful.com',
});
// TypeScript types สำหรับ content
export interface BlogPost {
contentTypeId: 'blogPost';
fields: {
title: contentful.EntryFieldTypes.Text;
slug: contentful.EntryFieldTypes.Text;
excerpt: contentful.EntryFieldTypes.Text;
content: contentful.EntryFieldTypes.RichText;
featuredImage: contentful.EntryFieldTypes.AssetLink;
author: contentful.EntryFieldTypes.EntryLink<Author>;
tags: contentful.EntryFieldTypes.Array<contentful.EntryFieldTypes.Symbol>;
publishedDate: contentful.EntryFieldTypes.Date;
seoTitle: contentful.EntryFieldTypes.Text;
seoDescription: contentful.EntryFieldTypes.Text;
};
}
export interface Author {
contentTypeId: 'author';
fields: {
name: contentful.EntryFieldTypes.Text;
bio: contentful.EntryFieldTypes.Text;
avatar: contentful.EntryFieldTypes.AssetLink;
twitter: contentful.EntryFieldTypes.Text;
};
}
ดึงข้อมูล Blog Posts
// src/lib/posts.ts
import { contentfulClient } from './contentful';
import type { BlogPost } from './contentful';
import type { Entry } from 'contentful';
export async function getAllPosts(options?: {
limit?: number;
skip?: number;
tag?: string;
}) {
const response = await contentfulClient.getEntries<BlogPost>({
content_type: 'blogPost',
order: ['-fields.publishedDate'],
limit: options?.limit ?? 10,
skip: options?.skip ?? 0,
...(options?.tag && { 'fields.tags[in]': options.tag }),
include: 2, // depth ของ linked entries
});
return {
posts: response.items,
total: response.total,
limit: response.limit,
skip: response.skip,
};
}
export async function getPostBySlug(slug: string) {
const response = await contentfulClient.getEntries<BlogPost>({
content_type: 'blogPost',
'fields.slug': slug,
limit: 1,
include: 3,
});
return response.items[0] ?? null;
}
export async function getAllSlugs() {
const response = await contentfulClient.getEntries<BlogPost>({
content_type: 'blogPost',
select: ['fields.slug'],
limit: 1000,
});
return response.items.map((item) => item.fields.slug);
}
Blog Index Page
---
// src/pages/blog/index.astro
import { getAllPosts } from '../../lib/posts';
import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
const page = parseInt(Astro.url.searchParams.get('page') ?? '1');
const limit = 9;
const skip = (page - 1) * limit;
const { posts, total } = await getAllPosts({ limit, skip });
const totalPages = Math.ceil(total / limit);
---
<html lang="th">
<body>
<h1>บทความ</h1>
<div class="posts-grid">
{posts.map((post) => {
const { title, slug, excerpt, featuredImage, publishedDate, author } = post.fields;
const imageUrl = (featuredImage as any)?.fields?.file?.url;
const authorName = (author as any)?.fields?.name;
return (
<article>
{imageUrl && (
<img src={`https:${imageUrl}?w=400&h=250&fit=fill`} alt={title} />
)}
<h2><a href={`/blog/${slug}`}>{title}</a></h2>
<p>{excerpt}</p>
<p>โดย {authorName} | {new Date(publishedDate).toLocaleDateString('th-TH')}</p>
</article>
);
})}
</div>
<!-- Pagination -->
<nav>
{page > 1 && <a href={`/blog?page=${page - 1}`}>ก่อนหน้า</a>}
<span>หน้า {page} / {totalPages}</span>
{page < totalPages && <a href={`/blog?page=${page + 1}`}>ถัดไป</a>}
</nav>
</body>
</html>
Rich Text Renderer
// src/lib/richText.ts
import { documentToHtmlString } from '@contentful/rich-text-html-renderer';
import { BLOCKS, INLINES, MARKS } from '@contentful/rich-text-types';
import type { Document } from '@contentful/rich-text-types';
export function renderRichText(document: Document): string {
return documentToHtmlString(document, {
renderNode: {
[BLOCKS.PARAGRAPH]: (node, next) =>
`<p class="mb-4">${next(node.content)}</p>`,
[BLOCKS.HEADING_1]: (node, next) =>
`<h1 class="text-4xl font-bold mb-6">${next(node.content)}</h1>`,
[BLOCKS.HEADING_2]: (node, next) =>
`<h2 class="text-3xl font-bold mb-4">${next(node.content)}</h2>`,
[BLOCKS.EMBEDDED_ASSET]: (node) => {
const { file, title } = node.data.target.fields;
return `<img src="https:${file.url}" alt="${title}" class="rounded-lg my-6" />`;
},
[INLINES.HYPERLINK]: (node, next) =>
`<a href="${node.data.uri}" class="text-blue-600 hover:underline">${next(node.content)}</a>`,
},
renderMark: {
[MARKS.CODE]: (text) =>
`<code class="bg-gray-100 px-1 rounded">${text}</code>`,
},
});
}
Webhook สำหรับ ISR
// src/pages/api/revalidate.ts
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => {
const secret = request.headers.get('x-contentful-webhook-secret');
if (secret !== import.meta.env.WEBHOOK_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
const body = await request.json();
const slug = body.fields?.slug?.['en-US'];
if (slug) {
// Trigger revalidation
console.log(`Revalidating: /blog/${slug}`);
}
return new Response(JSON.stringify({ revalidated: true }));
};
สรุป
Contentful กับ Astro เป็นคู่ที่เหมาะสำหรับ enterprise websites ที่ต้องการ content management ที่ครบครัน ด้วย rich content modeling, localization support, และ powerful API ทำให้ทีม content สามารถจัดการเนื้อหาได้อย่างมีประสิทธิภาพ