WordPressでのサイト構築の備忘録

クライアントから自社サイトを安く構築したいと相談を受けWordPressでやったのでメモしておく。


##要件
1. 無料テーマを改造して実装する。


##環境
1. Amazon Linux2
2. Wordpress 5.2.2
3. Visual Studio Code + SSHFS


##環境構築
使いやすさでVisual Studio Codeを選択。SSHFSプラグインでLinuxサーバを直接いじる。これが快適でだいぶ生産性があがった。

Visual Studio Codeで拡張機能検索で「SSH FS」で検索してインストール。
Configurationを作成、入力するのは
1. Host
2. Port
3. Root
4. Username
5. Private Key(AWSのpemファイルを指定)
である。
作成できたら右クリで接続。緑丸になれば接続成功。
後はVSCで直接いじれる。


##実装
機能を大別すると
1. 表向きのページ(いわゆるHP)
2. 管理者ページ(記事投稿用)
がある。

Wordpressのインストールファイル

```java:ツリー図
.
├── themes
│   └── bluestreet
├── wp-admin //管理者ページのルート
│   ├── css★
│   ├── images★
│   ├── includes
│   ├── js★
│   ├── maint
│   ├── network
│   └── user
├── wp-content　//HPページのルート
│   ├── languages
│   ├── plugins★　プラグイン
│   ├── themes★
│   │   ├── bluestreet★　親テーマのルート
│   │   └── wallstreet★　子テーマのルート
│   ├── upgrade
│   └── uploads★　記事投稿リソース保存先
└── wp-includes　//インクルードリソース
    ├── blocks
    ├── certificates
    ├── css
    ├── customize
    ├── fonts
    ├── ID3
    ├── images
    ├── IXR
    ├── js
    ├── pomo
    ├── random_compat
    ├── Requests
    ├── rest-api
    ├── SimplePie
    ├── Text
    ├── theme-compat
    └── widgets
```

★がついているところが主に手を入れたところ。
下記に要点を列挙する。

###共通部品
グローバルヘッダー、グローバルフッター。
header.php、footer.phpをいじる。
ベースにしたテーマではSNSリンクがあったが使わないのでこれは外した。
コンタクト情報はDBのoptionから取る方式はそのままとした。

グローバルヘッダーはポップアップメニューとした。

```html:header.php
<!-- PC版メニュー -->
<div class="navbar navbar-wrapper navbar-inverse navbar-static-top headerbar" role="navigation">
  <!-- Brand and toggle get grouped for better mobile display -->
  <div>
    <ul class="navbar-header global-header-menu">
      <li class="header_div_main">
        <a href="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>">
          <div class="header_div_sub1">
            <!--<span class="header_label1-1"><?php echo get_option('company_mainlogo'); ?></span>-->
            <span class="header_label1-2"><?php echo get_option('company_sublogo'); ?></span>
          </div>
          <div class="header_div_sub2">
            <span class="header_label2"><?php $l->g('company'); ?></span>
          </div>
        </a>
      </li>
      <li>
        <ul class="header-menu-table">
          <li class="header-menu-parent accodionArea">
            <a href="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>/company-introduction/">
              <div class="accodionBtn">
                <span class="header-menu-label1">会社案内</span>
              </div>
            </a>
            <ul class="header-menu-children sub-menu-close sub-menu-hidden"> ★ここ以下がポップアップする。
              <li>
                <a href="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>/company-introduction" class="sub-menu-label1">会社の理念</a>
              </li>
              <li>
```

```css:css
@keyframes menu-fadeout {
  0% {
    opacity: 1;
    margin-top: 0;
    transform: translateY(0);
  }
  99% {
    opacity: 0;
    margin-top: 0;
    transform: translateY(-50px);
  }
  100% {
    opacity: 0;
    margin-top: -200px;
  }
}
.sub-menu-open {
  animation: menu-fadein 0.5s forwards;
  display: block !important;
}
.sub-menu-close {
  animation: menu-fadeout 0.5s forwards;
}
.sub-menu-hidden {
  display: none;
}
```

