JETBABY

プリザンターをヘッドレスCMSにする、その2

前回は歯切れの悪かったプリザンターをヘッドレスCMSにするというネタ。
悔しいので再挑戦です。

追記2020.10.26

アクセス負荷が高くなる可能性やセキュリティへの配慮などもするべきなので、今回の記事の内容を使用する場合はその環境で問題がないかを検討して使用してください。

他のやり方としては、

  1. プリザンターで記事を作成したときなどに、プリザンターのスクリプト機能を使ってデータを別の場所に保存 (Webサーバーや今回使用したFirebaseのストレージなど)
  2. その後、表示する側で先ほど保存したデータを取得

といったやり方にするのがいいかもしれません。

この追記はプリザンターの情報発信をしている、株式会社リーデックス様の小川さんよりアドバイスいただき掲載しました。

追記2020.10.23

記事アップした直後に見直したり確認したりして気づきました。

JavaScriptバージョンで使用したパッケージのうち2つ

request
request-promise-native

requestが2020/02/11に非推奨なっていたようで、依存するrequest-promise-nativeも非推奨で更新も今後行われません。
GitHubみたら書いてあるのですが、作成前のキャッチアップが足らなくてすみません。

代替えにはnode-fetchやaxiosを使えばいいかなと。
node-fetchのほうが0.4kbと軽いのでこっちがいいかな?ソース追記もしました。
どうでもいいけど、axios(アクシオス)って聞くとガンダム アスクレプオスを思い出してしまうのはおれだけですか?そうですか。

今回こそプリザンターにつながってやる

どうも、前回クラウド無料版のプリザンターを使ってうまくいかなくかった企画の続編。
その後「60日のデモ版ならフル機能使えるからAPI使えますよ」と情報もらいました。
なんですが、試したところCORSの問題でフロントからJavaScriptだけで直接APIからデータを持ってこれない。

サーバーサイドで何とかしないといけないのかというか、サーバーサイドで試したら結論としてはうごきました。

今回はサーバーサイドで動かす方法を2パターン試して、皆様にお届けいたします。

  1. PHPでサクッとやってみる
  2. フロントにこだわってGoogle Cloud FunctionsでJavaScriptだけで頑張る

この2つです。

プリザンターとは

詳しくはこれまでに書いた2つの記事「プリザンターをヘッドレスCMS代わりにする」、「プリザンターのUIをChrome拡張で自分好みにしてみよう」や、ローコード開発のできるプリザンターのサイトなんかを見てちょうだい!

ちなみに前回前々回含めて、プリザンターの宣伝記事か?と思われるかもしれませんが、安心?してください、宣伝ではありません。
お金は絡んでいません。
完全に意地と趣味です。
確かに中の人は知り合いですが、この件に関してはまじで勝手にやってます。
その方が気楽だから!

メリット

実際にプリザンターをヘッドレスCMSにするメリットってあるんでしょうか。

ただブログを作りたいとか、コーポレートサイトをCMSで管理したい、なんて場合はあまりメリットはないかもしれません。

しかし、既にプリザンターを業務で導入している会社などでは違ってきます!
普段使っている業務システムの一環でサイト更新ができるようになって、便利ということが考えられます。
サイト全部とはいかなくても、お知らせのコーナーを部分的になんてことも可能になるので便利です。
業務に携わる人も新しくCMSの使い方を覚える必要はなし!
普段使ってるプリザンターでサイト更新ができるのであれば、乗り越えるハードルも低いかもしれません。

CORSの問題

さて、実は前回デモ版ならAPIいけると教わってから、すぐに試したんですね。
適当なJavaScriptを書いて、プリザンターから情報取得するってのを。

ところがCORSの問題でデータ取ってこれない。 俺の間違いかな、と思って一応検証。

APIへの接続方が多少違うとはいえ、例えばMicroCMSからは普通に取れる。
※ヘッドレスCMSかつ同じ国産ということでMicroCMSで試した。というか以前から使ったことがあるので動くと思ってた。
他にもこのブログのWordPressの設定を

