WordPressのカテゴリー・タグなどのデータをFirestoreに同期する
WordPressのカテゴリー・タグなどのデータをFirestoreに同期する
WordPressブログをNuxt+Firebaseに移行するシリーズ。

WordPressの記事データをFirestoreに同期する
WordPressブログをNuxt+Firebaseに移行するシリーズ。概要は↓[nlink id=6056] 今回は最初のステッ... 続きを読む
前回、記事を全て同期させたので、次は表示……と思っていろいろ作っていたのですが、
作業の中でカテゴリー・タグ・コメントのコレクションが必要となり、
その手順を行った後にFirebaseの表示する方が流れとしてわかりやすいと思ったので、記事の順番を入れ替えることにしました。
なので現在、第3回「記事表示・ルーティング編」と第4回「サイドバー編」がほとんど書きあがっています。
記事の同期
基本的には前回書いた通りですが、いろいろと必要なパラメータが増えたので完成版を丸ごと載せます。
function save_post_to_firestore($post) {
function get_first_image($post) {
if (has_post_thumbnail()) {
return get_the_post_thumbnail_url($post->ID, 'card-thumb');
} else if (preg_match('/<img.*?src=(["\'])(.+?)\1.*?>/i', $post->post_content, $imgurl)) {
return $imgurl[2];
} else {
return '/shared/images/noimage.png';
}
};
$url = "https://**********.cloudfunctions.net/addPost";
$categories = get_the_category($post->ID);
$tags = get_the_tags($post->ID);
$previous_post = get_previous_post();
$next_post = get_next_post();
$data = [
// base
'id' => $post->ID,
'author' => $post->post_author,
'slug' => $post->post_name,
'title' => $post->post_title,
'content' => array(
'text' => strip_tags($post->post_content),
'rendered' => $post->post_content,
),
'date' => $post->post_date,
'modified' => $post->post_modified,
// taxonomy
'categories' => $categories,
'category_slugs' => array_map('convert_tax_to_slug', $categories),
'tags' => $tags,
'tag_slugs' => array_map('convert_tax_to_slug', $tags),
// meta
'post_type' => $post->post_type,
'post_status' => $post->post_status,
'ping_status' => $post->ping_status,
'comment_status' => $post->comment_status,
'comment_count' => $post->comment_count,
'like_count' => get_post_meta($post->ID, '_liked', true),
// pager
'postnavi_prev' => !empty($previous_post) ? [
'id' => $previous_post->ID,
'title' => $previous_post->post_title,
'date' => $previous_post->post_date
] : null,
'postnavi_next' => !empty($next_post) ? [
'id' => $next_post->ID,
'title' => $next_post->post_title,
'date' => $next_post->post_date,
] : null,
'thumbnail_images' => array(
'medium' => get_the_post_thumbnail_url($post->ID, 'medium'),
'large' => get_the_post_thumbnail_url($post->ID, 'large'),
'full' => get_the_post_thumbnail_url($post->ID, 'full'),
'thumbnail' => get_first_image($post)
)
];
};
categoryとtagのslugのみを配列にしている理由は、array-containクエリで絞り込みするため。
読み取り回数を減らすためにpostnaviで前後記事のID、タイトル、日付を入れておきます。
like_countも一旦そのまま入れます。
thumbnail_imagesは配列で取りつつ、サムネイルがない場合の最初の画像も対応。
このブログ、メイン領域とサイドバーで画像を出し分けてるんですよね。記事の最初に載ってる画像、メイン領域で拡大するほどではないけどサイドバーに出したかったりするので。
【WordPress】記事内の最初の画像をサムネイルとして取得する方法【全サイズ・srcset&sizes対応版】 – Qiita
画像URLのことは後で考えます。
あと、Cloud Functionsでタイムスタンプに変換する際にタイムゾーンがズレます。
GCPのCloud Functionsでタイムゾーンを日本時間に設定する – 動かざることバグの如し
対処法はこの記事に書いてあるので参照してください。Firebase側から設定する方法は不明なので、Cloud Functionsのダッシュボードから。どうにかならないものなのか。というかCloud Functionsのサーバーも日本にできないのだろうか(設定見落としてるだけかも)。
固定ページの同期
Cloud Functions側
export const addPage = functions.https.onRequest(async (req, res) => {
const data = {
...req.body,
date: admin.firestore.Timestamp.fromDate(new Date(req.body.date)),
modified: admin.firestore.Timestamp.fromDate(new Date(req.body.modified)),
author: req.body.author ? Number(req.body.author) : null,
comment_count: req.body.comment_count ? Number(req.body.comment_count) : 0,
like_count: req.body.like_count ? Number(req.body.like_count) : 0
}
await db.collection('pages').doc(`${data.id}`).set(data);
res.send(req.body);
});
WP側はほとんど上と同じなので割愛。
カテゴリー・タグ
posts、pagesと同じようにfunction.phpに
function save_taxonomy_to_firestore($term) {
$url = "https://********.cloudfunctions.net/addTaxonomy";
$data = [
// base
'ID' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'term_group' => $term->term_group,
'term_taxonomy_id' => $term->term_taxonomy_id,
'taxonomy' => $term->taxonomy,
'description' => $term->description,
'parent' => $term->parent,
'count' => $term->count
];
/* 以下略 */
}
exportの際は
$categories = get_terms('category');
foreach ( $categories as $category ) {
save_taxonomy_to_firestore($category);
};
受け取る側のCloud Functionは
export const addTaxonomy = functions.https.onRequest(async (req, res) => {
const data = {
...req.body
}
const collectionName = req.body.taxonomy === 'category' ? 'categories' : 'tags';
await db.collection(collectionName).doc(`${data.ID}`).set(data);
res.send(req.body);
});
categoryとtag以外にカスタムタクソノミーを使っている方はcollectionNameをさらに分岐させてください。もしくは関数増やすか。
で、これで行けると思ったのですが、Cloud Functionsの呼び出し回数が多すぎてエラーが出ました。
100件以上あるタグで全てCloud Functionを叩いたのがダメだったようです。
このFirebaseの料金表、月合計ではなく100秒あたりの制限がある中での合計だったようです。
GoogleHomeアプリの無料運用のためのコスト計算とスケール感 – Qiita
この記事のFunction invocations per 100 seconds : 50が怪しい。Google公式の記述は発見できなかったのですが。
では、Cloud Functions側の処理はどのくらいまでやっていいのか? というと、
どうやら60秒。GCPの540秒という制限に比べると厳しく感じますが、冷静に考えてください、JavaScriptで何らかの処理に1分もかかった記憶ありますか?
これなら多少重い処理をさせても大丈夫そう、ということで、
カテゴリーは連想配列で投げることにしました。
PHP側は
function save_taxonomies_to_firestore($terms) {
$url = "https://**********.cloudfunctions.net/addTaxonomies";
$header = [
"Content-Type: application/json"
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => $header,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($terms),
CURLOPT_RETURNTRANSFER => true
]);
$response = curl_exec($ch);
curl_close($ch);
return;
};
Cloud Functionsはこんな感じ。
export const addTaxonomies = functions.https.onRequest(async (req, res) => {
const taxonomies = [
...req.body
];
await Promise.all(taxonomies.map(async tax => {
const collectionName = tax.taxonomy === 'category' ? 'categories' : 'tags';
await db.collection(collectionName).doc(`${tax.id}`).set({
...tax,
id: tax.term_id
});
}))
res.send(req.body);
});
どちらも細かい処理をしていないのでコードは簡潔になりました。タクソノミーはタイムスタンプの変換とかもないですし。
これでカテゴリー・タグでそれぞれ1回ずつ叩けば良くなったので、問題なく保存できるようになりました。
ということは記事の完全同期も配列で投げる必要になりそうですが、それはまあ頑張ってください。
月別アーカイブ
これ単体で記事にしたいくらい面倒だった。何が面倒かというと、wp_get_archives
がタグ付き文字列しか得られない。
$archives_data = [];
$my_archives = wp_get_archives(array(
'type'=>'monthly',
'show_post_count'=>true,
'post_type'=>'post',
'format'=>'custom',
'echo' => 0
));
$archives = explode("\n", preg_replace('/\t/', '', strip_tags($my_archives))); // return => 2019年11月 (6)
foreach($archives as $arc) {
preg_match('/(\d*)年(\d*)月.*\((\d*)/', $arc, $data);
if($data) {
$key = $data[1].str_pad($data[2], 2, 0, STR_PAD_LEFT);
$archives_data[$key] = [
'id' => $key,
'year' => $data[1],
'month' => $data[2],
'count' => $data[3]
];
};
};
echo json_encode(array_values($archives_data));
一旦全てのタグをstrip_tags
で除去した後、それを改行ごとに区切って再度配列にする。
そこからpreg_match
でyear, month, countを取って、ドキュメント名に使うyear-month形式のIDとセットで配列に入れる。
ランキング
私の場合、サイドバーに表示するランキングには、数ヶ月前からWordPressプラグインではなくGoogle Analytics APIを使っています。WordPressに依存したくなかったので。
Google Analytics APIを使ってWordPressの記事をランキング表示させる方法 | Tips Note by TAM
Node.jsでGoogle AnalyticsのPV数を取得 – Qiita
で、上記の記事のようにNode.jsを使って取ってくる(APIをCloud Functionに作る)のが正攻法なんですが、既にPHPで取るやり方を極めたのでそっちでやることにしました。
というよりも、毎日0時に定期実行でランキングを取ってくる、という処理をCloud Functionsでやろうと思うとたぶんBlaze Plan必要だし、アクセスのたびに取ってくるのも無駄だし、解約しないならロリポップのcron使う方が楽だという結論に。積極的にPHPに頼っていきます。
cron設定 / サーバー・プログラム / マニュアル – レンタルサーバーならロリポップ!
ロリポップはcron設定めっちゃ楽だし、月250円で5個まで登録できるので割といい感じです。
と思いきや地味に苦戦してしまった……。
- 絶対パスを指定する
- WordPressの関数を呼び出したい場合はwp-load.phpを最初に読み込む
という2点が重要です。特に2つ目が意外と出てこなかった。これは私がGoogle Analyticsから得たURLを元に記事IDを特定して、その記事データ(特にサムネイル)も詰めた形でFirestoreに送りたかっただけで、単にURL・アクセス数・ページ名といったAnalytics側にある情報を送る分にはWordPress要らないのですが。
順位を意味するsessions
またはpageviews
はNumber型で入れるのを忘れずに。それさえやっておけばorderbyで簡単に取れます。
コメント一覧
今のところ優先度が低いので一旦飛ばします。
一番の問題はどのようにWordPressとの同期を取るか。
1つはWP REST APIにPOSTしてWPから同期する。正攻法ですが、WordPressサイトは最終的にbasic認証で隠して完全なバックエンドにしたいという目標があるので、ややそぐわない。
2つ目はFirestoreにコメントを追加させ、それをトリガーにCloud FunctionsからWordPressに送る。これも良い感じですが、Cloud Functionsの無料枠では外部のAPIを叩けない。よってNG。
さてどうしたものかと考えていたところ、急に閃きました。
Firestoreからその日投稿されたコメントを表示するAPIを作り、毎日1回、cronでGETリクエストしてコメントがあればWordPressに追加する。
どうせWordPressはバックアップにしか使わないのだから即時実行でなくても良いはず。というか1日1回もコメント来ないし。
……いや、コメントはFirestoreで持てば良いだけでは? もしFirebaseを離れるならその時にまとめて移行すれば良いし。
まとめ
ここまで解説してきた同期を全て行うと、データ構造はこんな感じになります。
これでデータ的には十分ですね!
まあ実際のところ、サイドバーでこのコレクションほぼ全部からデータを取ってきて読み取り回数大丈夫なのかという疑問はあるし、
そういえばrankingに関してはpostsにranking_dateとsessionを付けて渡せば絞り込める気もしてきましたが、ひとまずこれで運用してみます。場合によっては一部だけWordPressから取得する選択肢もありそうだし。
次回はいよいよ実際に記事表示!
といってもFirestoreのクエリの取り方がわかっていればどうにでもなるので、そんなにボリュームないと思います。ページの分け方・ルーティング中心にやっていきます。