Creating a Static Blog with Next.js, microCMS, GitHub Actions, and GitHub Pages (Translated by ChatGPT)

2024-12-29T14:05:04.928Z

Introduction

When dealing with corporate websites or blogs that only have a small amount of dynamic content, it becomes easier for non-engineers to manage the site. By eliminating the need for server management and allowing content to be edited via a headless CMS, changes are directly reflected on the site. I decided to give it a try.

Created Static Site

Structure

Structure GitHub Actions are triggered:
・When changes are made to the main branch ・When content is updated in microCMS The generated static files are deployed to GitHub Pages.

DevelopmentEnvironment

Node.js v18.16.1
next.js 13.4.10
(Node.js was developed locally using nodebrew.)

Application

SSG Support

・Add the output configuration to next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
    output: "export",
}

module.exports = nextConfig

・Remove the Image tag as it won't be used: .eslintrc.json

{
  "extends": "next/core-web-vitals",
  "rules": {
      "@next/next/no-img-element": "off"
  }
}

Creating an Environment File

Create a .env file and add the API key and service endpoint:

NEXT_PUBLIC_API_KEY=xxxxxxxxxxx
NEXT_PUBLIC_SERVICE_DOMAIN=xxxxxxxxxxx

Adding Libraries

・@types/marked 5.0.1
・microcms-js-sdk 2.5.0

Github

Creating a Repository

The repository name must be [user].github.io.

Obtaining a Token

Generate a token for setting up a Webhook in microCMS. https://github.com/settings/tokens

The scope you should select is:

  • public_repo

only.
This might go without saying, but if the access token expires, GitHub Actions will stop running when you update article content in microCMS.

microCMS

Create a microCMS account using the free plan. You can create up to three APIs for free, but for this blog, one API will suffice for implementing both list and detail features.

Setting Up a Webhook

Input the token created in GitHub into microCMS, and configure it to trigger GitHub Actions when operations are performed.

EnvironmentVariableSetup

To avoid storing API information in the repository, store the API key and service endpoint created in microCMS in:
・The application's .env file ・The repository's "Settings" → "Secrets and variables" → "Actions"

GithubActions

GithubPages

Under "Settings" → "Pages", set the Source to GitHub Actions. The theme option will automatically recognize it as Next.js.

Adding Authentication to nextjs.yml

Before committing the default nextjs.yml file generated by GitHub Actions, add authentication information. Also, add repository_dispatch to ensure GitHub Actions is triggered by microCMS operations. After editing and committing the file, the build process will begin.

nextjs.yml

# Sample workflow for building and deploying a Next.js site to GitHub Pages
#
# To get started with Next.js see: https://nextjs.org/docs/getting-started
#
name: Deploy Next.js site to Pages