```javascript:javascript
jQuery(function() {
  jQuery('.accodionBtn').on('mouseenter', function() {
    jQuery(this).parent().next(".header-menu-children").removeClass("sub-menu-close");
    jQuery(this).parent().next(".header-menu-children").addClass("sub-menu-open");
  });

  jQuery('.accodionArea').on('mouseleave', function() {
    jQuery(this).children(".header-menu-children").removeClass("sub-menu-open");
    jQuery(this).children(".header-menu-children").addClass("sub-menu-close");
  });
});
```

###ページ内項目外出し化
基本的にDBのwp_optionに突っ込んでget_optionsで取得するようにする。


###サブ機能の実装
ベースにしたテーマになかった「お知らせ」、「求人票」の投稿周りの機能をプラグインとして実装した。
functions.phpに実装してもできるとは思うが、プラグインだと有効かしたときにコンストラクタでDBテーブルを作成するなどの準備処理ができる。
規定のコメントヘッダーを入れることでWordpress管理画面のプラグイン一覧に載るようになる。

```wp-content\plugins\information-function.php
<?php
/*
Plugin Name: information function
Plugin URI:
Description: お知らせコンテンツ関係機能プラグイン
Author: fantec
Version: 1.0
Author URI: 
*/

class informationFunction {
  //テーブル名
  var $table_name;

  //コンストラクタ
  public function __construct()
  {
    global $wpdb;

    //接頭辞（wp_）を付けてテーブル名を設定
    $this->table_name = $wpdb->prefix . 'informations';
    
    //プラグイン有効化したとき実行
    register_activation_hook (__FILE__, array($this, 'create_table'));
  }

  //初期化処理
  function create_table() {
    global $wpdb;

    //DBのバージョン
    $func_db_version = '1.0';

    //現在のバージョン取得
    $installed_ver = get_option( 'information_func_version' );
    
    //DBバージョンが違ったら作成
    if( $installed_ver != $func_db_version ) {
      $sql = "CREATE TABLE " . $this->table_name . " (
            info_id bigint(20) UNSIGNED DEFAULT '0' NOT NULL,
            info_title text,
            author_id int(10) DEFAULT NULL,
            content text,
            imgname text,
            regist_date datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
            update_date datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
            UNIQUE KEY info_id (info_id)
          )
          CHARACTER SET 'utf8';";
        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        dbDelta($sql);

        //オプションにDBバージョン保存
        update_option('information_func_version', $func_db_version);
    }
  }

  //全お知らせ取得
  public function get_all() {
    global $wpdb;

    return $wpdb->get_results("SELECT * FROM wp_informations ORDER BY update_date DESC");
  }

  //お知らせ先頭n件取得
  public function get_topn($n) {
    global $wpdb;

    return $wpdb->get_results("SELECT * FROM wp_informations ORDER BY update_date DESC LIMIT ".$n);
  }
  
  //一覧表示用お知らせ取得
  //
  public function get_page_list($page) {
    global $wpdb;
    $result = array();

    //一覧ページあたり掲載数
    $num_per_page = 5;

    $tmp = $wpdb->get_results("SELECT * FROM wp_informations ORDER BY update_date DESC");
    $result['data'] = array_slice($tmp, $num_per_page*($page-1), $num_per_page);
    $result['maxpage'] = (count($tmp) % $num_per_page == 0) ? floor(count($tmp) / $num_per_page) : (floor(count($tmp) / $num_per_page))+1;

    return $result;
  }

  //特定お知らせ取得
  public function get_by_infoid($info_id) {
    global $wpdb;

    $result = $wpdb->get_results("SELECT * FROM wp_informations WHERE info_id = ".$info_id." ORDER BY update_date DESC");
    return isset($result[0]) ? $result[0] : null;
  }

  //特定お知らせ新規登録
  public function insert($info_title, $author_id, $content, $imgname) {
    global $wpdb;

    $datetime = new DateTime();
    $datetime->setTimeZone( new DateTimeZone('Asia/Tokyo'));
    $date_formated = $datetime->format('Y-m-d H:i:s');
    
    $result = $wpdb->get_results("SELECT MAX(info_id) AS maxid FROM wp_informations");

    $wpdb->insert('wp_informations', 
      array( 
          'info_id' => $result[0]->maxid+1,
          'info_title' => $info_title,
          'author_id' => $author_id,
          'content' => $content,
          'imgname' => $imgname,
          'regist_date' => $date_formated,
          'update_date' => $date_formated
      ));

      return intval($result[0]->maxid+1);
  }

  //特定お知らせ更新(画像変更なし)
  public function update_noimg($info_id, $info_title, $author_id, $content) {
    global $wpdb;

    $datetime = new DateTime();
    $datetime->setTimeZone( new DateTimeZone('Asia/Tokyo'));
    $update_date_formated = $datetime->format('Y-m-d H:i:s');

    $wpdb->update('wp_informations', 
      array( 
          'info_title' => $info_title,
          'author_id' => $author_id,
          'content' => $content,
          'update_date' => $update_date_formated
      ), 
      array( 'info_id' => $info_id ));

    return intval($info_id);
  }

  //特定お知らせ更新
  public function update($info_id, $info_title, $author_id, $content, $imgname) {
    global $wpdb;

    $datetime = new DateTime();
    $datetime->setTimeZone( new DateTimeZone('Asia/Tokyo'));
    $update_date_formated = $datetime->format('Y-m-d H:i:s');

    $wpdb->update('wp_informations', 
      array( 
          'info_title' => $info_title,
          'author_id' => $author_id,
          'content' => $content,
          'imgname' => $imgname,
          'update_date' => $update_date_formated
      ), 
      array( 'info_id' => $info_id ));

    return intval($info_id);
  }

  //画像ルートURI取得
  public function get_img_root_uri() {
    return content_url()."/uploads/informations/img/";
  }
  
  //画像ルートディレクトリ取得
  public function get_img_root_dir() {
    $upload_dir = wp_upload_dir();
    return $upload_dir['basedir']."/informations/img/";
  }

  //未設定時画像パス取得
  public function get_notsetimg_uri() {
    return content_url()."/uploads/informations/img/noimage.jpg";
  }

  //文の省略加工
  public function get_omission_str($str, $len) {
    return mb_strlen($str) > $len ? mb_substr(esc_html($str), 0, $len)."..." : $str;
  }
}
$incetance = new informationFunction;
```

