Counts the Clouds

Headless Wordpressの勉強 (2)

Headless Wordpressの勉強〜プラグインの作成以降〜
2022.07.14

Next.js
GitHub Actions
WordPress

timothy-meinberg-0XU5EtH7O6s-unsplash

WordPressからGitHub Actionsをキックする(実装編)

前回の続き。

GitHub Actionsをキックするプラグインの作成

どういった操作でキックされたかわかるように細かくイベントを追加。

# frontend/.github/workflows/deploy.yml
# ...
- wp_github_actions_kicker_post_published
- wp_github_actions_kicker_post_trashed
- wp_github_actions_kicker_post_switched_to_draft
- wp_github_actions_kicker_page_published
- wp_github_actions_kicker_page_trashed
- wp_github_actions_kicker_page_switched_to_draft
# ...

プラグインを実装。記事および固定ページの追加・更新・削除に反応するようにしている。

<?php
// wordpress/plugins/github-actions-kicker/index.php
/*
  Plugin Name: GitHub Actions Kicker
  Plugin URI:
  Description: GitHub Actionのトリガー
  Version: 1.0.0
  Author: Yuji Ito
  Author URI:
  License: MIT
 */

if ( ! defined( 'ABSPATH' ) ) exit;

require_once(__DIR__ . '/src/settings.php');
require_once(__DIR__ . '/src/settings-page.php');
require_once(__DIR__ . '/src/hooks.php');
<?php
// wordpress/plugins/github-actions-kicker/src/hooks.php

if ( ! defined( 'ABSPATH' ) ) exit;

function on_post_published($post_id, $post) {
  error_log('---post published---');
  dispatch_github_actions($post_id, $post, 'wp_github_actions_kicker_post_published');
}

function on_post_trashed($post_id, $post) {
  error_log('---post trashed---');
  dispatch_github_actions($post_id, $post, 'wp_github_actions_kicker_post_trashed');
}

function on_post_switched_to_draft($post_id, $post) {
  error_log('---post switched to draft---');
  dispatch_github_actions($post_id, $post, 'wp_github_actions_kicker_post_switched_to_draft');
}

function on_page_published($post_id, $post) {
  error_log('---page published---');
  dispatch_github_actions($post_id, $post, 'wp_github_actions_kicker_page_published');
}

function on_page_trashed($post_id, $post) {
  error_log('---page trashed---');
  dispatch_github_actions($post_id, $post, 'wp_github_actions_kicker_page_trashed');
}

function on_page_switched_to_draft($post_id, $post) {
  error_log('---page switched to draft---');
  dispatch_github_actions($post_id, $post, 'wp_github_actions_kicker_page_switched_to_draft');
}

