Next.js と Movable Type Data API を使用して Jamstack な Blog を作ってみる

Movable Type をヘッドレス CMS として使用し、Data API から取得したデータで Next.js による SSG (Static Site Generation)、所謂 Jamstack なサイトを作成してみようというお話。

この記事は 「Movable Type Advent Calendar 2020」、1日目の記事です。

Movable Type Advent Calendar、私はてっきり過去に何度か参加させていただいたと思い込んでいたんですが、どうやら今回がはじめてみたいです。初参加で 1日目の記事を書くのはどうなのかとは思いましたけども、新参者は他の方が書いてハードルが上がらないうちに終わらせておこうということで、1日目を選択させていただきました。

さて、前置きはこのへんで。今回は、Movable Type をヘッドレス CMS のように利用しつつ、Next.js の SSG (Static Site Generation) による、所謂 Jamstack な Blog サイトを作るっていうのをお勉強がてらにちょっと試してみたので、サンプルを交えつつ簡単に書いてみたいと思います。

目次

なぜこれを作ろうと思ったのか

Jamstack というと、基本的には Markdown、JSON、YAML といった形式のデータを、ローカルで作成、もしくは API から取得し、そのデータを使用してビルド → デプロイするわけですけども、API からデータを取得する場合、ヘッドレス CMS 以外では、脱・動的ページ生成という文脈で WordPress を使用する例 (よくあるパターンとしては、WordPress + GraphQL + Gatsby ですかね) というのはメジャーなものの、Movable Type を使用した例っていうのを見たことがなかったので試してみようかなと思ったのが最初。

もともとスタティックに HTML を生成する Movable Type を、わざわざ Jamstack で生成し直すのも微妙かなとも思いましたけど、Movable Type を記事データ投稿のためだけに使用、つまりヘッドレス CMS 的な使い方にしてしまって、Next.js で開発する方が、MT タグを書いていくより楽なケースってのもあるかなと思いますし、更新系のコンテンツを管理するために Movable Type が稼働する環境さえ 1つ用意しておけば (MovabeleType.net とかでもいいし)、実際のサイト自体はサーバレスで配信できるってのは、企業のコーポレートサイトや Blog みたいな用途なら便利なケースも多いかなと。

もちろん、記事を投稿したり、更新した際の自動ビルドとかどうすんの? みたいな話は別途解決しないといけないですけども。

ということで、ソースコード自体は雑ですが、参考までにということで実際に作ってみたサンプルをもとに解説します。

ちなみに、CodeSandbox に動作するサンプルを作っておきましたので、実際のソースコードや動作はそちらで確認してください。

また、サンプルのソースコードは GitHub にまとめてありますのでどうぞ。

前提

今回使用した環境は下記の通りです。基本的には本記事用のサンプルを書いていた、11月中旬時点で最新。

  • Movable Type 7 r.4701
  • Data API v4
  • Node.js 14.15.0 LTS
  • React 17.0.1
  • Next.js 10.0.1

稼働している Movable Type がすでに存在し、Node.js はすでに導入済みの前提になります。また、私は普段、npm を使用しているのでその前提で書いていますが、パッケージマネージャに yarn を使用している場合は読みかえてください。

必要なパッケージのインストール

まずは基本的なところですが、作業用ディレクトリに移動して、

  1. npx create-next-app blog-sample

しましょう。これで blog-sample という名前でアプリケーションのひな形が成されます。

  1. cd blog-sample

次に上記コマンドで作成されたディレクトリに移動して、

  1. npm run dev

を実行すると開発サーバが立ち上がりますので、ブラウザで localhost:3000 にアクセスしましょう。これで Next.js の初期ページ的なものが開けば OK です。

一旦 Ctrl + C で開発サーバを停止し、今回のサンプルにおいて、追加で必要なパッケージをインストールします。

Font Awesome の導入

機能的な部分とは全く関係ないんですが、今回のサンプルで 1箇所、Font Awesome を使用していますので、インストールします。

公式に React コンポーネントが提供されていますので、そちらを使用すると簡単です。下記のコマンドをそれぞれ実行してパッケージをインストールしましょう。

  1. npm install --save @fortawesome/fontawesome-svg-core
  2. npm install --save @fortawesome/free-solid-svg-icons
  3. npm install --save @fortawesome/react-fontawesome

本筋からは外れるため詳しくは書きませんが、

  1. import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'

