ทำเว็บ Blog ด้วย Next.js และ Strapi

/ 5 min read

Share on social media

next-strapi สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

** เนื่องจากบทความนี้มีลักษณะเป็น hand-ons ค่อนข้างมาก จะไม่ได้ลง detail ไว้ในบทความ (บทความจะเป็นเหมือนสารบัญร่วมกันกับคลิปที่ทำ) หากท่านใดที่แวะเวียนเข้ามา สามารถดูได้ผ่าน Video นะครับ

Strapi คืออะไร ?

Ref: https://strapi.io/

Strapi คือ open-source project ที่ใช้สำหรับทำ CMS แบบ Headless โดยเป็นตัวช่วยอำนวยความสะดวกในการทำ API และทำหน้า CMS สำหรับจัดการ API ให้แบบอัตโนมัติ

ทีนี้ก่อนจะไปต่อ เราต้องเข้าใจก่อนว่า “Headless CMS” เนี่ยมันคืออะไร

CMS คือ software application ที่ใช้สำหรับจัดการ digital content ซึ่งปกติก็ใช้สำหรับจัดการเว็บที่เกี่ยวข้องกับการแสดงเนื้อหาเช่น

  • Landing Page
  • Blog

ทีนี้ โดยปกติ Technology ใช้ทำ CMS นั้นมันก็มี software บางประเภทที่ได้เตรียม feature สำหรับจัดการ CMS ให้อยู่แล้ว ซึ่งปกติเราจะแบ่งเป็น 3 หมวดใหญ่ๆคือ

  1. Traditional CMS = ทั้งส่วนแสดง Content และ ส่วนจัดการ content อย่าง CMS จะรวมกันอยู่ใน application เดียวกัน
  • หมายความว่า ทั้ง Backend (ที่จัดการสร้าง / แก้ไข content) และ Frontend (ที่แสดงผล content) รวมอยู่ใน service เดียวกัน
  • CMS ที่มีไอเดียนี้เช่น Wordpress, Joomla
  1. Headless CMS = มีแค่ส่วน CMS และ API มาให้ ที่เหลือฝั่ง Frontend ต้องไปจัดการเอง (จึงเรียกสิ่งนี้ว่า Headless)
  • จึงทำให้มีความยืนหยุ่นสูงตอนทำ Frontend สำหรับการแสดงผล
  • API อาจจะเตรียมมาให้ในรูปแบบของ Rest API หรือ GraphQL
  • CMS ที่มีไอเดียนี้เช่น Strapi, Sanity, Wordpress Headless (*)
headless-cms

Ref: https://www.contentstack.com/cms-guides/headless-cms-vs-traditional-cms

เราสามารถใช้งานร่วมกับ Frontend ได้ยังไง ?

REST API/GraphQL
Fetch Data
Send Data
Strapi CMS
Next.js Frontend

ดังนั้นเมื่อ Strapi เป็น Headless CMS เท่ากับว่า

  • Content Management ทั้งหมดจัดการบน Strapi (แยกเว็บออกมาจัดการต่างหาก)
  • และหากต้องการ Content จาก Strapi ไปใช้ จะสื่อสารผ่าน API ที่เป็น protocal กลางแทน
  • ทำให้ทั้ง 2 Service แยกออกจากกัน = ไม่ขึ้นตรงต่อกัน ต่างคนต่างทำไป จะทำให้ไม่กระทบต่อกัน เวลามีการแก้ไขได้ (ยกเว้นแต่มีการแก้ไขส่วนของ API)

Overview feature ของ Strapi

Ref: https://docs.strapi.io/dev-docs/quick-start

มา Start project strapi + เล่นไปพร้อมๆกัน

Terminal window
npx create-strapi-app@latest my-project --quickstart
  • เลือกเป็น quickstart = ใช้ SQLite database ซึ่งเป็น database ที่เก็บเป็น file (อำนวยความสะดวกทำให้ไม่ต้อง setup database เพิ่ม)
  • เมื่อ command run เสร็จลงตาม step ให้เรียบร้อย (มันจะพาสร้าง admin คนแรก)
  • สร้างเสร็จจะเจอหน้าตาประมาณนี้ออกมา
strapi-cms-01

เล่น 3 แบบคือ

  1. แบบ Schema (Content แบบมี Schema ควบคุม)
  2. Single Component (เช่น landing page)
  3. ควบคุมการแสดงผลออกมาทั้งแบบ Authentication และไม่ Authentication
  • ความเจ๋งของ Strapi ก็คือ เราจะสามารถได้ API และ Service ออกมาจากการจัดการผ่าน UI ได้เลย ! = เราไม่ต้อง code สำหรับส่วนของ API เลย

เราจะทำอะไรกันบ้าง

แบ่งทำเป็น 2 path ครับ