Access-Control-Allow-Origin: *

に一旦してWPのREST API試すとかしもいける。

ということでクライアントサイドからJavaScriptで接続が、サーバー側、APIの制限でできなんだろうと決めつけました。
違ったらきっとのこの記事を読んで、中の人が教えてくれる・・・と嬉しい。

ちなみにプリザンターは自分で任意のサーバーに設置することもできるので、その場合は気にしないでもよくなることもあるとは思います。
なんだけど、今回はあくまで株式会社インプリムさんが用意している環境を使う前提でね。

ということで、クライアントサイドだけで何とかしようとしたから失敗したのであって、サーバーサイドからめる分には結構気楽にいけると思いますよ。

試しましょう。

前提

プリザンター(SaaSデモ版)に用意されているAPIからデータを取得して、そのまま返してくれるAPIを作る。
フロント側は一覧画面と詳細画面の2つ。
詳細ページのURLをどうにかとかはしない。パラメータつけて済ます。
(実際になにか作るようならこの部分はリライトするなりなんなりで個別のURL生成する方がいいとは思います)
CSSは書かない。
説明は最低限だけ。Qiitaとかに書くとなればそうはいかないけど、自分のブログってプレッシャー少ないので楽っすね。

プリザンターにデータを用意する

メリットにも書いた通りの状況、業務のついでにブログなんかを更新するというていで、プリザンターにデータを作っておきます。
そのままんま、ブログとしました

プリザンターのトップ画面で「新規作成」 > 「標準」タブ > 「記録テーブル」テンプレートを選びました。 基本的にはどれを選んでもできると思いますが、ブログなんで記録かなと。

そこにデータを追加していきます。

ちなみに内容部分にHTMLって入るのかなと思ってためしたところ、HTML普通に書けました。 ブログ書きやすいね。
Markdownでかけたらもっといいですね、チラッ。

こんな感じでデータの用意ができました。 「あれ、なんか俺の知ってるプリザンターの画面と違うぞ?」と思った方がいましたら、先日書いたこちらの記事をご覧ください。
プリザンターのUIをChrome拡張で自分好みにしてみよう

PHPでさっくりとJSON取得する

手っ取り早く、変なこだわりもなければこれが早いかもしれません。 Firebaseを使ったJavaScriptバージョンと違って、自作のAPIのPHPファイルも自分のサイトなどサーバにひょいっとおいてしまえばいいだけですし。

ということでPHP。

先もまだあるのに、ここまでで結構長くなってきた。もうこっから先は簡単に書くよ。

ファイル構成はこれだけ。
簡単にファイルの説明書くと

  • api.php:API接続をしてJSONを返す自作APIのPHPファイル
  • list.html:一覧ページのHTML&JavaScript
  • detail.html:詳細ページのHTML&JavaScript

早速PHP部分から

<?php
/**
 * おんどとり WebStorage APIさんのソースを使わせてもらいました
 * <https://ondotori.webstorage.jp/docs/api/reference/api_sample.html>
 */

//URLからパラメータ取得
$id = $_GET['query'];

//プリザンターAPIのエンドポイント
$url = "<https://pleasanter.azurewebsites.net/api/items/".$id."/get>";

//プリザンターのAPIの指定のコンテンツタイプ
$headerdata = array(
	'Content-Type: application/json'
);

//POSTで送信するデータを配列で一時作成します。:送信データについてはAPIの仕様をご確認ください。
//※正しくJSON形式の文字列として生成するために標準関数を利用し、配列からjson形式へと変更しています。
$param = array(
	"ApiVersion" => "1.1",
	"ApiKey" => "プリザンターで発行されるAPIキー"
);

//送信データを配列からJSON形式に変更します。
$postdata = json_encode($param);

