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.
- Structure
- Development Environment
- Application
- Github
- microCMS
- Environment Variable Setup
- GithubActions
- Deployment
- Fetching Data from microCMS
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.