利用側

```html:
<!-- AddThis Button END -->
<?php $wallstreet_pro_options=theme_data_setup();
$current_options = wp_parse_args(  get_option( 'wallstreet_pro_options', array() ), $wallstreet_pro_options );
//お知らせ機能用
$infofunc = new informationFunction();
?>
<div class="information-section">
  <div>
    <div class="row">
      <div class="section_heading_title">
        <h1><?php echo esc_html(get_option('information_title')); ?></h1>
        <div class="pagetitle-separator">
          <div class="pagetitle-separator-border">
            <div class="pagetitle-separator-box"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="row">
      <?php
        $infos = $infofunc->get_topn(3);
        foreach($infos as $info) {
          $info_id = isset($info->info_id) ? $info->info_id : null;
          $title = isset($info->info_title) ? $info->info_title : null;
          $content = isset($info->content) ? $info->content : null;
          $imgname = isset($info->imgname) ? $info->imgname : null;
          $update_date = isset($info->update_date) ? $info->update_date : null;
          $datetime = new DateTime($update_date);
          //$update_date_formated = $datetime->format('Y/m/d H:i:s');
          $update_date_formated = $datetime->format('Y/m/d');
      ?>
      <div class="information-outline">
        <div class="headerdiv">
          <span class="information-title"><?php echo esc_html($title); ?></span>
          <div class="information-datetime">
            <span>掲載日：<?php echo esc_html($update_date_formated); ?></span>
          </div>
        </div>
        <div class="contentdiv">
          <?php if ($imgname!=null) { ?>
          <img src="<?php echo $infofunc->get_img_root_uri().esc_html($imgname); ?>" alt="" />
          <?php } ?>
          <span class="information-content"><?php echo $infofunc->get_omission_str(nl2br(esc_html($content)), 50); ?></span>
        </div>
        <form method="post" name="information_form_<?php echo $info_id ?>" action="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>/information-detail/">
          <input type="hidden" name="info_id" value="<?php echo $info_id ?>">
          <div class="information-detail-button-div">
            <a href="javascript:information_form_<?php echo $info_id ?>.submit()">
              <div class="information-detail-button">
                  <span>詳細を見る</span>
              </div>
            </a>
          </div>
        </form>
      </div>
      <?php
        }
      ?>
    </div>
    <div class="information-more-button-div">
      <a href="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>/information-list/">
        <div class="information-more-button">
          <span>お知らせをもっと見る</span>
        </div>
      </a>
    </div>  
  </div>  
</div>
<!-- /wallstreet Portfolio Section -->
```