//curlを使ったHTTP POST送信処理
$ch = curl_init($url);
$options = array (
	 CURLOPT_POST           => true             //HTTP "POST" で送信する
	,CURLOPT_RETURNTRANSFER => true             //curl_execの返り値を文字列化する
	,CURLOPT_HEADER         => true             //ヘッダの内容も出力:HTTPステータスコード&レートリミット状況を確認する場合に必要
	,CURLOPT_HTTPHEADER     => $headerdata      //設定する HTTPヘッダフィールドの配列
	,CURLOPT_POSTFIELDS     => $postdata        //HTTP "POST" で送信するデータ
);
curl_setopt_array($ch, $options);

//結果を受信して、curlによる通信を閉じる
$response             = curl_exec($ch);                 //結果を文字列で格納
$response_info        = curl_getinfo($ch);              //結果に関する情報を格納
$response_code        = $response_info['http_code'];    //通信結果のHTTPステータスコード
$response_header_size = $response_info['header_size'];  //通信結果のヘッダサイズ
curl_close($ch);            //通信をクローズ

$response_body   = substr($response, $response_header_size);        //ボディ部分の切り出し

echo json_encode($response_body);

api.phpの中身はほぼおんどとり WebStorage APIに載っていたコードです。それをただただ記事取得する部分だけ抜き出しました。
本当はもっとね、元ソースのような配慮もいるとは思うのですが、それはそれと割り切り割り切り。
割り切れないよ!って方は是非抜粋元を熟読してください。

こんな使われ方されるとは思わないで公開しているでしょうが、使えるものは何でも使います。

これはPHPわかるとは言わないですね。でもね、車輪の再発明なんて今はしたくないんだ!とりあえず動けというスタンスなんだよ!こちとら。
そもそもコードを書く仕事はフロント中心な俺なので、PHPは何となくわかる程度。WordPressのカスタムくらいならちょっとはできるくらい。
そう、私はよく言えばゼネラリスト、悪く言えば器用貧乏。
また話それた。

話し戻して、GETでURLのパラメータからID取得して、それを使ってプリザンターAPIでデータとってくるだけの簡単なお仕事してるファイルです。

次はフロント側。
JavaScriptで取得して画面作るだけ。HTMLファイルの中にJavaScriptも書いちゃいます。

まずは一覧画面のindex.html

<!DOCTYPE html>
<html lang="ja">

<head>
	<meta charset="utf-8">
	<title>プリザンターをヘッドレスCMSにする、PHPで</title>
	<meta name="robots" content="NOINDEX,NOFOLLOW">
	<meta name="viewport" content="width=device-width">

	<script>
		window.addEventListener('DOMContentLoaded', function () {
			const pid = "プリザンターの表示するデータのID。今回ならブログ";
			const ctr = document.getElementById('container');

			fetch("./api.php?query=" + pid)
				.then(res => {
					return res.json()
				})
				.then(json => {

					const obj = JSON.parse(json)

					if (obj.Response.TotalCount >= 1) {
						const elem = document.createElement('ul');
						elem.id = 'contents';
						ctr.appendChild(elem);
						const contents = document.getElementById('contents')

						for (let i of Object.keys(obj.Response.Data)) {
							const html = '<li class="item-' + i + '">'
								+ '<a href="detail.html?query=' + obj.Response.Data[i].ResultId + '">'
								+ obj.Response.Data[i].Title
								+ '</a></li>'
							contents.insertAdjacentHTML('beforeend', html);
						}
					}

				}).catch(function (error) {
					console.log(error)
				})

		});

	</script>
</head>

<body>
	<h1>プリザンターでヘッドレスCMS、PHPで</h1>
	<main>
		<div id="container">
		</div>
	</main>
</body>

</html>

JavaScriptのfetchで作ったAPIからJSONを取得して、なんやかんHTMLに書き出しています。
各記事へのリンクは記事毎のIDをパラメータでつけてる

無事取得。

最後に詳細ページのHTML

<!DOCTYPE html>
<html lang="ja">