で読み込んでおいて、次に使用したいアイコンを読み込みますが、例えば今回の作例の中で使用しているアイコン (angle-left) の場合、下記のように指定します。

  1. import { faAngleLeft } from '@fortawesome/free-solid-svg-icons'

法則的には、アイコンを読み込む際に使用する class 名、例えば angle-left アイコンであれば、fa-angle-left をローワーキャメルケース (LCC) に置き換えて faAngleLeft と記述する形ですね。

あとは、実際にアイコンを表示したい箇所で、

  1. <FontAwesomeIcon icon={ faAngleLeft } />

のように使えばよいと。

その他、詳しくは公式ドキュメントを確認してください。

ディレクトリ構成

ここまでの時点で、blog-sample ディレクトリ内は下記のような構成になっていると思います。以降は、blog-sample ディレクトリをプロジェクトのルートディレクトリとして話を進めます。

  1. blog-sample
  2. .next/
  3. node_modules/
  4. styles/
  5. globals.css
  6. Home.module.css
  7. pages/
  8. api/
  9. hello.js
  10. _app.js
  11. index.js
  12. public/
  13. favicon.ico
  14. vercel.svg
  15. .gitignore
  16. package-lock.json
  17. package.json
  18. README.md

このうち、今回のサンプルでは、下記のファイルは使用しないので削除してしまっても大丈夫です。

  • /styles/Home.module.css
  • /pages/api/hello.js
  • /public/vercel.svg

あと、

/public/favicon.ico/public/img/favicon.ico になるように img ディレクトリを作成して移動しておいてください。そうすると後述するソースコードとパスが一致します。

設定ファイル

まず、プロジェクトのルートディレクトリに、Next.js の設定を記述する next.config.js を新規作成して、下記のように設定を記述します。

  1. // next.config.js
  2. module.exports = {
  3. reactStrictMode: true,
  4. devIndicators: {
  5. autoPrerender: false,
  6. },
  7. trailingSlash: true,
  8. }

next.config.js に関するドキュメントは下記にありますが、trailingSlash: true という設定で、各ページ URL の末尾につける 「/」(トレイリングスラッシュ / Trailing Slash) を強制することができますので、今回はそのようにしています。別に必要ないっていう場合は書かなくても問題はないです。

