🖲️

レンタルサーバーでもmicroCMS + Next.js でプレビュー機能を使いたい (SSR未使用)

2022/04/26に公開

ニッチすぎる記事です。

現在、自分のHPを以下の構成で運用しているんですが、

静的サイトジェネレーター : Next.js
コード管理 / ビルド : GitHub / GitHub Actions
Headless CMS : microCMS
ホスティング : ヘテムルレンタルサーバー

プレビュー機能を使うのって、
「Vercel」 などのホスティングサービス前提なんですね。。
まぁ、そういう商売だから当たり前か。
https://nextjs.org/docs/advanced-features/preview-mode

Node.jsが使える環境でSSRさせないといけないらしいので、
当然といえば当然なんですが、
色々なサービスを横断していて、さくっと実装するにはハードルが高いと思いました。

そこで試行錯誤して、なんとか普通のレンタルサーバーでも、プレビュー機能を使えるように
してみました。

実績紹介ページでpreviewを実装しよう

実績紹介の詳細ページを公開前に目視で確認したいのでプレビュー機能が欲しい。
かつ、microCMSにログインしていないユーザーにもプレしたい。

一覧

/works/

詳細

/works/osigoto1/
/works/osigoto2/
.
.

ファイル構成と実際のコード

今回は、works詳細ページのプレビュー機能を作るので
ページのURLは以下にしようと思います。(これは自由です)

/preview/works

ファイル構成(関係あるやつだけ)

src/
 pages/
  works/
        [slug].tsx    // works詳細ページ(参考)
  preview/
         works.tsx // ← 新しく作るworks詳細プレビューページ

プレビューページ全体のコード

src/pages/preview/works.tsx

import React, { useEffect, useState } from 'react'
import { createClient } from 'microcms-js-sdk'

// type
import { Post } from 'types/index'

// components
import Loader from 'components/loader' // ローディング
import WorksDetailBody from 'components/worksDetailBody' // 詳細ページのコンポーネント

type FetchData = {
  post: Post | null                       // 現在の記事の情報
  prev: { slug: string } | null  // 1つ後の記事の情報 今回は関係無い
  next: { slug: string } | null  // 1つ前の記事の情報 今回は関係無い
}

// プレビューページのPage コンポーネント ////////////////////////////
export default function WorksPreview() {
    // loadingを制御
    const [isLoading, setIsLoading] = useState(true)
    // fetchDataを入れる
    const [postData, setPostData] = useState({} as FetchData)

  useEffect(() => {
    (async () => {
      // APIから記事データを取得
      const result = await previewWorksDetailProps()
      if (result?.post) {
          // プレビューのデータを取得出来たらデータをセット
        setPostData(result)
        setIsLoading(false)
        return
      }
      // データが取得出来なかったら、works top へ リダイレクト
      location.href = '/works/'
    })()
  }, [])

  if (isLoading) return <Loader />
  // ここは、「/works/[slug].tsx」 と同じものを表示するので、コンポーネント化しておきましょう
  // プレビューページなので、isPreviewを渡す
  return postData ? WorksDetailBody({ ...postData, isPreview: true }) : <></>
}