<head>
	<meta charset="utf-8">
	<title></title>
	<meta name="robots" content="NOINDEX,NOFOLLOW">
	<meta name="viewport" content="width=device-width">

	<script>
		window.addEventListener('DOMContentLoaded', function () {

			const query = location.search.substring(1);
			const queryArray = query.split('=');

			const pid = queryArray[1];
			const titleElem = document.getElementById('title');
			const bodyElem = document.getElementById('body');

			fetch("./api.php?query=" + pid)
				.then(res => {
					return res.json()
				})
				.then(json => {
					const obj = JSON.parse(json)
					document.title = obj.Response.Data[0].Title;
					titleElem.textContent = obj.Response.Data[0].Title;
					bodyElem.insertAdjacentHTML('beforeend', obj.Response.Data[0].Body);
				}).catch(function (error) {
					console.log(error)
				})

		});
	</script>
</head>

<body>
	<main>
		<h1 id="title"></h1>
		<div id="body"></div>
	</main>
</body>

</html>

詳細ではパラメータから記事のIDを取得して、それをAPIに投げて記事取得して表示。
一覧も、詳細も単純です。

こっちもOK!

これでとりあえず既存のコーポレートサイトなんかに表示させることも可能やね。
プリザンターでブログとかお知らせとか作ってください。

ちょっとフロントエンドっぽいことをする

さて、こっちの方が記事が長くなる。 ここまででも長いのに長くなる。

そもそもこの記事は最初、プリザンターのAPIをJavaScriptで叩いてパパっと終わらす予定だったわけですよ。
それは失敗に終わったけども、PHPでやっただけではなんか納得いかないね、と思うわけ。

で、どうしようかなと思案して、ちょいちょい遊んでるGoogle Firebaseを思いついた。
Firebase楽しいですよね。いろんな機能が手軽に使えて。使用されているプロダクトも順調に増えてるように思われますし。
あと名前がいいね、Fireって入ってるといいじゃんて思うのは自分の屋号がFireColorだからさ。

その中のFunctionsでJavaScriptでAPI作って、フロントもJavaScriptで取得すれば、まぁ、なんか当初の予定に近いし、今どきのフロントエンドっぽいよね、と。

AWSのLambdaやAzure Functionsでも同じことできるだろうけど、今回はFirebaseで使えるGoogle Cloud Functionsです。 ついでにフロントはFirebaseのHostingで。

ファイル構成はPHPの時よりごちゃごちゃとあるけど、やることはほぼ同じ。

Cloud FunctionsにAPIを作って、FirebaseにHostingしたHTMLとJavaScriptで動かす。

Firebaseのことや、Functions、Hostingなんかはググってもらえばわかると思うので割愛。

実際にやること

  1. Firebaseのアカウントを作ります。
  2. npmでFirebase-toolsをインストールしてfirebaseコマンドを使えるようにします
  3. firebase initで初期化してコードを書いて、デプロイ
  4. 動かす

こんな感じです。

今回はHostingにあげるフロント部分も作るので、別々にディレクトリ作って、それぞれ、APIとHTML&JavaScriptを作りました。

API作成前に必要なパッケージをインストール

$ npm i --s express request request-promise-native cors

express:node.jsのウェブアプリ用のMVCフレームワーク。いろんな機能があって便利ですが、今回はAPIの作成部分で使用します

request:HTTP/HTTPSリクエストの処理を簡単に書けるモジュール

request-promise-native:上記、requestにPromiseを追加するモジュール

cors:CORS周りをごにょごにょしてくれるモジュール

express、requestなんかは使わないでもできるけど、使った方が断然楽です。 特に外部APIと連携させるときは、マジ楽。 corsはもう面倒なCORS周りを楽にしてくれるので入れときましょう。セキュリティ的な配慮は忘れずに。 ここでも使えるものは何でも使う精神で、車輪の再発明なんてしないのです。 したいときもあるけどね。

2020.10.23追記

冒頭に書いたようにrequestの代わりにnode-fetch使います。

$ npm i --s node-fetch

でモジュール追加してください。後はソースに追記しておきます。

各ファイル構成はこんな感じ

Functions(API用)

Hosting(フロント)

まずはCloud Functionsにあげる部分

const functions = require('firebase-functions');
const express = require('express');
const fetch = require('node-fetch');//追記分
//const requestPromise = require('request-promise-native');非推奨なので使用をやめる
const cors = require('cors');

