WordPress | 自作メンテナンスプラグインで手軽に確実なメンテ運用

WordPress のメンテナンス(リリース切替・障害対応・夜間作業など)を、安全に・即時に切り替えたい。
本記事では、未ログインの訪問者にはドメイン直下/mentainance/ を常に表示し、ログイン中の作業者は通常どおりサイトを閲覧・操作できる軽量プラグイン Simple Maintenance Switch を紹介します。
合わせて、相対パスが壊れる問題の回避方法(①ルート相対に統一/②<base>要素活用)と、実運用の注意点・サンプルページも提供します。

注意: ディレクトリ名は要件に合わせ mentainance と表記しています(一般的には maintenance)。


目次

  1. ねらいとメリット
  2. 仕様(挙動・許可パス・SEO配慮)
  3. インストールと配置
  4. 相対パス崩れの原因と解決(① or ②)
  5. メンテページのサンプル(①/② 両対応)
  6. 管理UIの使い方
  7. CDN/キャッシュ環境での注意点
  8. よくある質問(FAQ)
  9. プラグインの全ソースコード

1. ねらいとメリット

  • 一般訪問者を 統一のメンテページ に誘導(HTTP 503)
  • 作業者は ログインしたまま本番挙動を検証(全ログイン or 管理者限定を選択)
  • 管理画面からワンクリックで ON/OFF 切替
  • 503/Retry-Afternoindex/no-cache を自動付与 → SEO・再訪誘導に配慮
  • WordPress 直下インストールでなくてもOK(ドメイン直下 /mentainance/ を参照)

2. 仕様(挙動・許可パス・SEO配慮)

  • 表示切替
  • 未ログイン → /mentainance/index.(php|html)HTTP 503 で返す
  • ログイン中 → 通常サイト(設定で「全ログイン」or「管理者のみ」を選択)
  • 常時許可パス
  • /wp-login.php /wp-admin/admin-ajax.php /wp-cron.php /xmlrpc.php /wp-json
  • /robots.txt /favicon.ico
  • /mentainance/ 配下(CSS/JS/画像直リンク)
  • SEO/キャッシュ
  • status=503Retry-After: 3600X-Robots-Tag: noindex, nofollow
  • Cache-Control: no-store, no-cache, must-revalidate 等でキャッシュ抑止

3. インストールと配置

  1. プラグインを配置
  • wp-content/plugins/simple-maintenance-switch/ を作成
  • 後述の PHP ファイルを simple-maintenance-switch.php として保存
  • 管理画面 → プラグイン → 有効化
  1. メンテページを配置(公開ルート)
   /mentainance/
     ├─ index.html  または  index.php
     ├─ css/
     ├─ js/
     ├─ images/
     └─ favicon.ico

ドメイン直下(DocumentRoot) に置く点がポイント。WP をサブディレクトリに入れていても問題ありません。


4. 相対パス崩れの原因と解決(① or ②)

なぜ崩れる?

メンテ中に /news/ のような任意URLへアクセスされた場合、<link href="css/style.css">/news/css/style.css と解決されます。実体は /mentainance/css/style.css なのに参照がズレるため、相対パスは壊れます

解決策(本記事の推奨はこの2択)

① ルート相対パスに統一(最も堅牢・簡単)
/mentainance/... で始める。HTML を直接直すだけで完了。
<base> を使う(既存の相対参照を活かしたい場合)
<head><base href="https://example.com/mentainance/"> を入れる。
 - ドメイン・スキームを含む絶対URL<base> を推奨(ブラウザ解決が確実)
 - 既存HTMLを大きく変えずに運用可能

③ プラグイン側で <base> を自動注入する方法もありますが、まずは ①/② がシンプルで確実です。


5. メンテページのサンプル(①/② 両対応)

以下のどちらかお好みで。

5-1) ① ルート相対パス版(/mentainance/ 起点)

/mentainance/index.html

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>メンテナンス中 | Example Corp.</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- ルート相対で参照(最も堅牢) -->
  <link rel="icon" href="/mentainance/favicon.ico">
  <link rel="stylesheet" href="/mentainance/css/style.css">
</head>
<body>
  <main class="container">
    <section class="card">
      <img src="/mentainance/images/open.png" alt="" class="brand">
      <h1>ただいまメンテナンス中です</h1>
      <p>サイトリニューアル作業に伴い、一時的に公開を停止しています。しばらくお待ちください。</p>
      <hr>
      <p class="sub">We’ll be back online soon.</p>
    </section>
  </main>
  <script src="/mentainance/js/common.js"></script>
</body>
</html>

5-2) ② <base> 版(相対参照のまま運用)

/mentainance/index.php

<?php
// 現在のスキーム・ホストを自動取得して <base> に採用
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host   = $_SERVER['HTTP_HOST'] ?? '';
$base   = $scheme . '://' . $host . '/mentainance/';
?>
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>メンテナンス中 | Example Corp.</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- 相対参照をこのURL基準で解決 -->
  <base href="<?php echo htmlspecialchars($base, ENT_QUOTES); ?>">
  <link rel="icon" href="favicon.ico">
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <main class="container">
    <section class="card">
      <img src="images/open.png" alt="" class="brand">
      <h1>ただいまメンテナンス中です</h1>
      <p>サイトリニューアル作業に伴い、一時的に公開を停止しています。しばらくお待ちください。</p>
      <hr>
      <p class="sub">We’ll be back online soon.</p>
    </section>
  </main>
  <script src="js/common.js"></script>