// microCMS のAPIから データを取得する関数 ////////////////////////////
const previewWorksDetailProps = async (): Promise<FetchData | null> => {

  // urlからqueryパラメータを取得
    const url = new URL(location.href)
  const searchParams = url.searchParams

  // プレビューに必要なパラメータのキー
  const targetParams = ['contentId', 'draftKey', 'microcmsApiKey']
  // queryパラメータを取得に3つの必要なパラメータがあるかチェック
  const isExistParams = targetParams.every((p) => {
    return searchParams.has(p)
  })

    // パラメータが足りなかったら処理を完了
  // postデータがnullが返るとリダイレクトされる
  if (!isExistParams) {
    console.log('Invalid param')
    return null
  }

    // microcms-js-sdkを使って、クライアントを作成
  // パラメータから、それぞれキーの値を取得
  const client = createClient({
    serviceDomain: 'hogehoge123',
    apiKey: searchParams.get('microcmsApiKey') ?? '',
  })
  const contentId = searchParams.get('contentId') ?? ''
  const draftKey = searchParams.get('draftKey') ?? ''

  // キーがあるので、下書き状態のデータを取得出来る
  const post = await client
    .get({
      endpoint: 'works',
      contentId, // これをセット
      queries: {
        draftKey, // これをセット
        fields: 'title,slug,url,url2,date,body,production_period,credit,category,technology,slider',
      },
    })
    .then((v) => {
      return v
    })
    .catch((err) => {
      return null
    })

  // postデータがnullが返ると処理を完了
  // リダイレクトされる
  if (post === null) {
    console.log("Couldn't get the data")
    return null
  }

   // ページャーの処理なので今回は関係無し start
  const pager: any[] = await Promise.allSettled([
    client
      .get({
        endpoint: 'works',
        queries: {
          draftKey,
          limit: 1,
          orders: '-date',
          filters: `date[less_than]${post.date}`,
          fields: 'slug',
        },
      })
      .then((v) => {
        return v?.contents?.length ? v.contents[0] : null
      })
      .catch((err) => {
        return null
      }),
    client
      .get({
        endpoint: 'works',
        queries: {
          draftKey,
          limit: 1,
          orders: 'date',
          filters: `date[greater_than]${post.date}`,
          fields: 'slug',
        },
      })
      .then((v) => {
        return v?.contents?.length ? v.contents[0] : null
      })
      .catch((err) => {
        return null
      }),
  ]).then((results) =>
    results.map((r) => {
      if (r.status === 'fulfilled') {
        return r.value
      }
      return null
    })
  )
   // ページャーの処理なので今回は関係無し end

   // 取得したpostデータを返却
  return {
    post,
    prev: pager[0] ?? null,
    next: pager[1] ?? null,
  }
}

microcmsのプレビュー機能を使用

https://blog.microcms.io/draftkey_and_preview/

ここにプレビューページへのURLを貼り付けます。

microcmsApiKeyは、.envファイルに貼っているやつです。
APIからGETする時の認証で必要になります。これを設定
↓↓↓↓↓↓↓↓

https://hogehoge123.site/preview/works?contentId={CONTENT_ID}&draftKey={DRAFT_KEY}&microcmsApiKey=●●●●●●●●●●●

設定画面

プレビュー画面

ボタンを押すと以下にリダイレクト

https://hogehoge123.site/preview/works?contentId=▲▲▲▲▲▲&draftKey=▼▼▼▼▼▼&microcmsApiKey=●●●●●●●●●●●

プレビューページが表示される(パラメータが無い場合はリダイレクト)

パラメータから、認証キーを取得 → それを元にAPIを叩いて必要な情報をGET!

念の為プレビューページはクロールされないようにする

public/robots.txt
User-agent: *
Disallow: /preview/
src/components/head.tsx
    // previewページはnoindexのメタを追加
  {isPreview && <meta name="robots" content="noindex" />}

今回の実装の懸念点(1)

今回は、worksのプレビューを作ったが、
APIごとに、previewページを用意しないといけないのが面倒。

今回の実装の懸念点(2)

必要なパラメータは全てGETパラメータから取得する必要があり
プレビューURLをシェアする時に 「X-MICROCMS-API-KEY」 がURLに出る。
これはセキュリティ的に大丈夫なのか、的な不安がある。
今回は、自分の小規模サイトだし問題なさそう。

公式ブログによると
POSTなどには、別途 「X-WRITE-API-KEY」 が必要なんですね。
https://blog.microcms.io/http_post_api/

それぞれのキーで権限が別れていているのでその点は安心で、Goodですね!!

もし他に懸念点などあったら教えて頂けるとありがたいです。

Discussion