WordPress標準制作フロー2024

## 現在のWordPress標準制作フロー

[5年前に記載したこちらの記事](https://qiita.com/koji-yamauchi/items/394c8e111dcd14a5d311)は、もう昨今の作業環境には適合しなくなりました。
2024年現在での弊社の標準的な制作フローをご紹介します。

[WordPressを扱うエンジニアに対する質問事項](https://qiita.com/koji-yamauchi/items/31edcfe443285b6d7194)で記載した回答例と一部重複しますが、より詳細に案内したいと思います。

## 前提事項

5年前の記載内容と前提事項は同様です。

* Web制作会社などでいちからサイトを制作することを想定
  汎用テーマなどを使って簡易に構築するとかではなく、ガッツリとそのサイト専用のテーマを作成する
* それぞれのツールのインストール方法などは記載しない
  Qiita内で丁寧に手順を記載している記事があるので検索してみてください

## ローカル開発環境

WordPressでサイト制作を決めた場合、携わるエンジニアは開発環境として何を使えばよいでしょうか？

我々の回答としては、基本的には[@wordpress/env](https://ja.wordpress.org/team/handbook/block-editor/reference-guides/packages/packages-env/)、場合によっては[LOCAL](https://localwp.com/)を考えています。

最終的にShifterで動作させる想定の場合は[Shifter(Static) - Local](https://github.com/digitalcube/shifter-static-local)、Kinstaでの動作を想定している場合は[DevKinsta](https://kinsta.com/jp/devkinsta/)という選択肢もあるかもしれません。[^1]

[^1]: KinstaDevの利用経験はありませんが、Shifter(Static) - Localは本番稼働をShifterを想定したサイトで、Shifter特有の問題が発生した際のデバッグには有効でした。Shifterでの起きがちな問題や解決方法については機会があれば別途記載したいと思います。

### @wordpress/env

WordPress公式が提供する今考えられる一番標準的な開発環境です。
Node.jsとDockerを動かすため、これらの基礎的な知識が必要となります。

#### メリット

* PHPバージョン設定などがコードとして定義されるため、IaC的観点で良い
* CLIからの操作が主のため、スクリーンショット付きの手順書などが不要となる
* WordPress公式なので安心できる

#### デメリット

* Node.jsやDockerなどの知識が必要になる
* CLIからの操作が主のため、非エンジニアには扱いづらい
* 良くも悪くも、開発対象の独自テーマや独自プラグイン以外の構成がIDEから見えにくくなるので、IDE内の関数未定義エラーなどを解決するにはスタブの設定が必要

#### 設定例

@wordpress/envは`.wp-env.json`にて設定を行います。
下記に設定を記載します。

```json
{
  "phpVersion": "8.1",
  "port": 8765,
  "env": {
    "tests": {
        "port": 8764
    }
  },
  "config": {
    "WP_DEBUG": true,
    "SAVEQUERIES": true
  },
  "mappings": {
    "../data": "./misc/data",
    ".htaccess": "./misc/.htaccess",
    "../install.sh": "./misc/install.sh",
    "wp-content/uploads": "./uploads",
    "wp-content/themes/example_wp_theme_name": "./themes/example_wp_theme_name"
  }
} 
```

<dl>
<dt>phpVersion</dt>
<dd>PHPのバージョンを指定します。本番環境に合わせると良いでしょう。</dd>
<dt>port</dt>
<dd>サイトごとに重複しないポートにすると同時に立ち上げることができて良いでしょう。</dd>
<dt>config</dt>
<dd><code>wp-config.php</code>の設定値を定義します。
Warningなどを検出できるよう、デバックモードはtrueにします。</dd>
<dt>mappings</dt>
<dd>ホストOSのディレクトリをDockerインスタンス上にマウントします。
<dd>ここではDBダンプなど、なんらかのファイルをDocker側とやりとりする際の<code>data</code>ディレクトリ、<code>.htaccess</code>、初期セットアップ用のシェルスクリプト（後述）、メディアファイル、テーマファイルをマッピングしています。
<code>data</code>ディレクトリと<code>uploads</code>ディレクトリの中のファイルは<code>.gitignore</code>でバージョン管理外にすると良いでしょう。</dd>
</dl>

#### スタブ

デメリットで挙げたようにVSCodeの[PHP Intelephense](https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client)を用いている場合、WordPressの標準関数を定義しているファイルをIDEが把握できないため、エラー扱いとなります。

これは、VSCode設定画面のintelephenseから`wordpress`を追加することで回避できます。

![スクリーンショット 2024-06-13 11.27.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426622/ff8d22c3-c137-d962-4682-b4a164afc41b.png)

また、WordPressのオリジナルテーマ開発には[Advanced Custom Fields](https://ja.wordpress.org/plugins/advanced-custom-fields/)を用いることも多いでしょう。
この場合は、`get_field`や`the_field`などの関数もエラーにならないように[php-stubs/acf-pro-stubs](https://packagist.org/packages/php-stubs/acf-pro-stubs)も導入すると良いでしょう。

### LOCAL

Docker環境は想定外のトラブルシューティングにある程度の知識を求められるため、Node.jsやDockerなどの基礎的な知識を持たないメンバーが参画する場合はLOCALのようなGUIアプリケーションの利用も考えられます。

#### メリット

* GUIアプリケーションで簡単に扱える
* 仮想技術などが入っておらず、WordPress本体のコードも含めFinder上から確認しやすい

#### デメリット

* GUIからの操作が主のため、情報共有にはスクリーンショット付きの手順が必要となる
* 設定がコード化されていないため、メンバー間で想定外の動作環境の差異が発生しやすい

## テーマ作成の第一歩

WP CLIの[wp scaffoldコマンド](https://developer.wordpress.org/cli/commands/scaffold/)を用いて、underscoresベースのテーマ雛形を作成し、そこから不要部分の除去とリファクタリングを実施し、要件にあわせた機能追加を進めていきます。

:::note info
**過去案件をベースにつくれないか**

ゼロからWP CLIで構築するより、過去に自身が携わったサイトのテーマを複製するほうが効率的と考えるかもしれません。

それが適切な場合もありますが、下記の理由から前述のような雛形生成ツールを用いるのがおすすめです。

* 固有名詞の置換もれが発生しやすい（受託の場合は深刻なトラブルにも）
* 潜在的な不具合、既知の不具合を引き継ぐ
* 新規構築することにより、もう一度考えなす機会が発生しより効率的でよいコードに成長する
:::

### 付属ツール

`underscores` には下記の便利なツールが付属しています。制作時の効率化のため活用しましょう。
ここではよく使う2つをご紹介します。

#### PHP Parallel Lint

例えば、 `<?php $failedCode = ?>` のような不正なコードをが混入していると実行時に下記のようにエラーを検出します。

```
> @php ./vendor/bin/parallel-lint --exclude .git --exclude vendor .
PHP 8.0.29 | 10 parallel jobs
............X....                                            17/17 (100 %)


Checked 17 files in 0.1 seconds
Syntax error found in 1 file

------------------------------------------------------------
Parse error: ./header.php:27
    25| <div id="page" class="container mx-auto bg-white">
    26|         <a class="skip-link screen-reader-text" href="#primary"><?php esc_html_e( 'Skip to content', 'sample-theme' ); ?></a>
  > 27|         <?php $failedCode = ?>
    28|         <header id="masthead" class="bg-gray-300 px-4 py-2 flex flex-col gap-4">
    29|                 <div class="site-branding">
Unexpected token ";" in ./header.php on line 27
Script @php ./vendor/bin/parallel-lint --exclude .git --exclude vendor . handling the lint:php event returned with error code 1
✖ Command failed with exit code 1
Command failed with exit code 1
```

#### PHP_CodeSniffer

WordPressのコーディング規約に違反している箇所を教えてくれます。
例えば、下記のようにWordPressのコーディング規約に違反するコードを混入させた実行結果です。

```php
if ( ! defined( '_S_VERSION' ) ) {
	// Replace the version number of the theme on each release.
	define( '_S_VERSION', '1.0.0' );
define( 'CS_FAILED', 'data' ); $fooBar = null;
}
```

下記のようにインデントしていないことや変数がスネークケースになっていないこと、1行で書いてはいけないことが指摘されます。

```
FILE: functions.php
-------------------------------------------------------------------------------------------------------------------------------------------------------------
FOUND 7 ERRORS AFFECTING 5 LINES
-------------------------------------------------------------------------------------------------------------------------------------------------------------
   3 | ERROR | [ ] Doc comment short description must start with a capital letter (Generic.Commenting.DocComment.ShortNotCapital)
  13 | ERROR | [x] Line indented incorrectly; expected at least 1 tabs, found 0 (Generic.WhiteSpace.ScopeIndent.Incorrect)
  13 | ERROR | [ ] Variable "$fooBar" is not in valid snake_case format, try "$foo_bar"
     |       |     (WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase)
  13 | ERROR | [x] Each PHP statement must be on a line by itself (Generic.Formatting.DisallowMultipleStatements.SameLine)
```

デフォルトでは、ルールが厳しすぎるところもあるので、案件に応じて`phpcs.xml`をカスタマイズすると良いでしょう。
私の場合、`<rule ref="WordPress">`配下に下記のコードを追加することが多いです。

```xml
<!-- テーマ内でのカスタム投稿定義を許容 -->
<exclude name="WPThemeReview.PluginTerritory.ForbiddenFunctions.plugin_territory_register_post_type" />
<!-- テーマ内でのブロックタイプ定義を許容 -->
<exclude name="WPThemeReview.PluginTerritory.ForbiddenFunctions.editor_blocks_register_block_type" />
<!-- ページごとのテンプレートを許容 -->
<exclude name="WPThemeReview.Templates.ReservedFileNamePrefix.ReservedTemplatePrefixFound" />
<!-- wp_enqueue_styleでのバージョンnullを許容 -->
<exclude name="WordPress.WP.EnqueuedResourceParameters.MissingVersion" />
<!-- テーマ内でのタクソノミー定義を許容 -->
<exclude name="WPThemeReview.PluginTerritory.ForbiddenFunctions.plugin_territory_register_taxonomy" />
<!-- 無名関数の記述を許容 -->
<exclude name="PHPCompatibility.FunctionDeclarations.NewClosure.ThisFound" />
<!-- 無名関数の記述を許容 -->
<exclude name="PHPCompatibility.FunctionDeclarations.NewClosure.Found" />
<!-- JSON_UNESCAPED_UNICODE定数の利用を許容 -->
<exclude name="PHPCompatibility.Constants.NewConstants.json_unescaped_unicodeFound" />
<!-- テーマファイルでのショートコードを定義を許容 -->
<exclude name="WPThemeReview.PluginTerritory.ForbiddenFunctions.plugin_territory_add_shortcode" />
```

部分的にルールを適用外にするには下記のように`phpcs:ignore`コメントにて除外することも可能です。

```php
	$args      = array(
		'post_type'      => $target_post_type,
		'posts_per_page' => $post_per_page,
		// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		'meta_query'     => array(
			array(
				'key'     =>$target_key,
				'value'   => get_the_ID(),
				'compare' => 'LIKE',
			),
		),
	);
	$the_query = new WP_Query( $args );
```

## CI/CD

近年の社内案件では、下記のGitHub Actionを設定しています。

### Lint

前述のPHP Parallel LintとPHP_CodeSnifferを実行し、エラーがある場合はPull Requestのマージをブロックします。

```yaml
name: lint Workflow
on:
  pull_request:
    branches:
      - main
      - develop
jobs:
  php-lint:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./themes/example_wp_theme_name/
    steps:
      - uses: actions/checkout@v3
      - run: composer install
      - run: composer lint:php
      - run: composer lint:wpcs
```

### Deploy

```yaml
name: Deploy to staging
on:
  push:
    branches:
      - develop
jobs:
  build-deploy:
    runs-on: ubuntu-latest
    environment:
      name: develop
      url: https://example.com
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm i
      - run: npm run build
      - name: Set Commit Hash
        id: set_commit_hash
        run: echo "::set-output name=hash::${GITHUB_SHA:0:7}"
      - name: Update Theme Version
        run: |
          commit_hash=$(echo ${{ steps.set_commit_hash.outputs.hash }})
          sed -i "s/__COMMIT_HASH__/${commit_hash}/" themes/example_wp_theme_name/style.css
      - name: rsync deployments
        uses: burnett01/rsync-deployments@5.2.1
        with:
          switches: -avcz --delete --exclude-from='rsync-exclude.txt'
          path: ./themes/example_wp_theme_name/
          remote_path: ${{ secrets.DEVELOP_WEB_ROOT }}/wp-content/themes/example_wp_theme_name/
          remote_host: ${{ secrets.DEVELOP_HOST }}
          remote_port: ${{ secrets.DEVELOP_PORT }}
          remote_user: ${{ secrets.DEVELOP_USER_NAME }}
          remote_key: ${{ secrets.SSH_SECRET_KEY }}
```

本番環境へのデプロイも対象ブランチ違いで別途設定します。

#### キャッシュ対応

このGitHub Actionで特筆すべきは`Update Theme Version`の箇所です。
WordPressのテーマファイルの修正を反映した際に、ユーザのブラウザキャッシュからCSSやJavaScriptが読み込まれ、レイアウト崩れを発生させることがありますが、開発者が意識しなくても回避できる工夫を入れています。

具体的にはテーマの`style.css`に定義されるヘッダーコメントセクションの`Version`を下記のようにしています。

```css
/*!
Theme Name: example_wp_theme_name
Theme URI: https://example.com/
Author: add more
Author URI: https://www.add-more.co.jp/
Description: WordPress sample theme
Version: 1.0.__COMMIT_HASH__
Requires PHP: 7.4
Text Domain: addmore
*/
```

これを`sed`コマンドで`__COMMIT_HASH__`の部分をそのときのGitコミットハッシュ値に置き換えています。
そうするとバージョン情報は下記のようになります。

```
Version: 1.0.6af948a
```

手動でテーマバージョンを変更し忘れたとしても、デプロイの都度、バージョンが必ず変わることを意味します。

```php
function addmore_scripts() {
	$theme_data = wp_get_theme();
	$theme_ver  = $theme_data->get( 'Version' );

	wp_enqueue_style( 'addmore-style', get_stylesheet_uri(), array(), $theme_ver );
	wp_enqueue_script( 'addmore-common-js', get_template_directory_uri() . '/js/common.js', array( 'jquery' ), $theme_ver, true );
}
add_action( 'wp_enqueue_scripts', 'addmore_scripts' );
```

例えば、上記例ではデプロイ後に生成されるHTMLとしては下記のようバージョンのクエリパラメータが付加され、以前のテーマから読み込むURLが必ず変化します。

```html
<link rel='stylesheet' id='addmore-style' href='https://example.com/wp-content/themes/example_wp_theme_name/style.css?ver=1.0.6af948a' media='all' />
```

```html
<script src="https://example.com/wp-content/themes/example_wp_theme_name/js/common.js?ver=1.0.6af948a" id="addmore-common-js"></script>
```

## 初期データのセットアップ

5年前はWordMoveなどを用いて、外部サーバーからDBやメディアファイルを取得してローカル環境の整備を行っておりましたが、IaCの観点から近年は初期構築用のコードを記述するケースが多くなってきました。

例えば、`npm run setup`を実行すると必要なプラグインのインストールからWordPressの各種設定、シード（初期データ）の登録が行われるような動作にしています。

### package.json

package.jsonの抜粋

```
"scripts": {
    "setup": "npx wp-env run cli ../install.sh",
```

これにより、`npm run setup`は`.wp-env.json`の設定でDockerインスタンスにマウントしている`install.sh`を実行することを意味します。

### install.sh

`install.sh`の抜粋

```bash
#!/bin/bash

DEV_USER_EMAIL="username@example.com"
DEV_USER_PASSWORD="PleaseSetPasswordHere"

# プラグインインストール
wp plugin install wordpress-seo --activate

# テーマ有効化
wp theme activate example_wp_theme_name

# 日本語化
wp language core install ja
wp site switch-language ja
wp language plugin install --all ja

# timezone設定
wp option update timezone_string 'Asia/Tokyo'

# 管理者メールアドレス設定
wp option update admin_email $DEV_USER_EMAIL
wp option update new_admin_email $DEV_USER_EMAIL

# 管理者アカウントの情報変更
wp user update admin --user_pass=$DEV_USER_PASSWORD --user_email=$DEV_USER_EMAIL

# 基本設定
wp option update blogname "SAMPLE_WP_SITE"

# コメントやPING受付を停止
wp option update default_comment_status "closed"
wp option update default_ping_status "closed"
wp option update default_pingback_flag 0

# Yoast SEOの設定
wp option patch update wpseo_titles separator "sc-pipe"

# WP-CLI独自コマンドにより初期データのインポート
wp example_wp_theme_name load_init

# パーマリンク設定
wp rewrite structure '/%postname%/'
wp rewrite flush

# 開発関連のインストール
cd wp-content/themes/example_wp_theme_name/ && composer install
```

#### プラグインインストール

[.wp-env.jsonのpluginsフィールド](https://ja.wordpress.org/team/handbook/block-editor/reference-guides/packages/packages-env/#wp-env-json)でプラグインを定義することも可能です。
しかし、この機能を用いてインストールしたプラグインは正常にアップデートできない不具合が生じていたため、弊社ではシェルスクリプトの中でWP-CLIを用いてインストールする手法をとっています。

#### オプション値の設定

WordPressの設定を`wp option`コマンドをつかって記載することで、具体的にどのような設定をすべきかがコードとして定義され、初期セットアップ手順をスクリーンショット付きの手順書をつくる必要がなくなります。

また、一般的なプラグインも設定内容を`wp_options`テーブルに保持しており、これらは`wp option`コマンドから変更可能です。

たとえば[YoastSEO](https://ja.wordpress.org/plugins/wordpress-seo/)の下記の設定画面でのタイトル区切り文字の変更が可能です。

![スクリーンショット 2024-06-13 11.48.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426622/b4573d38-3c10-6a22-07dc-b7808d871ffb.png)

これは`wp option`コマンドを下記のように実行することでも対応可能です。

```bash
wp option patch update wpseo_titles separator "sc-pipe"
```

#### WP-CLI独自コマンドにより初期データのインポート

WP-CLIの`wp post create`などのコマンドを駆使して固定ページや投稿のデータ作成を行うことも可能ではあるのですが、複数のデータが関連する場合、登録後に明確となるIDを用いて関連付けが必要となり、シェルスクリプトで定義するには限界が生じます。

このような複雑な処理が求められるものは、WP-CLIの独自コマンドとして定義すると良いでしょう。

下記は`WP_CLI::add_command`にて定義された独自コマンドを呼び出し、その処理の中でメディアアップロードや投稿の作成を行います。

```bash
wp example_wp_theme_name load_init
```

プログラムコード内でデータを定義すると見通しが悪くなるため、JSON形式で定義したり、投稿の本文は外部のテキストファイルとして記載したものを読み込む処理にするのがおすすめです。[^2]

[^2]: 具体的なプログラムの解説になると、とても長くなるのでここでは割愛します。 こちらも機会があれば別途記載したいと思います。

## これからの進化

Webサイト制作において、よくある要求や課題は開発支援用のプラグインとして管理し、`@wordpress/env`でラップした標準スターターセットとして準備しておくのも良いかと考えています。

株式会社add moreではWordPressサイト制作をより快適に効率よくすすめる環境整備を手伝ってくれるエンジニアを募集しています。
ご興味のある方は[コーポレートサイト](https://www.add-more.co.jp/recruit/engineer/)または[Wantedly](https://www.wantedly.com/companies/add-more)からご応募ください。