const app = express();

app.use(cors());

//追記分、node-fetchで動くように書き換え
const getPleasanter = async id => {

  const apiKey = {
    "ApiVersion": 1.1,
    "ApiKey": "プリザンターで発行されるAPIキー"
  };

  const opt = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(apiKey)
  }

  const result = await fetch("https://pleasanter.azurewebsites.net/api/items/" + id + "/get", opt)
    .then(res => res.json())
    .then(json => {
      return json;
    })
    .catch(function (error) {
      return error
    })

  return result;
}
//古い関数
/*
const getPleasanter = async id => {
  const apiKey = {
    "ApiVersion": 1.1,
    "ApiKey": "プリザンターで発行されるAPIキー"
  };

  const opt = {
    url: "<https://pleasanter.azurewebsites.net/api/items/>" + id + "/get",
    method: "POST",
    body: JSON.stringify(apiKey),
    headers: {
      'Content-Type': 'application/json'
    }
  }
  const result = await requestPromise(opt);
  return result;
}
*/

// テスト用のエンドポイント
app.get('/hello', (req, res) => {
  res.send('Hello! Pleasanter');
});

// プリザンターと接続するエンドポイント
app.get('/get/:id', (req, res) => {
  getPleasanter(req.params.id)
    .then(response => res.send(response))
});

const api = functions.https.onRequest(app);
module.exports = { api };

こいつをCloud functionsにデプロイしてやりましょう。
※2020.10.23 node-fetchで動くようにソースの修正を行いました。古いものを一応コメントアウトして書いてあります。

$ firebase deploy

だけでもデプロイできるけど

$ firebase deploy --only functions

ってfunctionsにだけやった方が早い気が何となくするけどどうなんだろ。

テスト用のエンドポイントはコンソールに「Hello! Pleasanter」と表示するだけですが、テストとしておいておきます。消しても問題ありません。

フロントのソース

例によってHTMLの中にJavaScriptも書いてしまいます。
そしてPHP版とほぼ一緒です。
APIの呼び出しが違うだけ。

まずは一覧、index.hrml

<!DOCTYPE html>
<html>

<head>
	<meta charset="utf-8">
	<title>プリザンターをヘッドレスCMSにする、JSで</title>
	<meta name="robots" content="NOINDEX,NOFOLLOW">
	<meta name="viewport" content="width=device-width">
	<script>
		document.addEventListener('DOMContentLoaded', function () {
			const pid = "プリザンターの表示するデータのID。今回ならブログ";
			const ctr = document.getElementById('container');
			//とりあえずテストのエンドポイントを呼び出してみる
			//今回ならhttps://us-central1-functions-○○○○.cloudfunctions.net/api/helloみたいな
			fetch("作ったAPIのエンドポイント")
				.then(res => {
					return res.text();
				})
				.then(text => {
					console.log(text)
				}).catch(function (error) {
					console.log(error)
				})
			//プリザンターからデートをとるAPIのエンドポイント
			fetch("作ったAPIのエンドポイント" + pid)
				.then(res => {
					return res.json();
				})
				.then(json => {
					if (json.Response.TotalCount >= 1) {
						const elem = document.createElement('ul');
						elem.id = 'contents';
						ctr.appendChild(elem);
						const contents = document.getElementById('contents')

						for (let i of Object.keys(json.Response.Data)) {
							const html = '<li class="item-' + i + '">'
								+ '<a href="detail.html?query=' + json.Response.Data[i].ResultId + '">'
								+ json.Response.Data[i].Title
								+ '</a></li>'
							contents.insertAdjacentHTML('beforeend', html);
						}
					}
				}).catch(function (error) {
					console.log(error)
				});
		});
	</script>
</head>

<body>
	<h1>プリザンターでヘッドレスCMS、JSで</h1>
	<main>
		<div id="container">
		</div>
	</main>
</body>

</html>

ほぼほぼ一緒ですね。

表示はこんな感じ

で、詳細ページ

<!DOCTYPE html>
<html>