###問合せをメール送信
PHPMailerで実装。メール本文はテンプレートをキーワード置換で作成するようにした。

```
<?php
  get_header();
  
/*
Template Name: 問い合わせ内容のメール送信ページ
*/

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

require 'PHPMailer/src/Exception.php';
require 'PHPMailer/src/PHPMailer.php';
require 'PHPMailer/src/SMTP.php';

$inquiry_no;
$mail = new PHPMailer(true);

$is_spam = spam_check();  // スパム判定
$error = null;

try {
  // サーバ設定
  //$mail->SMTPDebug = 2;     // デバッグモード。本番時はコメントアウトすること。
  $mail->isSMTP();
  $mail->Host = 'localhost';
  $mail->SMTPAuth = true;
  $mail->Username = 'xxx@bbb.cc';    // 送信アカウント
  $mail->Password = 'xxxxxxx';           // 送信アカウントのパスワード
  $mail->SMTPSecure = false;              // TLSなどの暗号化非対応のサーバならfalseを設定。使えるなら'tls'か'ssl'を設定。
  $mail->SMTPAutoTLS = false;             // SMTPSecureをfalseにする場合はfalseにする。それ以外なら未設定で。
  $mail->Port = 587;

  // 送り先情報
  $mail->setFrom('xxxx@bbb.ccc');              // 送信元アドレス
//    $mail->addCC();                           // CC
//    $mail->addBCC();                          // BCC
  $mail->addAddress('aaa@xxx.cc');          // 送信先アドレス
  $mail->addReplyTo('aaa@xxx.cc');          // 返信先アドレス

  // 本文
  $mail->isHTML(false);
  $mail->CharSet = 'UTF-8';

  $inquiry_type = $_POST['inquiry_type'];    // 問合せ種別(joboffer:派遣求人票問合せ tradingcompany:派遣取引企業向け問合せ recruitment:自社採用情報問合せ)

  // 置換対象を順に指定
  $search = array('$$inquiry_no$$', '$$simei$$', '$$furi$$', '$$inquiry_ages$$', '$$inquiry_sex$$', '$$tel1$$', '$$tel2$$', '$$tel3$$',
      '$$email$$', '$$offer_no$$', '$$inquiry_subject$$', '$$inquiry_content$$');
  // 置き換える文字列を順に指定
  $inquiry_no = make_inquiry_no("JO");
  $replace = array($inquiry_no, $_POST['simei'], $_POST['furi'], $age, $sex, $_POST['tel1'], $_POST['tel2'], $_POST['tel3'],
                $_POST['email'], $_POST['offer_no'], $subject, $_POST['inquiry_content']);
  $mail->Body = str_replace($search, $replace, $template);    // 本文
  $mail->Subject = mb_encode_mimeheader($subject."　問い合わせ番号「".$inquiry_no."」");                   // 件名
  $mail->send();

} catch (Exception $e) {
  echo $mail->ErrorInfo;
}
```