function dispatch_github_actions($post_id, $post, $github_event_type) {
  error_log('post send: ' . $post_id);
  // error_log(print_r($post, true));
  $header = [
    'Authorization: token ' . esc_attr(get_option('github_token')),
    'Accept: application/vnd.github.everest-preview+json',
    'User-Agent: WordPress_webhook_post', // You must specify user-agent
  ];
  $data = [
    'event_type' => $github_event_type,
  ];
  $url = curl_init('https://api.github.com/repos/' . esc_attr(get_option('github_account'))
      . '/' . esc_attr(get_option('github_repository')) . '/dispatches');

  if ($post->post_status === 'publish') {
    curl_setopt($url, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($url, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($url, CURLOPT_HEADER, true);
    curl_setopt($url, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($url, CURLOPT_HTTPHEADER, $header);
    curl_exec($url);
  }
  if (!(defined( 'REST_REQUEST' ) && REST_REQUEST )) {
    error_log(REST_REQUEST);
  }
}

add_action('publish_post', 'on_post_published', 10, 2);
add_action('trash_post', 'on_post_trashed', 10, 2);
add_action('draft_post', 'on_post_switched_to_draft', 10, 2);
add_action('publish_page', 'on_page_published', 10, 2);
add_action('trash_page', 'on_page_trashed', 10, 2);
add_action('draft_page', 'on_page_switched_to_draft', 10, 2);
<?php
// wordpress/plugins/github-actions-kicker/src/settings-page.php

if ( ! defined( 'ABSPATH' ) ) exit;

function github_actions_settings_page()
{
    if (true == $_GET['settings-updated']) { ?>
        <div id="settings_updated" class="updated notice is-dismissible">
            <p><strong>設定を保存しました。</strong></p>
        </div>
    <?php } ?>

    <div class="wrap">
        <h2>設定</h2>
        <form method="post" action="options.php">
            <?php settings_fields('github-actions-settings'); ?>
            <?php do_settings_sections('github-actions-settings'); ?>
            <table class="form-table">
                <tr>
                    <th>Github アカウント</th>
                    <td>
                        <input
                            type="text"
                            name="github_account"
                            id="github_account"
                            value="<?= esc_attr(get_option('github_account')); ?>"
                            required="required"
                        />
                    </td>
                </tr>
                <tr>
                    <th>リポジトリ名</th>
                    <td>
                        <input
                            type="text"
                            name="github_repository"
                            id="github_repository"
                            value="<?= esc_attr(get_option('github_repository')); ?>"
                            required="required"
                        />
                    </td>
                </tr>
                <tr>
                    <th>Personal access tokens</th>
                    <td>
                        <input
                            type="password"
                            name="github_token"
                            id="github_token"
                            value="<?= esc_attr(get_option('github_token')); ?>"
                            required="required"
                        />
                    </td>
                </tr>
            </table>
            <?php submit_button('設定を保存', 'primary large', 'submit', true, array('tabindex' => '1')); ?>
        </form>
        <div id="pluginurl" data-pluginurl="<?= esc_attr(plugins_url()); ?>"></div>
    </div>

<?php } ?>
<?php
// wordpress/plugins/github-actions-kicker/src/settings.php

if ( ! defined( 'ABSPATH' ) ) exit;

function create_menu() {
  add_options_page(
    'Github Actions',
    'Github Actions',
    'administrator',
    'github_actions_setting',
    'github_actions_settings_page'
  );
  add_action( 'admin_init', 'github_actions_register_settings' );
}

add_action( 'admin_menu', 'create_menu' );

function github_actions_register_settings() {
  register_setting( 'github-actions-settings', 'github_account' );
  register_setting( 'github-actions-settings', 'github_repository' );
  register_setting( 'github-actions-settings', 'github_token' );
}

プラグインができたら、実際にWordPress管理画面の設定を見てみる。

プラグインの設定画面

アカウント名、リポジトリ名、Personal Access Tokenをそれぞれ入力して保存。記事および固定ページの追加・更新・削除に反応してGitHub Actionsがキックされる。

GitHub Actionsがキックされた様子

プラグインをレンタルサーバーに転送して再確認。今回はscpで転送した。レンタルサーバーからもGitHub Actionsをキックすることができた。

WordPressのプレビューをNext.jsで見れるようにする

WordPress側はfunctions.phpでプレビューボタンを押したときの動作をフックする。今回は、記事のプレビューだけ行う。

カスタムテーマを追加

% mkdir -p themes/headless-wordpress-research && touch $_/{index.php,functions.php,style.css}

テーマは既存のプラグインとかぶらないようにする。Headlessプラグインはすでに存在するのでテーマ画面にアップデートボタンが表示されてしまう。

/*
Theme Name: Headless Wordpress Research
*/

functions.phpにプレビュー用のアクションを書く。localhostのときだけNext.jsのポートを見るように指定。

<?php

add_action('template_redirect', function () {
  $is_https = isset($_SERVER['HTTPS']);
  $is_localhost = preg_match('/^localhost/', $_SERVER['HTTP_HOST']);
  $preview_protocol = $is_https ? 'https://' : 'http://';
  $preview_host = $is_localhost ? 'localhost:3000' : $_SERVER['HTTP_HOST'];

  if (!is_admin() && isset($_GET['preview']) && $_GET['preview'] == true) {
    $redirect = add_query_arg(
      [
        'id' => $_GET['preview_id'] ? $_GET['preview_id'] : $_GET['p'],
        'nonce' => wp_create_nonce( 'wp_rest' )
      ],
      $preview_protocol . $preview_host . '/preview'
    );
    wp_redirect($redirect);
  }
});

コンテナ側の修正

インストールスクリプトでwp-jsonを有効化するためにパーマリンクを変更し、独自テーマの有効化も行う。

# wp-install.sh
# ...
wp option update permalink_structure '/%postname%/'
# ...
wp theme activate headless-wordpress-research

独自テーマをボリュームマウントしておく。

# docker-compose.yml
# ...
- ./wordpress/themes/:/var/www/html/wp-content/themes/
# ...

コンテナを再ビルドして変更内容を反映しておく。

% docker-compose build
% docker-compose up -d
% docker exec -it --user 33:33 headless-wp-wordpress /bin/bash ./scripts/wp-install.sh

フロントエンドの修正

フロント側にプレビュー用のコンポーネントを追加。

% touch frontend/pages/preview.tsx

Cookie認証するのでfetchのオプションでcredentials: 'include'として常にCookieを送信するようにしておく。

APIのURLは環境変数で渡せるようにしておく。

import { useEffect, useState } from "react";
import type { NextPage } from "next";
import { useRouter } from "next/router";

const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || "";

const Preview: NextPage = () => {
  const router = useRouter();
  const [post, changePost] = useState<any>(null);
  const { id, nonce } = router.query;

  useEffect(() => {
    if (!id || !nonce) return;

    const requestHeaders: HeadersInit = new Headers();
    requestHeaders.set("X-WP-Nonce", `${nonce}`);
    (async () => {
      const response = await fetch(`${API_BASE}posts/${id}?_embed&status=draft`, {
        credentials: "include",
        headers: requestHeaders,
      });
      if (!response.ok) {
        console.error("something wrong.");
        return;
      }
      changePost(await response.json());
    })();
  }, [id, nonce]);

  return (
    <div>
      {post ? (
        <div>
          <h1>{post.title.rendered}</h1>
          <article dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
        </div>
      ) : null}
    </div>
  );
};

export default Preview;

ローカル環境用に環境変数を追加。

# .env.local
NEXT_PUBLIC_API_BASE_URL="http://localhost:8000/wp-json/wp/v2/"

これでフロントエンドとWordPressを両方立ち上げればプレビューを確認できる。

プレビューの様子

リモートで確認

SSGの場合は、環境変数は.env.localでいいようだが、.env.localはバージョン管理しないので、GitHub Actionsのワークフローで使えるように設定で追加する必要がある。

リポジトリの(アカウントのではない)Settings→Secrets→ActionsにNEXT_PUBLIC_API_BASE_URLを定義。

ビルドするステップの前に、secretsを書き加えた.env.localを作成するステップを追加する。

# frontend/.github/workflows/deploy.yml
- name: Set env
  run: |
    touch .env.local
    echo NEXT_PUBLIC_API_BASE_URL=${{ secrets.NEXT_PUBLIC_API_BASE_URL }} >> .env.local

テーマをリモートに送信。今回はscpで転送した。そして、フロントエンドの変更をGitHubにプッシュすればレンタルサーバー側でもプレビューを確認できる。

おまけ

フロントエンドのコンテナ化

当初、いろいろ試していて、変にローカルのnode_modulesをコピーしてしまったのか、SWCが見つからないエラーでハマった。公式の例に沿っていくとちゃんとできた。

# frontend/Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package.json yarn.lock* .

RUN yarn install

COPY pages ./pages
COPY styles ./styles
COPY public ./public
COPY .env*.local .
COPY next-env.d.ts .
COPY next.config.js .
COPY tsconfig.json .

CMD yarn dev

# docker-compose.yml
# ...
frontend:
  container_name: headless-wp-frontend
  build:
    context: ./frontend
    dockerfile: Dockerfile
  volumes:
    - ./frontend/pages:/app/pages
    - ./frontend/styles:/app/styles
    - ./frontend/public:/app/public
  restart: always
  ports:
    - 3000:3000
# ...

ホットリロードも効いている。

カスタムプラグインとテーマもGitHub Actionsでデプロイする

% cd wordpress
% mkdir -p .github/workflows && touch $_/deploy.yml
name: Deploy custom WordPress plugin and theme

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-20.04

    steps:
      - name: 🚚 Checkout
        uses: actions/checkout@v2

      - name: 📂 Sync theme files
        uses: SamKirkland/FTP-Deploy-Action@4.3.0
        with:
          server: ${{ secrets.FTP_SERVER }}
          username: ${{ secrets.FTP_USER }}
          password: ${{ secrets.FTP_PASSWORD }}
          server-dir: "<PATH TO WORDPRESS>/wp-content/themes/headless-wordpress-research/"
          local-dir: "./themes/headless-wordpress-research/"

      - name: 📂 Sync plugin files
        uses: SamKirkland/FTP-Deploy-Action@4.3.0
        with:
          server: ${{ secrets.FTP_SERVER }}
          username: ${{ secrets.FTP_USER }}
          password: ${{ secrets.FTP_PASSWORD }}
          server-dir: "<PATH TO WORDPRESS>/wp-content/plugins/github-actions-kicker/"
          local-dir: "./plugins/github-actions-kicker/"

参考にしたサイト