on:
  repository_dispatch:
    types: [microCMSで命名したWebhook名]

  # Runs on pushes targeting the default branch
  push:
    branches: ["main"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Detect package manager
        id: detect-package-manager
        run: |
          if [ -f "${{ github.workspace }}/yarn.lock" ]; then
            echo "manager=yarn" >> $GITHUB_OUTPUT
            echo "command=install" >> $GITHUB_OUTPUT
            echo "runner=yarn" >> $GITHUB_OUTPUT
            exit 0
          elif [ -f "${{ github.workspace }}/package.json" ]; then
            echo "manager=npm" >> $GITHUB_OUTPUT
            echo "command=ci" >> $GITHUB_OUTPUT
            echo "runner=npx --no-install" >> $GITHUB_OUTPUT
            exit 0
          else
            echo "Unable to determine package manager"
            exit 1
          fi
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: "16"
          cache: ${{ steps.detect-package-manager.outputs.manager }}
      - name: Setup Pages
        uses: actions/configure-pages@v3
        with:
          # Automatically inject basePath in your Next.js configuration file and disable
          # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
          #
          # You may remove this line if you want to manage the configuration yourself.
          static_site_generator: next
      - name: Restore cache
        uses: actions/cache@v3
        with:
          path: |
            .next/cache
          # Generate a new cache whenever packages or source files change.
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
          # If source files changed but packages didn't, rebuild from a prior cache.
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
      - name: Install dependencies
        run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
      - name: Build with Next.js
        run: ${{ steps.detect-package-manager.outputs.runner }} npm run build
        env:
          NEXT_PUBLIC_API_KEY: ${{ secrets.NEXT_PUBLIC_API_KEY }}
          NEXT_PUBLIC_SERVICE_DOMAIN: ${{ secrets.NEXT_PUBLIC_SERVICE_DOMAIN }}
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v2
        with:
          path: ./out

  # Deployment job
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v2

Deployment

After a successful deployment, the site will be displayed at [user].github.io.

FetchingDataFromMicroCMS

・Create a file /src/app/post/[id]/page.tsx to route all requests to localhost:3000/post/**** to this file. ・Implement generateStaticParams to dynamically generate files for each article at build time. ・Use the marked library to convert markdown strings from microCMS into HTML for display.

Article Detail Page

import { createClient } from "microcms-js-sdk";
import { notFound } from "next/navigation";
import { marked } from 'marked';
import type {
  MicroCMSQueries,
  MicroCMSImage,
  MicroCMSDate,
 } from "microcms-js-sdk";

export const client = createClient({
  serviceDomain: process.env.NEXT_PUBLIC_SERVICE_DOMAIN || "",
  apiKey: process.env.NEXT_PUBLIC_API_KEY || "",
  customFetch: (input, init) => {
    if (typeof input === 'string') {
      const newInput = new URL(input)
      const time = new Date()
      newInput.searchParams.set('cacheclearparam', `${time.getMinutes()}`)
      return fetch(newInput.href, init)
    }
    return fetch(input, init)
  },
});

//ブログの型定義
export type Blog = {
  id: string;
  title: string;
  body: string;
  markdown: string;
  eyecatch?: MicroCMSImage;
 } & MicroCMSDate;

 // ブログ一覧を取得
export const getList = async (queries?: MicroCMSQueries) => {
  const listData = await client.getList<Blog>({
   endpoint: "blogs",
   queries,
  });
 
  return listData;
 };

export async function generateStaticParams() {
  const posts = await getList();

  return posts.contents.map((post:Blog) => ({
    id: post.id,
  }))
}

  // ブログ詳細を取得
export const getObject = async (contentId: string, queries?: MicroCMSQueries) => {

  const objectData = await client.get({
   endpoint: "blogs",
   contentId: contentId,
   queries,
  })
  .then((res) => {
    // console.log(res)
    return res;
  })
  .catch((res) => {
    return {
      notFound: true,
    };
  });
 
  return objectData;
 };

export default async function Post({
  params: { id },
}: {
  params: { id: string };
}) {
  const blog = await getObject(id);

  if(blog.id == undefined) {
    notFound();
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-start p-6 w-full">
      <div className="w-full place-items-center z-10 max-w-5xl justify-center font-mono flex">
        <p className="text-2xl font-bold font-mono flex justify-center border-b border-gray-300 w-auto rounded-xl border bg-gray-200 p-4 dark:bg-zinc-800/30">
          {blog.title}
        </p>
      </div>

      <div className="flex flex-col items-center justify-start ">
        <div className="mt-2">
          <p>{blog.revisedAt}</p>
        </div>
      </div>

      <div className="w-full sm:w-full md:w-5/6 lg:w-1/2">
        <div className="prose">
          <div className="py-5" dangerouslySetInnerHTML={{__html: marked(blog.markdown ?? "", {mangle:false})}}/>
        </div>
      </div>
    </main>
  );
};

Additional Notes

In my case, when copying articles from Hatena Blog to microCMS, I had to make adjustments for certain notations that didn’t function properly, such as:

  • Attaching images from Hatena Blog
  • Embedded URLs
  • Script display using "```" inside details tabs

These required rewriting to ensure proper functionality.