<head>
	<meta charset="utf-8">
	<title></title>
	<meta name="robots" content="NOINDEX,NOFOLLOW">
	<meta name="viewport" content="width=device-width">

	<script>
		window.addEventListener('DOMContentLoaded', function () {

			const query = location.search.substring(1);
			const queryArray = query.split('=');

			const pid = queryArray[1];
			const titleElem = document.getElementById('title');
			const bodyElem = document.getElementById('body');

			fetch("作ったAPIのエンドポイント" + pid)
				.then(res => {
					return res.json();
				})
				.then(json => {
					document.title = json.Response.Data[0].Title;
					titleElem.textContent = json.Response.Data[0].Title;
					bodyElem.insertAdjacentHTML('beforeend', json.Response.Data[0].Body);
				});

		});
	</script>
</head>

<body>
	<main>
		<h1 id="title"></h1>
		<div id="body"></div>
	</main>
</body>

</html>

こちらもPHP版とほとんど一緒。
違うのはURLのパラメータの取得のところくらい。

こちらもきちんと表示されました。

こんな感じで無事に取得と表示ができました。

あとは目的に合わせてカスタムしてやればOKですね。

これにてJavaScript版も無事完成として、今回の目的は達成です!

これは注意

やってみて気づいた、注意点。

Firebaseのプランには無料のSparkと従量課金のBlazeがあります。

今回やろうとすることはBlazeプランにしないとできません。外部のAPIにアクセスとかは無料プランではできないんですね。
いままでGoogleのサービス内でしかつなげたことないから気づかなかった。

そしてどのみち来年からBlazeプランじゃないといろいろと制限付きそうなので、Blazeにしてしまえばいいと思います、使う人は。

BlazeにしたところでSparkと同じだけの無料使える範囲があるので、がっつりアプリ作るとかじゃなければ課金されないと思いますが、保証はしません。

ちなみにこちらはこのブログに記事アップしたころの状態。テストでAPI叩きまくったけど、こんなもんでした。

フロントにFirebaseのHosting使おうと思ったのは、Functionsと連携させやすいかなという理由。
その関係でリージョンがus-central1なんですが、そのせいで動作がちょっと遅い気がするのも反省。
最終的に表示側のHTMLはどこにおいても簡単に動くものになってしまい、それらなFunctionsのリージョンはasia-northeast1(東京)にすればよかったなと。別に変更可能だけどしてないだけなんだけど。

あとは普段、WindowsでVScode内でPowerShellでコマンド叩いてるんですが、Firebase-toolのインストール時に「このシステムではスクリプトの実行が無効に~」というメッセージが出ることがあります。
この時はPowerShell の実行ポリシーを変更してやればOKです。詳細はリンク先でどうぞ。

最後に注意ではないのですが、これやった後デモ版ではなくて無料のクラウド版(最初に失敗したやつね)につないでみたらデータ取れました。
あれ?無料版はAPI制限あるはずだけどな。まぁ、取れる分にはいいんですけどね。

まとめ

これでプリザンター連載?はいったん完結です。

あとはChrome拡張のほうの反響で、コメントや直接メッセージいただき、ソースみたいと言われましたので整理しようて何とか公開しようと思います。い・づ・れ!

あ、ソースのコメント少ないですか?少ないです!シンタックスハイライト的なのないから見にくい?見にくいね! ごめんなさいね。

それにしても、今回の記事はプリザンター使う人たちにとって、少しのアイデアで活用できることを増やすのに役に立つんじゃないでしょうか。たってくれ。

もともとプリザンターのOSSを自分たちで設置できるような人たちには無用の長物な気もします。
が、そうでない人たちにとってはプリザンターの使い道の幅が出せていいのではないでしょうか。

あとは、GoogleのFirebaseは気軽にいろいろな機能が使えて楽しいし、無料枠もたっぷりあるし、料金そこまで高くないので、遊ぶ人が増えるといいなと思います。

今回の内容でもなにかありましたらコメントなりメッセージなりお待ちしています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

最近の投稿

カテゴリー

アーカイブ