```テンプレート
差出人: HP　「派遣のお取引企業」お問い合わせ自動送信フォーム

お問い合わせ番号：「$$inquiry_no$$」

【貴社名】：
$$company$$

【部署名】：
$$section$$

【ご担当者様のお名前】：
$$simei$$

【フリガナ】：
$$furi$$

【お電話番号】：
$$tel1$$ - $$tel2$$ - $$tel3$$

【FAX番号】：
$$fax1$$ - $$fax2$$ - $$fax3$$

【メールアドレス】：
$$email$$
  
【お問い合わせ件名】：
$$inquiry_subject$$
  
【お問い合わせ内容】：
$$inquiry_content$$

-- 
このメールは 株式会社xxxx (yyyyyy) の「派遣お取引企業様向けお問い合わせフォーム」から送信されました
```


##管理者画面のメニュー操作
1.　wp_enqueue_styleでリソースのインクルード。
2.　remove_menu_pageで余計なmニューを削除。
3.　add_menu_pageとadd_submenu_pageで独自なメニューを追加。

```php:\wp-content\themes\bluestreet\functions.php

function bluestreet_custmizer_style()
{
    wp_enqueue_style('bluestreet-customizer-css',get_stylesheet_directory_uri() .'/css/cust-style.css');
}
add_action('customize_controls_print_styles','bluestreet_custmizer_style');


/* 管理画面メニューのカスタマイズ */
define('MAX_FILE_SIZE', 1 * 1024 * 1024); //アップロード画像のサイズ制限(1MB)
define('JTAG_ON', 'on');                   //求人タグの選択時データ
define('JTAG_OFF', 'off');                 //求人タグの未選択時データ
if (is_user_logged_in()) {
  function remove_admin_menu() {
    remove_menu_page( 'index.php' );                // ダッシュボード
    remove_menu_page( 'edit.php' );                 // 投稿
    remove_menu_page( 'upload.php' );               // メディア
    remove_menu_page( 'edit.php?post_type=page' );  // 固定ページ
    remove_menu_page( 'edit-comments.php' );        // コメントs
    remove_menu_page( 'themes.php' );               // 外観
    remove_menu_page( 'plugins.php' );              // プラグイン
    remove_menu_page( 'users.php' );                // ユーザー
    remove_menu_page( 'tools.php' );                // ツール
    remove_menu_page( 'options-general.php' );      // 設定
  }
  $user = wp_get_current_user();
  if($user->roles[0]!="administrator") {
    /* 管理者以外なら不要な管理画面メニューを非表示にする */
    add_action( 'admin_menu', 'remove_admin_menu', 999 );
  }

  //各カスタムページを出力するコールバック定義外部ファイル読み込み
  require( get_template_directory().'/functions/my-admin/admin-info-list.php');          //お知らせ一覧
  require( get_template_directory().'/functions/my-admin/admin-info-post.php');          //お知らせ編集
  require( get_template_directory().'/functions/my-admin/admin-joffer-list.php');        //求人一覧
  require( get_template_directory().'/functions/my-admin/admin-joffer-post.php');        //求人編集
  require( get_template_directory().'/functions/my-admin/admin-joffertags-edit.php');    //求人タグ編集
  require( get_template_directory().'/functions/my-admin/admin-joffertypes-edit.php');  //求人職種編集
  require( get_template_directory().'/functions/my-admin/admin-jofferareas-edit.php');  //求人勤務地編集
  require( get_template_directory().'/functions/my-admin/admin-recruitment-list.php');  //採用情報一覧
  require( get_template_directory().'/functions/my-admin/admin-recruitment-post.php');  //求人編集

  /* 独自メニューを追加 */
  function add_admin_menu() {
    //お知らせ機能
    add_menu_page( 'お知らせ一覧', 'お知らせ', 'publish_posts', 'informations', 'admin_info_list', '', 0 );
    add_submenu_page( 'informations', 'お知らせ編集', '新規登録', 'publish_posts', 'admin_info_post', 'admin_info_post');
    //求人機能
    add_menu_page( '求人一覧', '求人', 'publish_posts', 'joboffers', 'admin_joffer_list', '', 1 );
    add_submenu_page( 'joboffers', '求人編集', '新規登録', 'publish_posts', 'admin_joffer_post', 'admin_joffer_post');
    add_submenu_page( 'joboffers', 'タグ編集', 'タグ編集', 'publish_posts', 'admin_joffertags_edit', 'admin_joffertags_edit');
    add_submenu_page( 'joboffers', '職種編集', '職種編集', 'publish_posts', 'admin_joffertypes_edit', 'admin_joffertypes_edit');
    add_submenu_page( 'joboffers', '勤務地編集', '勤務地編集', 'publish_posts', 'admin_jofferareas_edit', 'admin_jofferareas_edit');
    //採用情報機能
    add_menu_page( '採用情報一覧', '採用情報', 'publish_posts', 'recruitment', 'admin_recruitment_list', '', 1 );
    add_submenu_page( 'recruitment', '採用情報編集', '新規登録', 'publish_posts', 'admin_recruitment_post', 'admin_recruitment_post');
    
    $user = wp_get_current_user();
    if($user->roles[0]!="administrator") {
      /* 管理者以外なら余計な部品は非表示にする */
      wp_enqueue_style( 'hidden-parts-style', get_theme_root_uri()."/wallstreet/css/my-admin-hidden-parts.css" );
    }
  }
  add_action('admin_menu', 'add_admin_menu');
}
```

