Habanero BeeにおけるGoogle スプレッドシートを用いたコンテンツ管理戦略
先日、自身が作成しているHabanero Beeというオープンソースのツールについて紹介させてもらいました。
今回は前回の続きという形で、Habanero Beeにおける技術的な箇所について少し書かせていただこうと思います。
Google スプレッドシートを利用したコンテンツ管理戦略
上にリンクを貼ったポストの中でも書いたとおり、 Habanero Beeではサイトの基本情報・メタ情報・コンテンツ情報をすべて一つのGoogle スプレッドシートファイルで管理するようにしています。
これについてはサイト作成者が一つのファイルだけでサイト情報をすべて管理できるという、管理のしやすさ、さらには使い慣れたGoogleスプレッドシートを用いることで、新たな知識習得の必要性をなくす(例えばダッシュボードの使い方を学ぶなど)、ということがまずはあります。
以上のような利用者側の管理コスト削減はもちろんのこと、副次的に実装者側の負担削減にも大いに貢献しています。
WordPressの管理画面を想像していただくとイメージしやすいかと思いますが、ダッシュボードの実装はログイン管理や権限管理を始め、実装量や気を使う箇所も非常に多く、個人開発レベルのリソースではスピード感を持って実装を行うのは大変です。
今回、Google スプレッドシートをコンテンツ管理用のツールとして用いることで、結果的に権限周りもログイン周りもすべてGoogle側に押し付ける形となり、そこについての実装はすべて省略できる形となりました。
Google スプレッドシートのAPI化
次に、実際にGoogle スプレッドシートに記入したサイト情報をいかにしてHabanero Bee側に渡すか?という話ですが、ここは単純にGoogle スプレッドシートをAPI化して利用することにしました。
実際にどのようにAPI化するのかについては、以下のドキュメントにまとめているので、ご覧いただけたらと思います。
Habanero Bee Google Apps Script | ウェブアプリとしてのリリース手順
上の手順を踏んで、Google スプレッドシートの内容を公開APIとしてデプロイすることで、下記のようなAPIが出来上がります。
このリンクにアクセスしていただくと、Habanero Beeのデモサイト用に作成したコンテンツ情報がJSON形式で返されます。
Habanero Bee Demo site data(Google Sheets API)
上のAPIですが、Google スプレッドシート的には下記のような情報が記載されています。
Habanero Bee Demo site data(Google スプレッドシート)
こちらの記述をAPIとして利用するために、下記のようなGoogle Apps Scriptを用いています。
(現時点でのコードをすべて載せます。なおこちらのソースコードは、このリポジトリ(habanero-bee-google-apps-script)で管理されています)
const getSheetData = sheetName => {
const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
const rows = sheet.getDataRange().getValues();
// Get keys(rows[0]) and Delete rows[0]
const keys = rows.splice(0,1)[0];
return rows.map((row) => {
const obj = {};
row.map((item, index) => {
obj[String(keys[index])] = String(item);
});
return obj;
});
}
const doGet = () => {
const general = getSheetData('general')[0];
const meta = getSheetData('meta')[0];
const content = getSheetData('content');
const data = { general, meta, content };
return ContentService.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON);
}
このdoGet
関数を使うことで、指定したURLにGETリクエストが行われた場合に指定の処理を行うことができます。
doGet
内で行われている処理はとても単純で、Google スプレッドシードに記載されている内容をJSON化して返しているだけです。
doGet関数の普段使いはパフォーマンス的に厳しい
ちなみにこのように doGet
関数を用いてAPI化するような手法ですが、レスポンスのスピードは大変遅いです。
つまりこの doGet
関数をサイトで直接用いるAPIなどのようにリアルタイムに使うことはおすすめできません。
ただ、Habanero Beeの場合、このAPIが呼ばれるのはサイトのビルド時と、yarn dev
コマンドを用いた開発時ぐらいのものです。
開発時はページアクセスのたびにこのAPIを叩くことになるため、動作がもっさりしていますが、ビルド時にはレスポンスのスピードはそこまで重要視されません。
一度サイトを生成してしまえば、すべての必要なデータは取得済みの状態となるためサクサク動きます。
以上のように doGet
関数の持つデメリットの部分については、Habanero Beeでは仕様上の観点から無視をすることができます。
Next.jsのGetStaticPathsとgetStaticPropsでの処理
さて、JSONとして返されたこちらのデータは、Habanero Bee側でのサイト生成時に利用されます。
前回も書いたとおり、Habanero BeeはNext.jsをベースにしています。
つまり、サイトのビルド時に getStaticPaths
や getStaticProps
内の処理でGoogle スプレッドシードから渡された情報をもとに 一覧ページ・詳細ページの作成
、付与されているタグに応じて タグページの作成
などがそれぞれ行われます。
具体的な処理を下記に載せます。
これはHabanero Beeのソースコードから一部を抜粋したもので、詳細ページ生成時のソースコード(habanero-bee/src/pages/[slug].tsx
)となります。
解説のため日本語のコメントを追加しています。
export const getStaticPaths: GetStaticPaths = async () => {
if (!process.env.SHEET_URL) {
throw new Error('BUILD ERROR: Setting the SHEET_URL is required.');
}
const { SHEET_URL } = process.env;
// Google スプレッドシートにアクセスしてJSON化されているサイト情報を取得している
const response = await fetch(SHEET_URL).then((r) => r.json());
// 各ページのパスを設定
const paths = response.content.map((c: Content) => `/${getSlugText(c.slug)}`);
return { paths, fallback: false };
};
type Params = {
params: {
slug: string;
};
};
export const getStaticProps: GetStaticProps = async ({ params }: Params) => {
if (!process.env.SHEET_URL) {
throw new Error('BUILD ERROR: Setting the SHEET_URL is required.');
}
const { SHEET_URL } = process.env;
// Google スプレッドシートにアクセスしてJSON化されているサイト情報を取得している
const response = await fetch(SHEET_URL).then((r) => r.json());
const { general, meta, content } = response;
// URLパスに応じたコンテンツを取得している
const contentData = content.find(
(c: Content) => getSlugText(c.slug) === params.slug
);
// サイトの本文をHTMLにレンダリング
// (サイトの本文はmarkdownに対応しているため、markdown -> amp htmlへの変換処理を挟んでいます)
contentData.renderedHTML = await renderAmpHTML(contentData.text);
// 後述しますが、サイトで利用している画像を一度ダウンロードしています。
contentData.downloadedImagePath =
contentData.imagePath &&
(await getDownloadedImagePath(contentData.imagePath));
// 前のページ・次のページに関するリンクの作成
const slugList = content.map((c: Content) => getSlugText(c.slug));
const targetPageIndex = slugList.indexOf(params.slug);
contentData.prevPageUrl =
targetPageIndex && `/${slugList[targetPageIndex - 1]}`;
contentData.nextPageUrl =
slugList.length > targetPageIndex + 1 &&
`/${slugList[targetPageIndex + 1]}`;
if (!isValidData(general, meta, new Array(contentData))) {
throw new Error('BUILD ERROR: Invalid sheet data');
}
return {
props: {
general,
meta,
contentData: contentData,
},
};
};
以上のような処理を用いて、ページの生成を行っています。
amp-imgタグの生成について
上のソースコード内コメントで、後述すると書いた画像のダウンロードについてはまた改めて別枠で書こうと思っていますが、簡単な概要だけこちらに書かせていただけたらと思います。
Habanero BeeはAMP対応のサイトを生成するため、コンテンツ内に画像が存在する場合、その画像を amp-img
タグに変換しなければなりません。
amp-img
タグは画像サイズの指定が必須のため、このビルドフェーズの間に、用いられている画像のダウンロード、ならびにサイズのチェックを行い、画像それぞれに対してamp-img
タグを生成します。
まだ最適化の余地は大いにありそうですが、この処理を行うことで付与されている画像をHabanero Beeで生成したサイト上に一緒にホスティングすることにしています。
これはなるべく違ったドメインへのアクセスを極力減らし、サイトパフォーマンスを上げるため、という意図もあります。
(ちなみにこの実装を行った前後でパフォーマンスを計測してみたところ、意外とそこまでパフォーマンス向上には繋がりませんでした。。。計測サイトが大きくなればもう少し改善率も高くなりそうですが、実際のところどうなんでしょう?下記に参考画像も貼ります)
imgタグからamp-imgタグへの変換ツール - img2amp-img
ちなみにimg
タグからamp-img
タグへの変換処理についてはこちらのnpmパッケージを用いています。
Habanero Beeで変換する必要があったため、急遽npmパッケージとして作成したものです。
もしimg
タグからamp-img
タグへの変換ツールがないものかとお探しの方がいらっしゃいましたら試してみてください。
リポジトリはこちらです。PRもお待ちしていますm(_ _)m
文章が長くなってきてしまったので今日はここらへんで区切ろうと思います。
個人開発したことについて書く場合、どうしても自身が作成した思い入れのある部分について語りたくなってしまうので、少々文章が長くなってしまう傾向にあるようです😅
Habanero Bee、気軽にAMP対応サイトを生成できて、なかなか便利なツールになっていると思うので、よろしければ試してみてください。
Issue・PRなどもお待ちしています。
リポジトリはこちらです。
Discussion