keyboard_tabSkip to content
arrow_back All Posts

Dec 6, 2023

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

ภาคต่อจาก Next.js เราจะลองนำ Next.js มาสร้างเว็บ Content จริงๆกันผ่าน Strapi

สารบัญ

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 ได้ยังไง ?

Loading diagram…

ดังนั้นเมื่อ 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 + เล่นไปพร้อมๆกัน

bash
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
bash
curl --location 'localhost:1337/api/auth/local' \
--header 'Content-Type: application/json' \
--data-raw '{
    "identifier": "[email protected]",
    "password": "Mike1234"
}'
  • Register
bash
curl --location 'localhost:1337/api/auth/local/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "identifier": "[email protected]",
    "password": "Mike1234"
}'
  • Get Me
bash
curl --location 'localhost:1337/api/users/me' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzAxNjYwMjE2LCJleHAiOjE3MDQyNTIyMTZ9.YlcLibPvOzkPoyAvbp2UGqAQAHdKBXIPGbJIguYqf2Q'
  • Get thumbnail
bash
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 ออกมา

bash
npm install axios

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

bash
STRAPI_BASE_URL=http://127.0.0.1:1337

code หน้าหลัก

ที่ page.js

javascript
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

javascript
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
javascript
"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
javascript
"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
javascript
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 เพิ่มเพื่อดึงข้อมูล
javascript
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

forumComments (0)

lockLog in to join the conversation.

Log In

Loading comments…