</body>
</html>

簡易スタイル例(共通)/mentainance/css/style.css

:root{--bg:#cae8f4;--card:#fff;--text:#0f2a44;--sub:#2c4d6b}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);font:16px/1.7 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
.container{min-height:100%;display:grid;place-items:center;padding:24px}
.card{background:var(--card);max-width:880px;width:100%;border-radius:14px;padding:48px 40px;box-shadow:0 8px 28px rgba(0,0,0,.08)}
.brand{height:48px;margin-bottom:24px}
h1{margin:0 0 16px;color:var(--text);font-size:28px;font-weight:700}
p{margin:0 0 14px;color:var(--text)}
hr{border:none;border-top:1px solid #a8d9ea;margin:20px 0}
.sub{color:var(--sub)}
@media (max-width:640px){.card{padding:32px 20px} h1{font-size:22px}}

6. 管理UIの使い方

  • 設定 → Maintenance Mode
  • 「メンテナンスを有効化」… 未ログインは /mentainance/ を 503 で表示
  • 「ログインユーザーの扱い」… チェックあり=全員通す/なし=管理者のみ通す
  • 管理バーのトグル
  • 右上「Maintenance: ON/OFF」をクリックで即時切替(nonce・権限チェックあり)

7. CDN/キャッシュ環境での注意点

  • 503 をキャッシュしないよう CDN/リバプロ側の設定を確認
  • 切替直後に古い応答が返る場合は パージ を実施
  • <base> 方式(②)の場合、HTML ヘッダを書き換える一部中間装置がある環境では <base> が影響を受ける可能性があるため、最も堅牢なのは①ルート相対です

8. よくある質問(FAQ)

Q. WP をサブディレクトリに入れています。問題ありませんか?
A. 問題ありません。プラグインは $_SERVER['DOCUMENT_ROOT'] を基準に /mentainance/ を探します。

Q. REST API も止めたい
A. コード内の /wp-json 許可を削れば止まります。

Q. 相対のまま使いたいが、HTMLは触りたくない
A. <base> をプラグイン側で自動注入する拡張が可能です。必要なら追記版を用意します。

Q. マルチサイトでも使えますか?
A. サイトごとに ON/OFF できます(ネットワーク有効化も可)。メニューは各サイトの「設定」に出ます。


9. プラグインの全ソースコード

ファイル名: wp-content/plugins/simple-maintenance-switch/simple-maintenance-switch.php
Author はダミー表記(Example Devs)にしています。

<?php
/**
 * Plugin Name: Simple Maintenance Switch
 * Description: 未ログインの訪問者にはドメイン直下 /mentainance/ を表示。管理画面から ON/OFF 切替可。ログイン中(設定で「全員」または「管理者のみ」)は通常サイトを表示。
 * Version: 1.0.0
 * Author: Example Devs
 */

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

/**
 * 互換ヘルパ: PHP7環境でも安定して動く starts_with
 */
if (!function_exists('sms_starts_with')) {
  function sms_starts_with(string $haystack, string $needle): bool {
    return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0;
  }
}

/** 既定値 */
function sms_default_options(): array {
  return [
    'enabled'            => false, // メンテON/OFF
    'allow_all_loggedin' => true,  // true=ログイン済みは全員通す / false=管理者のみ通す
  ];
}

/** 設定取得(未保存のキーは既定値で補完) */
function sms_get_options(): array {
  $opt = get_option('sms_options', []);
  return array_merge(sms_default_options(), is_array($opt) ? $opt : []);
}

/** 設定登録(サニタイズ含む) */
add_action('admin_init', function () {
  register_setting('sms_group', 'sms_options', [
    'type' => 'array',
    'sanitize_callback' => function ($in) {
      $out = sms_default_options();
      $out['enabled']            = !empty($in['enabled']);
      $out['allow_all_loggedin'] = !empty($in['allow_all_loggedin']);
      return $out;
    },
  ]);
});

/** 設定ページ(設定 → Maintenance Mode) */
add_action('admin_menu', function () {
  add_options_page(
    'Maintenance Mode',
    'Maintenance Mode',
    'manage_options',
    'sms-settings',
    'sms_render_settings_page'
  );
});

/** 設定ページの描画 */
function sms_render_settings_page() {
  if (!current_user_can('manage_options')) return;
  $opt = sms_get_options();
  ?>
  <div class="wrap">
    <h1>Maintenance Mode</h1>
    <form method="post" action="options.php">
      <?php settings_fields('sms_group'); ?>
      <table class="form-table" role="presentation">
        <tr>
          <th scope="row">メンテナンスを有効化</th>
          <td>
            <label>
              <input type="checkbox" name="sms_options[enabled]" value="1" <?php checked($opt['enabled']); ?>>
              未ログインの訪問者に <code>/mentainance/index.(php|html)</code> を表示(HTTP 503 応答)
            </label>
          </td>
        </tr>
        <tr>
          <th scope="row">ログインユーザーの扱い</th>
          <td>
            <label>
              <input type="checkbox" name="sms_options[allow_all_loggedin]" value="1" <?php checked($opt['allow_all_loggedin']); ?>>
              ログイン済みは<strong>全員</strong>通す(オフの場合は<strong>管理者のみ</strong>通す)
            </label>
          </td>
        </tr>
      </table>
      <?php submit_button('保存'); ?>
    </form>
    <hr>
    <p>静的ページの場所(サーバー公開ルート配下):<code><?php echo esc_html($_SERVER['DOCUMENT_ROOT'] ?? '(unknown)'); ?>/mentainance/</code></p>
    <p><a class="button" target="_blank" href="/mentainance/" rel="noopener">/mentainance を開く</a></p>
  </div>
  <?php
}

/** 管理バーにON/OFFトグル(nonce付き)を追加 */
add_action('admin_bar_menu', function($bar){
  if (!current_user_can('manage_options')) return;
  $opt = sms_get_options();
  $enabled = $opt['enabled'];
  $action = $enabled ? 'sms_disable' : 'sms_enable';

  $bar->add_node([
    'id'    => 'sms-toggle',
    'title' => $enabled ? 'Maintenance: ON' : 'Maintenance: OFF',
    'href'  => wp_nonce_url(admin_url("admin-post.php?action=$action"), $action),
    'meta'  => ['title' => 'クリックで切替']
  ]);
}, 100);

/** トグルハンドラ(有効化) */
add_action('admin_post_sms_enable', function(){
  if (!current_user_can('manage_options')) wp_die('forbidden');
  check_admin_referer('sms_enable');
  $opt = sms_get_options();
  $opt['enabled'] = true;
  update_option('sms_options', $opt);
  wp_safe_redirect(wp_get_referer() ?: admin_url('options-general.php?page=sms-settings'));
  exit;
});

/** トグルハンドラ(無効化) */
add_action('admin_post_sms_disable', function(){
  if (!current_user_can('manage_options')) wp_die('forbidden');
  check_admin_referer('sms_disable');
  $opt = sms_get_options();
  $opt['enabled'] = false;
  update_option('sms_options', $opt);
  wp_safe_redirect(wp_get_referer() ?: admin_url('options-general.php?page=sms-settings'));
  exit;
});

/**
 * メンテ挙動(フロント側)
 * - OFFなら何もしない
 * - 管理画面はWP標準に任せる
 * - 特別パスは常に許可
 * - /mentainance/ 配下は常に許可(静的資産もOK)
 * - ログイン済みは「全員」または「管理者のみ」で通す
 * - それ以外は 503 で /mentainance/index.(php|html) を返す
 */
add_action('template_redirect', function () {
  $opt = sms_get_options();
  if (!$opt['enabled']) return;  // OFF
  if (is_admin()) return;        // 管理画面はWP標準制御

  $uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';

  // 常に許可する特別パス
  if (
    sms_starts_with($uri, '/wp-login.php') ||
    sms_starts_with($uri, '/wp-admin/admin-ajax.php') ||
    sms_starts_with($uri, '/wp-cron.php') ||
    sms_starts_with($uri, '/xmlrpc.php') ||
    sms_starts_with($uri, '/wp-json') ||
    $uri === '/robots.txt' ||
    $uri === '/favicon.ico'
  ) {
    return;
  }

  // /mentainance/ 配下の静的資産は常に許可
  if (sms_starts_with($uri, '/mentainance/')) {
    return;
  }

  // ログイン済みは通す(設定に応じて)
  if (is_user_logged_in()) {
    if ($opt['allow_all_loggedin'] || current_user_can('manage_options')) {
      return;
    }
  }

  // ここから未ログインユーザーへの応答(503 + /mentainance/)
  $docroot = rtrim($_SERVER['DOCUMENT_ROOT'] ?? ABSPATH, '/');
  $candidate = [
    $docroot . '/mentainance/index.php',
    $docroot . '/mentainance/index.html',
  ];
  $target = '';
  foreach ($candidate as $c) {
    if (is_file($c)) { $target = $c; break; }
  }

  if (!headers_sent()) {
    header('Content-Type: text/html; charset=UTF-8');
    header('X-Robots-Tag: noindex, nofollow', true);
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0', true);
    header('Pragma: no-cache', true);
    header('Expires: 0', true);
    header('Retry-After: 3600', true);
    status_header(503);
  }

  if ($target) {
    $ext = strtolower(pathinfo($target, PATHINFO_EXTENSION));
    if ($ext === 'php') {
      require $target;
    } else {
      readfile($target);
    }
  } else {
    echo '<!doctype html><meta charset="utf-8"><title>Maintenance</title><h1>現在メンテナンス中です</h1><p>しばらくお待ちください。</p>';
  }
  exit;
}, 0);

コメント

タイトルとURLをコピーしました