##画面遷移
普通に。

```php
<div class="information-more-button-div">
  <a href="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>/information-list/">
    <div class="information-more-button">
      <span>お知らせをもっと見る</span>
    </div>
  </a>
</div>
```

POSTパラメータつき。

```php
<form method="post" name="information_form_<?php echo $info_id ?>" action="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>/information-detail/">
  <input type="hidden" name="info_id" value="<?php echo $info_id ?>">
  <div class="information-detail-button-div">
    <a href="javascript:information_form_<?php echo $info_id ?>.submit()">
      <div class="information-detail-button">
          <span>詳細を見る</span>
      </div>
    </a>
  </div>
</form>
```

##一覧ページング
同一ページへの同期型POSTリクエスト方式

```php
<div class="joboffer-paging-area">
  <div class="row">
    <form method="post" name="joboffer_list_form_prev" action="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>/joboffer-list/">
      <input type="hidden" name="page" value="<?php echo $page-1 ?>">
      <input type="hidden" name="filter" value="<?php echo $filter ?>">
      <a class="<?php echo $page>1 ? '':'vs-hidden'; ?>" href="javascript:joboffer_list_form_prev.submit()">
        <div class="page-prev-button">
          <span>前へ</span>
        </div>
      </a>
    </form>
    <?php for($i=1; $i<=$offers['maxpage']; $i++) { ?>
    <form method="post" name="joboffer_list_form_<?php echo $i ?>" action="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>/joboffer-list/">
      <input type="hidden" name="page" value="<?php echo $i ?>">
      <input type="hidden" name="filter" value="<?php echo $filter ?>">
      <?php if($i!=$page) { ?>
      <a href="javascript:joboffer_list_form_<?php echo $i ?>.submit()">
        <div class="page-no-button">
          <span><?php echo $i ?></span>
        </div>
      </a>
      <?php } else { ?>
        <div class="page-no-button page-this">
          <span><?php echo $i ?></span>
        </div>
      <?php } ?>
    </form>
    <?php } ?>
    <form method="post" name="joboffer_list_form_next" action="<?php echo esc_url( get_option( 'ssl_url' ) ); ?>/joboffer-list/">
      <input type="hidden" name="page" value="<?php echo $page+1 ?>">
      <input type="hidden" name="filter" value="<?php echo $filter ?>">
      <a class="<?php echo $page<$offers['maxpage'] ? '':'vs-hidden'; ?>" href="javascript:joboffer_list_form_next.submit()">
        <div class="page-next-button">
          <span>次へ</span>
        </div>
      </a>
    </form>
  </div>
</div>
```