また、npx create-next-app で生成されるアプリケーションは、サーバにファイルを転送する際、ドキュメントルートに配置される前提になっています。つまり、ファイル内で参照されるリソースのパスがすべて「/」から始まる絶対パスで表記されるわけですけども、ドキュメントルート以外 (例えば https://example.com/blog/ など) で公開する場合は、next.config.js に、下記の設定を記述しておくと、生成されるファイル内のパス表記が、指定した値から始まるようになります。

  1. basePath: '/blog',

basePath について詳しくは公式ドキュメントを確認してください。

次に、package.json をエディタで開き、

  1. "scripts": {
  2. "dev": "next dev",
  3. "build": "next build",
  4. "start": "next start"
  5. },

上記の記述から、"build": "next build" の部分を、"build": "next build && next export" に変更します。

  1. "scripts": {
  2. "dev": "next dev",
  3. "build": "next build && next export",
  4. "start": "next start"
  5. },

これで、ビルド時、out ディレクトリに、サイトに必要なファイル一式が、Static HTML Export されるようになります。

その他、必須ではありませんが、Autoprefixer などの設定をしておきたい場合は、合わせて記述しておきましょう。

  1. "browserslist": [
  2. "last 1 version",
  3. "IE 11"
  4. ],

Movable Type Data API について

Movable Type から記事データを取得するため、Data API のエンドポイントについて確認しましょう。Data API は Movable Type 7 で利用可能な v4 を使用します。

公式リファレンスは下記にありますが、今回行いたいのは 「記事一覧の取得」 ですので、「Entry Collection」 内の、「Get」 で説明されている方法に基づいてリクエスト URL を決めていきます。

まず、Data API のエンドポイントの基本となる URL は下記の通り。[mt-path] は Movable Type のインストールディレクトリです。

  1. https://example.com/[mt-path]/mt-data-api.cgi/v4/

これをベースに、記事一覧をリクエストするためのパスを足していきます。下記のような感じですね。

  1. https://example.com/[mt-path]/mt-data-api.cgi/v4/sites/[siteid]/entries

[siteid] には、記事一覧を取得したい対象ブログの blog_id が入ります。

blog_id が 「1」 であれば、下記のように。

  1. https://example.com/[mt-path]/mt-data-api.cgi/v4/sites/1/entries

あとは少し条件を足します。公開済みの記事だけ取得したいので status パラメータと、今回はとりあえず最新 5件分の記事を取得したいので、limit パラメータを付け加えましょう。

  1. https://example.com/[mt-path]/mt-data-api.cgi/v4/sites/[siteid]/entries?limit=5&status=Publish

もし、「mt」 というディレクトリに Movable Type がインストールされていて、対象としたいサイトの blog_id が 「1」 であれば、リクエスト URL は下記のようになります。

  1. https://example.com/mt/mt-data-api.cgi/v4/sites/1/entries?limit=5&status=Publish

Data API へのリクエスト URL が確定したら、プロジェクトのルートディレクトリ直下に .env ファイルを作成し、下記のように Next.js の環境変数として指定しておきます。

  1. MT_ENDPOINT_URL=https://example.com/mt/mt-data-api.cgi/v4/sites/1/entries

なお、リクエスト URL 末尾に付けた下記のパラメータは、後述するファイル内で後から指定するので一旦保留しておいてください。

  1. ?limit=5&status=Publish

Next.js の環境変数に関しては、下記に公式ドキュメントがあります。

ちなみに、今回のサンプルでは作っていませんが、カテゴリ別の記事一覧ページが作りたいといった場合、Movable Type Data API からは、下記のような URL でカテゴリ一覧が取得できるので (blog_id が 「1」 の場合の例)、

  1. https://example.com/mt/mt-data-api.cgi/v4/sites/1/categories

このデータから各カテゴリの category_id を取得しておいて、下記のようにカテゴリを指定して記事データを取得すればよいと思います (下記の例は、category_id が 「3」 の場合の例)。

  1. https://example.com/mt/mt-data-api.cgi/v4/sites/1/categories/3/entries

カスタムフィールドの使用

今回のサンプルで使用する (データを取得してくる対象の) ブログは、Movable Type 標準のフィールド (タイトルや本文など) に加えて、カスタムフィールドが 1つだけ設定されている前提になっています。

具体的には OGP 画像の URL を入力するカスタムフィールドが登録されており、記事ごとに、そこに OGP 画像の URL が入っているという前提で記事個別ページのソースコードが書かれていますので、ご自身の環境で試す場合は注意してください。この辺は環境に合わせて書き換える必要があると思います。

各ページの作成

下準備は終わったので、ここから、実際に各ページを作成していきましょう。

ちなみに、今回は styles 内にある各スタイルシートに関してはとりあえずのレイアウトを作るために適当に書いているだけなので特に触れません。具体的な内容は GitHub にあるサンプルコードを見ていただければわかると思います。

共通のページレイアウトを作成

各ページの作成を楽にするため、まずは全体の共通レイアウトを作ってしまいましょう。全ページ共通のヘッダやフッタはここに記述します。

プロジェクトのルートディレクトリに components ディレクトリを作成し、Layout.js ファイルを新規作成します。

Layout.js ファイルの中身は下記の通り。

  1. import React, { useEffect } from 'react'
  2. import Head from 'next/head'
  3. import Link from 'next/link'
  4.  
  5. import styles from '../styles/layout.module.css'
  6.  
  7. const Layout = (props) => {
  8.  
  9. const { title, description, url, image, type, children } = props
  10. const siteTitle = 'サンプルブログ'
  11.  
  12. const [isSticky, setIsSticky] = React.useState(false)
  13. useEffect(() => {
  14. const scrollAction = () => {
  15. if (150 > window.scrollY) {
  16. setIsSticky(true)
  17. } else {
  18. setIsSticky(false)
  19. }
  20. };
  21. document.addEventListener('scroll', scrollAction, {
  22. capture: false,
  23. passive: true,
  24. })
  25. scrollAction()
  26.  
  27. return () => {
  28. document.removeEventListener('scroll', scrollAction)
  29. }
  30. }, [])
  31.  
  32. return (
  33. <div className={styles.container}>
  34. <Head>
  35. <title>{title ? `${title} - ${siteTitle}` : siteTitle}</title>
  36.  
  37. <meta name="description" content={description} />
  38. <meta property="og:title" content={title ? `${title}` : siteTitle} />
  39. <meta property="og:site_name" content={siteTitle} />
  40. <meta property="og:url" content={url} />
  41. <meta property="og:description" content={description} />
  42. <meta property="og:image" content={image} />
  43. <meta property="og:image:secure_url" content={image} />
  44. <meta property="og:type" content={type} />
  45. </Head>
  46.  
  47. <div className={isSticky ? '' : 'isSticky'}>
  48. <header className={styles.header}>
  49. <h1 className={styles.siteTitle}>
  50. <Link href="/">{siteTitle}</Link>
  51. </h1>
  52. </header>
  53. </div>
  54.  
  55. <main>
  56. <div className={styles.main}>
  57. {children}
  58. </div>
  59. </main>
  60.  
  61. <footer>
  62. <div className={styles.footer}>
  63. <div className={styles.copyright}>
  64. <p><small>Sample blog</small></p>
  65. </div>
  66. </div>
  67. </footer>
  68.  
  69. </div>
  70. )
  71. }
  72.  
  73. export default Layout

このうち、下記の部分は、スクロールしたときにヘッダを固定するためのちょっとした仕組みですので、今回のサンプルにおける話の本筋とは関係ありません。不要なら削除しても大丈夫です (useEffect の読み込みも不要)。

  1. const [isSticky, setIsSticky] = React.useState(false)
  2. useEffect(() => {
  3. const scrollAction = () => {
  4. if (150 > window.scrollY) {
  5. setIsSticky(true)
  6. } else {
  7. setIsSticky(false)
  8. }
  9. };
  10. document.addEventListener('scroll', scrollAction, {
  11. capture: false,
  12. passive: true,
  13. });
  14. scrollAction()
  15.  
  16. return () => {
  17. document.removeEventListener('scroll', scrollAction)
  18. }
  19. }, [])

Layout.js では、HeadLink コンポーネントを使用するので読み込み。

  1. import Head from 'next/head'
  2. import Link from 'next/link'

OGP の値などは他のコンポーネントから取得して反映したいので、そのための指定をしておきます。

  1. const { title, description, url, image, type, children } = props
  2. const siteTitle = 'サンプルブログ'

Head 内に必要な記述を入れておきます。

  1. <Head>
  2. <title>{title ? `${title} - ${siteTitle}` : siteTitle}</title>
  3.  
  4. <meta name="description" content={description} />
  5. <meta property="og:title" content={title ? `${title}` : siteTitle} />
  6. <meta property="og:site_name" content={siteTitle} />
  7. <meta property="og:url" content={url} />
  8. <meta property="og:description" content={description} />
  9. <meta property="og:image" content={image} />
  10. <meta property="og:image:secure_url" content={image} />
  11. <meta property="og:type" content={type} />
  12. </Head>

後述する各ページの内容は、下記の部分に読み込まれます。

  1. <main>
  2. <div className={styles.main}>
  3. {children}
  4. </div>
  5. </main>

全ページ共通で head 要素内に入れたい内容を指定

次に pages ディレクトリ内にある _app.js ファイルをエディタで開きましょう。下記のようなソースコードになっていると思います。

  1. import '../styles/globals.css'
  2.  
  3. function MyApp({ Component, pageProps }) {
  4. return <Component {...pageProps} />
  5. }
  6.  
  7. export default MyApp

これを下記のように書き換えて保存します。

  1. import Head from 'next/head'
  2. import '../styles/globals.css'
  3.  
  4. function MyApp({ Component, pageProps }) {
  5. return (
  6. <>
  7. <Head>
  8. <link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
  9. </Head>
  10. <Component {...pageProps} />
  11. </>
  12. )
  13. }
  14.  
  15. export default MyApp

Head コンポーネントを追加し、ここに全ページ共通で、head 要素に入れたい内容を記述します。例えば、Google Analytics 用のコードなんかもここに記述すればよいでしょう。

次のセクションで説明している _document.js に記述する例が多いのですが、個人的には _app.js に記述する方がよいと思います。

日本語対応

Next.js で生成される HTML は、そのままだと html 要素に lang="en" が指定されてしまいます。今回作成するのは日本語サイトという前提ですので、その辺を設定します。

pages ディレクトリ内に _document.js ファイルを新規作成し、下記のようにソースコードを記述して保存しましょう。

  1. import Document, { Html, Head, Main, NextScript } from 'next/document'
  2.  
  3. class MyDocument extends Document {
  4. render() {
  5. return (
  6. <Html lang="ja" dir="ltr">
  7. <Head />
  8. <body>
  9. <Main />
  10. <NextScript />
  11. </body>
  12. </Html>
  13. )
  14. }
  15. }
  16. export default MyDocument

dir="ltr" の指定はお好みでですが、

  1. <Html lang="ja">

とすることで、生成される HTML ファイルの html 要素に lang="ja" が指定されるようになります。

Data API からデータを取得するための処理を実装

次のセクションから、実際のトップページ (記事一覧ページ) や各記事の個別ページを作成しますが、その前に、それらで繰り返し使うことになる Movable Type Data API からデータを取得する部分の処理を作っておきます。

プロジェクトのルートディレクトリに lib ディレクトリを作成し、api.js というファイルを新規作成しましょう。中身は下記のような感じです。

  1. const ENDPOINT = process.env.MT_ENDPOINT_URL
  2.  
  3. const fetchAPI = async (path) => {
  4. const res = await fetch(`${ENDPOINT}${path}`).catch((err) => { console.error(err) })
  5. const json = await res.json()
  6. return json
  7. }
  8.  
  9. const fetchAllPosts = () => {
  10. const params = [['limit', '5'], ['status', 'Publish']]
  11. const param = new URLSearchParams(params).toString()
  12.  
  13. return fetchAPI(`?${param}`)
  14. }
  15.  
  16. export const getAllPosts = async () => {
  17. const data = await fetchAllPosts()
  18. return data
  19. }

前述したセクションで環境変数に指定したエンドポイントの URL をここで読み込みます。

  1. const ENDPOINT = process.env.MT_ENDPOINT_URL

環境変数に URL をセットする際に、末尾のパラメータはあとで付けますと書きましたが、それが下記の部分です。

  1. const fetchAllPosts = () => {
  2. // ↓ここでパラメータをセット。あとから修正しやすいように
  3. const params = [['limit', '5'], ['status', 'Publish']]
  4. const param = new URLSearchParams(params).toString()
  5.  
  6. return fetchAPI(`?${param}`)
  7. }

getAllPosts()export することで、他の場所から簡単に呼び出すことができるようになります。

  1. export const getAllPosts = async () => {
  2. const data = await fetchAllPosts()
  3. return data
  4. }

トップページ (記事一覧ページ) の作成

そうしたら、やっとですが、記事の一覧ページとなるトップページを作成しましょう。

pages ディレクトリ内に index.js があると思いますので、それをエディタで開いて編集します。今回書いた実際のソースコードは下記の通り。

  1. import Link from 'next/link'
  2. import Layout from '../components/Layout'
  3. import styles from '../styles/top.module.css'
  4. import { getAllPosts } from '../lib/api'
  5.  
  6. export const getStaticProps = async () => {
  7. const allPosts = await getAllPosts()
  8. return {
  9. props: {
  10. posts: allPosts.items
  11. }
  12. }
  13. }
  14.  
  15. const Blog = (posts) => {
  16. return (
  17. <Layout
  18. title="トップページ"
  19. description="サンプルブログのトップページです。"
  20. url="https://example.com/"
  21. image="/img/ogp.png"
  22. type="website"
  23. >
  24. <div className={styles.pageHeader}>
  25. <h2>新着記事</h2>
  26. </div>
  27. <div className={styles.articleList}>
  28. <ul>
  29. {posts.posts.map((post) => (
  30. <li key={post.id}>
  31. <Link href={`/articles/${post.basename}`}>
  32. {post.title}
  33. </Link>
  34. </li>
  35. ))}
  36. </ul>
  37. </div>
  38. </Layout>
  39. )
  40. }
  41.  
  42. export default Blog

まず、先ほど作成した共通レイアウトを読み込みます。

  1. import Layout from '../components/Layout'

また、前のセクションで作成しておいた、Movable Type Data API からデータを取得するための処理もここで読み込みますね。

  1. import { getAllPosts } from '../lib/api'

getStaticProps を使用して、Movable Type Data API から JSON 形式のデータを取得します。

  1. export const getStaticProps = async () => {
  2. const allPosts = await getAllPosts()
  3. return {
  4. props: {
  5. posts: allPosts.items
  6. }
  7. }
  8. }

あとは取得したデータをページ内に反映していきましょう。

  1. const Blog = (posts) => {
  2. return (
  3. <Layout
  4. title="トップページ"
  5. description="サンプルブログのトップページです。"
  6. url="https://example.com/"
  7. image="/img/ogp.png"
  8. type="website"
  9. >
  10. <div className={styles.pageHeader}>
  11. <h2>新着記事</h2>
  12. </div>
  13. <div className={styles.articleList}>
  14. <ul>
  15. {posts.posts.map((post) => (
  16. <li key={post.id}>
  17. <Link href={`/articles/${post.basename}`}>
  18. {post.title}
  19. </Link>
  20. </li>
  21. ))}
  22. </ul>
  23. </div>
  24. </Layout>
  25. )
  26. }
  27.  
  28. export default Blog

Layout コンポーネントに、先ほど、Layout.js で受け取れるように指定した各データをセットしておきます。

  1. <Layout
  2. title="トップページ"
  3. description="サンプルブログのトップページです。"
  4. url="https://example.com/"
  5. image="/img/ogp.png"
  6. type="website"
  7. >

JSON データから必要なデータを取り出していきますが、今回、ここで使用しているのは、各エントリーの idbasenametitle です。

  1. <div className={styles.pageHeader}>
  2. <h2>新着記事</h2>
  3. </div>
  4. <div className={styles.articleList}>
  5. <ul>
  6. {posts.posts.map((post) => (
  7. <li key={post.id}>
  8. <Link href={`/articles/${post.basename}`}>
  9. {post.title}
  10. </Link>
  11. </li>
  12. ))}
  13. </ul>
  14. </div>

これで、各エントリーのタイトルがリスト表示され、basename をもとに、各タイトルにリンクが設定されることになります。

リンク部分は、Link コンポーネントを使用して下記のように記述していますが、今回は各記事のページが articles ディレクトリ以下に配置される前提で進めるためです。

  1. <Link href={`/articles/${post.basename}`}>

これでトップページが完了。次に各記事ごとのページを生成します。

各記事 個別ページの作成

pages ディレクトリ内に articles ディレクトリを作成し、その中に [slug].js を新規作成しましょう。

pages ディレクトリ内で、ファイル名に [] を附与することで 「ダイナミックルーティング (Dynamic Routes)」 が有効になります。その上で getStaticPaths によってパスを指定することで、ダイナミックルーティングされたページの SSG が可能です。

今回は [slug].js というファイル名にしていますが、これはそのページの用途によってわかりやすい名前でよいと思います。例えばカテゴリ別にページを生成するなら [category].js などとしてもよいでしょう。

[slug].js ファイルの中身は下記のような感じです。

  1. import Link from 'next/link'
  2. import Layout from '../../components/Layout'
  3. import styles from '../../styles/posts.module.css'
  4. import { getAllPosts } from '../../lib/api'
  5.  
  6. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
  7. import { faAngleLeft } from '@fortawesome/free-solid-svg-icons'
  8.  
  9. export const getStaticPaths = async () => {
  10. const allPosts = await getAllPosts()
  11. return {
  12. paths: allPosts.items.map((post) => ({
  13. params: {
  14. slug: post.basename
  15. }
  16. })),
  17. fallback: false
  18. }
  19. }
  20.  
  21. export const getStaticProps = async ({ params }) => {
  22. const allPosts = await getAllPosts()
  23. const slug = params.slug
  24. const result = allPosts.items.find((v) => v.basename === slug)
  25. return {
  26. props: {
  27. post: result
  28. }
  29. }
  30. }
  31.  
  32. const Slug = (post) => {
  33. return (
  34. <Layout
  35. title={post.post.title}
  36. description={post.post.excerpt}
  37. url={post.post.permalink}
  38. image={post.post.customFields[0].value}
  39. type="article"
  40. >
  41. <div className={styles.title}>
  42. <h2>{post.post.title}</h2>
  43. </div>
  44. <div className={styles.excerpt}>
  45. <p>{post.post.excerpt}</p>
  46. </div>
  47. <div className={styles.entryBody}>
  48. <div className={styles.body}
  49. dangerouslySetInnerHTML={{ __html: post.post.body }}
  50. />
  51. <div className={styles.more}
  52. dangerouslySetInnerHTML={{ __html: post.post.more }}
  53. />
  54. </div>
  55. <div className={styles.backHome}>
  56. <FontAwesomeIcon icon={faAngleLeft} />
  57. <Link href="/">Back</Link>
  58. </div>
  59. </Layout>
  60. )
  61. }
  62. export default Slug

記事一覧ページ同様に、getAllPosts を読み込みますが、lib/api.js までのパスが異なっているので気をつけてください。

  1. import { getAllPosts } from '../../lib/api'

まず、getStaticPaths を使用して Movable Type Data API から取得したデータをもとに、各記事ページのパスをに設定していきます。今回は、各記事の basename からパスを生成しますので、JSON データからその部分を抜き出してセット。

  1. export const getStaticPaths = async () => {
  2. const allPosts = await getAllPosts()
  3. return {
  4. paths: allPosts.items.map(post => ({
  5. params: {
  6. slug: post.basename
  7. }
  8. })),
  9. fallback: false
  10. }
  11. }

fallback: false とすることで、今回設定したパス以外の、存在しないパスにアクセスされたときは 404 を返すようにします。

次に記事ごとのページの中身ですが、上で設定したパスごとに該当する記事のデータを取得しないといけないので、そこをどうするか。

今回は、getStaticPaths から受け取った slug と同じ値の basename を持った記事を探す (find()) っていう方法にしてみました。

  1. export const getStaticProps = async ({ params }) => {
  2. const allPosts = await getAllPosts()
  3. const slug = params.slug
  4. const result = allPosts.items.find((v) => v.basename === slug)
  5. return {
  6. props: {
  7. post: result
  8. }
  9. }
  10. }

basename が絶対に重複しない前提のコードになってしまっているので、その辺はもう少し真面目に考えた方がよいかもしれませんけども、Movable Type って basename が重複しないようになってた気がするし、まぁ今回はサンプルなので気にしないということで。

  1. <Layout
  2. title={post.post.title}
  3. description={post.post.excerpt}
  4. url={post.post.permalink}
  5. image={post.post.customFields[0].value}
  6. type="article"
  7. >

ちなみに、上記部分の、image={post.post.customFields[0].value} 部分が、先に 「カスタムフィールドの使用」 セクションで書いたカスタムフィールドからデータを取得している部分です。ここはブログに設定されているカスタムフィールドの数や、カスタムフィールドの種類が異なる場合、修正が必要になるかと思いますし、同じように別のカスタムフィールドからデータを取得してページに反映することも可能ですので、色々と試してみるとよいかもしれません。

動作確認

さて、ここまできたら、再度開発サーバを立ち上げて、各ページが正しく表示されるかを確認してみましょう。

  1. npm run dev

して、ブラウザで localhost:3000 にアクセスします。

トップページに記事一覧が表示され、記事をクリックした時にきちんと記事のページに遷移できれば成功です。エラーが出る場合はエラーメッセージを確認して修正しましょう。

サンプルブログのトップページ表示例

上記は、当 Blog の最新記事 5件を取得して作ってみた、トップページ表示例です

静的コンテンツとしてエクスポート

動作確認ができたら、

  1. npm run build

しましょう。

  1. Page Size First Load JS
  2. / 2.63 kB 65.8 kB
  3. css/hoge.css 327 B
  4. /_app 0 B 63.2 kB
  5. /404 1.21 kB 64.4 kB
  6. /articles/[slug]
  7.  
  8. [...略...]
  9.  
  10. λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
  11. (Static) automatically rendered as static HTML (uses no initial props)
  12. (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
  13. (ISR) incremental static regeneration (uses revalidate in getStaticProps)

上記のようなメッセージと共に build & export が実行されます。エラーがなく処理が終了すれば、プロジェクトのルートディレクトリ直下に out ディレクトリが生成され、Web サイト一式が書き出されていると思います。


ということで、Movable Type Data API から取得したデータを使用して、Next.js で SSG してみるというお話でした。書いてみたら長かった。

Jamstack なサイトを開発する際に Movable Type を使用するケースっていうのはあまりなさそうな気もしますが、ひとつのサンプルとして参考になれば。

Movable Type.net さんなんかも、GraphQL に対応するとか API 側を強化した、低価格な 「ヘッドレス CMS」 プランとか出してみたら面白いんじゃないですかね。まぁ余計なお世話ですけども。

さて、今回参加させていただいた、「Movable Type Advent Calendar 2020」 ですが、明日以降、他の方が色々と参考になる記事を書かれると思いますので、ぜひチェックしてみてください。

記事をここまで御覧頂きありがとうございます。
この記事が気に入ったらサポートしてみませんか?