Strapi = CMS

  • สร้าง 2 collections มาคือ
    • Blog = ให้คนทั่วไปใช้
    • Special Blog = เฉพาะคน Login ใช้ได้
  • สร้าง 1 collection สำหรับการใช้งานเพื่อ generate landing page

Next.js (แต่ง style เร็วๆด้วย daisyUI)

  • ดึงข้อมูล Blog มาแสดง
  • ทำ Login เพื่อทำการเช็ค Account กับ Strapi
  • ทำ Middleware เช็คว่า ต้อง Login เรียบร้อย ถึงจะดู Special Blog ได้

Deploy Strapi + Next.js ขึ้น Heroku + Vercel กัน (ให้เห็นภาพการนำไปใช้งานจริง)

** Require 1 อย่างก่อน follow ตาม = ควรดูหัวข้อ Next.js มาก่อน

เริ่มต้นทำจาก Strapi

Ref: https://docs.strapi.io/user-docs/intro

โจทย์

  • สร้าง 2 collections มาคือ
    • Blog = ให้คนทั่วไปใช้
    • Special Blog = เฉพาะคน Login ใช้ได้
  • สร้าง 1 collection สำหรับการใช้งานเพื่อ generate landing page

(อ่านตาม user guide หรือดูตามคลิปได้เลย)

Recheck Rest API

  • Login
Terminal window
curl --location 'localhost:1337/api/auth/local' \
--header 'Content-Type: application/json' \
--data-raw '{
"identifier": "mike@test.com",
"password": "Mike1234"
}'
  • Register
Terminal window
curl --location 'localhost:1337/api/auth/local/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"identifier": "mike@test.com",
"password": "Mike1234"
}'
  • Get Me
Terminal window
curl --location 'localhost:1337/api/users/me' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzAxNjYwMjE2LCJleHAiOjE3MDQyNTIyMTZ9.YlcLibPvOzkPoyAvbp2UGqAQAHdKBXIPGbJIguYqf2Q'
  • Get thumbnail
Terminal window
curl --location --globoff 'http://localhost:1337/api/blogs/1?populate[0]=thumbnail'

ต่อเข้ากับ nextjs

ที่ฝั่งของ Next.js นั้น

  • เนื่องจากเป็นการต่อ API แบบ Server-side อยู่แล้ว (จากหลักการของ Server component) = ไม่ต้องเปิด CORS อะไรออกมา
  • สามารถคุย API ผ่าน fetch หรือ axios ของ Server Component ได้เลย

Structure ของ next

.
├── README.md
├── app
│   ├── blog --> สำหรับแสดงผล Blog รายหน้า
│   │   └── [id]
│   │   └── page.js
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.js
│   ├── login --> สำหรับทำหน้า login (ด้วย Server action)
│   │   ├── action.js
│   │   └── page.js
│   ├── page.js --> หน้าหลัก
│   └── special-blogs --> สำหรับแสดงผล special blog หลังจาก login แล้ว
│   └── page.js
├── jsconfig.json
├── middleware.js
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│   ├── next.svg
│   └── vercel.svg
└── tailwind.config.js

เพื่อให้ project ลดจำนวน code ของการต่อ API = จะขอใช้ axios สำหรับการดึง API ออกมา

Terminal window
npm install axios

และทำการเพิ่ม .env.local ไว้สำหรับการเรียกผ่าน path ของ STRAPI โดยกำหนด .env.local เป็น

Terminal window
STRAPI_BASE_URL=http://127.0.0.1:1337

code หน้าหลัก

ที่ page.js

import axios from "axios";
import Link from "next/link";
const fetchBlogs = async () => {
try {
const response = await axios.get(`${process.env.STRAPI_BASE_URL}/api/blogs`);
return response.data.data;
} catch (error) {
console.log("error", error);
return [];
}
};
export default async function Page() {
const blogs = await fetchBlogs();
return (
<div>
Home page (Update new)
{blogs.map((blog, index) => (
<div className="flex gap-2" key={index}>
<div>ID: {blog.id}</div>
<div>{blog.attributes.title}</div>
<div>{blog.attributes.description}</div>
<div>
<Link href={`blog/${blog.id}`}>Read more...</Link>
</div>
</div>
))}
</div>
);
}

code ส่วน blog

ที่ blog/[id]/page.js

import axios from "axios";
const fetchBlog = async (id) => {
try {
const response = await axios.get(
`${process.env.STRAPI_BASE_URL}/api/blogs/${id}?populate[0]=thumbnail`,
);
return response.data.data;
} catch (error) {
console.log("error", error);
return [];
}
};
export default async function Page({ params }) {
const blog = await fetchBlog(params.id);
console.log("blog", blog);
return (
<div>
<img
src={`${process.env.STRAPI_BASE_URL}${blog.attributes.thumbnail.data.attributes.formats.thumbnail.url}`}
/>
{blog.id}
{blog.attributes.description}
</div>
);
}

code ส่วน login

  • ที่ login/page.js
"use client";
import { useFormState } from "react-dom";
import { login } from "./action";
export default async function Page() {
const initialState = {
message: null,
};
const [state, formAction] = useFormState(login, initialState);
return (
<div>
<form action={formAction}>
<div>
Email <input name="email" />
</div>
<div>
Password <input name="password" type="password" />
</div>
<button>Login</button>
<div>Message: {state?.message}</div>
</form>
</div>
);
}
  • ที่ login/action.js
"use server";
import { cookies } from "next/headers";
import { isRedirectError } from "next/dist/client/components/redirect";
import { redirect } from "next/navigation";
import axios from "axios";
export async function login(prevState, formData) {
try {
const email = formData.get("email");
const password = formData.get("password");
console.log("email", email);
console.log("password", password);
const response = await axios.post(`${process.env.STRAPI_BASE_URL}/api/auth/local`, {
identifier: email,
password,
});
if (response.data.jwt) {
cookies().set("token", response.data.jwt);
}
} catch (error) {
return { message: error.message || "Failed to create" };
}
redirect("/special-blogs");
}

code ส่วน middleware และ special blog

ที่ middleware.js

  • ต้องมี token ก่อน
  • และ token ต้องสามารถดึงข้อมูลจาก api/users/me ถึงจะอนุญาตให้เข้าไปได้
  • ท้ายสุด เอาข้อมูล user แนบไปกับ header
import { NextResponse } from "next/server";
// This function can be marked `async` if using `await` inside
export async function middleware(request) {
try {
let token = request.cookies.get("token");
let response = await fetch(`${process.env.STRAPI_BASE_URL}/api/users/me`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token.value}`,
},
});
if (!response.ok) {
throw new Error("Request error");
}
const payload = await response.json();
const requestHeaders = new Headers(request.headers);
requestHeaders.set("user", JSON.stringify({ email: payload.email }));
response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
return response;
} catch (error) {
console.log("error", error);
return NextResponse.redirect(new URL("/", request.url));
}
}
// See "Matching Paths" below to learn more
export const config = {
matcher: "/special-blogs/:path*",
};

ที่ special-blogs/pages.js

  • มีการใช้ token เพิ่มเพื่อดึงข้อมูล
import { headers, cookies } from "next/headers";
import axios from "axios";
const fetchSpecialBlogs = async () => {
try {
const token = cookies().get("token");
const response = await axios.get(`${process.env.STRAPI_BASE_URL}/api/special-blogs`, {
headers: {
Authorization: `Bearer ${token.value}`,
},
});
return response.data.data;
} catch (error) {
console.log("error", error);
return [];
}
};
export default async function Page() {
const headersList = headers();
const user = JSON.parse(headersList.get("user"));
const blogs = await fetchSpecialBlogs();
return (
<div>
<div>Hello {user.email}</div>
Special Blog
{blogs.map((blog, index) => (
<div className="flex gap-2" key={index}>
<div>ID: {blog.id}</div>
<div>{blog.attributes.title}</div>
<div>{blog.attributes.description}</div>
</div>
))}
</div>
);
}

และทั้งหมดก็จะได้ผลลัพธ์ตามที่เราต้องการออกมาได้

Deploy จริง

  1. strapi ใช้ heroku (เพราะ deploy ง่ายสุด)
  1. Next.js ใช้ vercel
  • deploy เหมือนหัวข้อของ Nuxt
  • ปรับ ENV ของ url สำหรับการต่อให้ถูกต้อง

Reference

https://medium.com/@themaxaboy/%E0%B8%AD%E0%B8%AD%E0%B8%81%E0%B9%81%E0%B8%9A%E0%B8%9A%E0%B8%AA%E0%B8%A3%E0%B9%89%E0%B8%B2%E0%B8%87-apis-%E0%B9%81%E0%B8%9A%E0%B8%9A%E0%B9%82%E0%B8%84%E0%B8%95%E0%B8%A3%E0%B9%80%E0%B8%A3%E0%B9%87%E0%B8%A7-%E0%B8%88%E0%B8%B1%E0%B8%94%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B9%80%E0%B8%99%E0%B8%B7%E0%B9%89%E0%B8%AD%E0%B8%AB%E0%B8%B2%E0%B9%81%E0%B8%9A%E0%B8%9A%E0%B9%82%E0%B8%84%E0%B8%95%E0%B8%A3%E0%B8%87%E0%B9%88%E0%B8%B2%E0%B8%A2-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-strapi-headless-cms-da907437040

Related Post

Share on social media