2026年7月7日

2026年7月7日

WordPressでコンテンツ保護と有料記事ペイウォールを実装する方法

はじめに

有料コンテンツ・会員限定記事をWordPressで実装する場合、フロントエンドのJavaScriptだけで制御するのはセキュリティ上不十分です。サーバーサイドの the_content フィルターを使って本文を適切に制限し、WooCommerceサブスクリプションと連携することで堅牢なペイウォールを構築できます。

症状・原因

  • 有料記事をブラウザの開発者ツールで見るとHTMLに全文が含まれており、CSSで隠しているだけになっている
  • ユーザーのサブスクリプション状態と記事のアクセスレベルを紐付ける仕組みがない
  • 無料プレビューと有料コンテンツの境界をどこに設定すべきか判断できていない

解決手順

ステップ1:the_contentフィルターで非購読者向けに本文を切り詰める

サーバーサイドでHTMLを切り詰め、非サブスクライバーには本文を送信しません。

<?php
// functions.php に追加 ─ コンテンツ保護フィルター

add_filter( 'the_content', function( $content ) {
    // ログイン中のユーザーはフルコンテンツを表示する
    if ( is_user_logged_in() ) {
        return $content;
    }

    // ペイウォール対象の投稿タイプのみ制限する
    if ( ! is_singular( 'post' ) ) {
        return $content;
    }

    // アクセスレベルが設定されていない投稿は制限しない
    $access_level = get_post_meta( get_the_ID(), '_access_level', true );
    if ( empty( $access_level ) || $access_level === 'free' ) {
        return $content;
    }

    // 非ログインユーザーには最初の段落のみ表示する
    $paragraphs    = explode( '</p>', $content );
    $teaser        = implode( '</p>', array_slice( $paragraphs, 0, 2 ) ) . '</p>';

    // ティーザーの後にCTAブロックを追加する
    $teaser .= get_paywall_cta_html();

    return $teaser;
}, 20 ); // 優先度20で他のフィルターの後に実行する

// ペイウォールのCTAブロックを生成する
function get_paywall_cta_html() {
    return '
    <div class="paywall-cta" style="border:2px solid #0073aa; padding:24px; text-align:center;
         margin:24px 0; border-radius:8px; background:#f0f6fc;">
        <h3 style="color:#1d2327; margin:0 0 8px;">この記事の続きを読むには</h3>
        <p style="color:#50575e; margin:0 0 16px;">会員登録(月額980円)で全記事が読み放題になります。</p>
        <a href="/register/" style="background:#0073aa; color:#fff; padding:12px 24px;
           border-radius:4px; text-decoration:none; font-weight:bold;">
            無料で7日間お試し
        </a>
        <p style="margin:12px 0 0;"><a href="/login/">すでに会員の方はログイン</a></p>
    </div>';
}

ステップ2:_access_levelポストメタとcurrent_user_canで権限チェックを実装する

投稿ごとにアクセスレベルを設定し、ユーザーの購読プランと照合します。

<?php
// functions.php に追加 ─ アクセスレベルのチェック関数

/**
 * 現在のユーザーが指定した投稿にアクセスできるかチェックする
 *
 * @param int $post_id チェックする投稿ID
 * @return bool アクセス可能ならtrue
 */
function user_can_access_post( $post_id ) {
    // 管理者・編集者は常にアクセス可能
    if ( current_user_can( 'edit_posts' ) ) {
        return true;
    }

    $access_level = get_post_meta( $post_id, '_access_level', true );

    // アクセスレベルなし または free は誰でも読める
    if ( empty( $access_level ) || $access_level === 'free' ) {
        return true;
    }

    // ログインが必要なコンテンツ
    if ( ! is_user_logged_in() ) {
        return false;
    }

    $user_id   = get_current_user_id();
    // ユーザーの購読プランをメタから取得する
    $user_plan = get_user_meta( $user_id, 'subscription_plan', true );

    $plan_levels = [
        'basic'    => 1, // ベーシックプラン
        'standard' => 2, // スタンダードプラン
        'premium'  => 3, // プレミアムプラン
    ];

    $content_level = $plan_levels[ $access_level ] ?? 99;
    $user_level    = $plan_levels[ $user_plan ] ?? 0;

    // ユーザーのプランレベルがコンテンツに必要なレベル以上かチェック
    return $user_level >= $content_level;
}

// 管理画面でアクセスレベルを設定するメタボックスを追加する
add_action( 'add_meta_boxes', function() {
    add_meta_box(
        'access-level',
        'アクセスレベル設定',
        function( $post ) {
            $level = get_post_meta( $post->ID, '_access_level', true ) ?: 'free';
            wp_nonce_field( 'save_access_level', 'access_level_nonce' );
            ?>
            <select name="access_level" id="access_level">
                <option value="free"     <?php selected( $level, 'free' ); ?>>無料(全員)</option>
                <option value="basic"    <?php selected( $level, 'basic' ); ?>>ベーシック以上</option>
                <option value="standard" <?php selected( $level, 'standard' ); ?>>スタンダード以上</option>
                <option value="premium"  <?php selected( $level, 'premium' ); ?>>プレミアムのみ</option>
            </select>
            <?php
        },
        'post', 'side', 'high'
    );
} );

ステップ3:ティーザーコンテンツとログイン・購読CTAブロックを実装する

最初のN段落を無料でプレビューし、続きを有料コンテンツとして保護します。

<?php
// functions.php に追加 ─ ティーザーとCTAの高度な実装

add_filter( 'the_content', function( $content ) {
    if ( ! is_singular() ) {
        return $content;
    }

    $post_id = get_the_ID();

    // アクセス可能なユーザーにはフルコンテンツを返す
    if ( user_can_access_post( $post_id ) ) {
        return $content;
    }

    $teaser_paragraphs = (int) get_post_meta( $post_id, '_teaser_paragraphs', true ) ?: 3;

    // DOMDocumentで安全にHTMLを分割する
    $dom = new DOMDocument( '1.0', 'UTF-8' );
    libxml_use_internal_errors( true ); // 不正なHTMLのエラーを抑制する
    $dom->loadHTML( '<?xml encoding="UTF-8">' . $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
    libxml_clear_errors();

    $paragraphs = $dom->getElementsByTagName( 'p' );
    $total      = $paragraphs->length;
    $keep       = min( $teaser_paragraphs, $total );

    // ティーザー部分のHTMLを組み立てる
    $teaser_html = '';
    for ( $i = 0; $i < $keep; $i++ ) {
        $teaser_html .= $dom->saveHTML( $paragraphs->item( $i ) );
    }

    // ぼかし効果のオーバーレイを追加する
    $locked_html  = '<div class="content-locked" style="position:relative;">';
    $locked_html .= '<div style="filter:blur(4px); pointer-events:none; user-select:none;">';
    // 次の段落を意図的にぼかして「続きがある」ことを示す
    if ( $total > $keep ) {
        $locked_html .= $dom->saveHTML( $paragraphs->item( $keep ) );
    }
    $locked_html .= '</div>';
    $locked_html .= get_paywall_cta_html();
    $locked_html .= '</div>';

    return $teaser_html . $locked_html;
}, 20 );

ステップ4:時間限定の無料プレビューを実装する(最初の24時間)

記事公開直後の24時間は全員に無料公開し、その後ペイウォールを適用します。

<?php
// functions.php に追加 ─ 時間限定無料プレビュー

add_filter( 'the_content', function( $content ) {
    if ( ! is_singular() ) {
        return $content;
    }

    $post_id    = get_the_ID();
    $access_level = get_post_meta( $post_id, '_access_level', true );

    // アクセスレベルが設定されていない記事はスキップ
    if ( empty( $access_level ) || $access_level === 'free' ) {
        return $content;
    }

    $published  = get_post_time( 'U', true ); // 公開時刻をUNIXタイムスタンプで取得
    $now        = time();
    $free_hours = (int) get_option( 'paywall_free_preview_hours', 24 ); // デフォルト24時間

    // 公開からfree_hours時間以内はペイウォールを適用しない
    if ( ( $now - $published ) < ( $free_hours * HOUR_IN_SECONDS ) ) {
        return $content; // 無料プレビュー期間中はフルコンテンツを返す
    }

    // 24時間経過後は通常のアクセスチェックに委ねる(他のフィルターが処理)
    return $content;
}, 10 ); // 優先度10でステップ3の優先度20より先に実行する

ステップ5:WooCommerceサブスクリプションとの連携

WooCommerceのサブスクリプション状態をもとにアクセス制御を行います。

<?php
// functions.php に追加 ─ WooCommerce Subscriptions との統合

/**
 * WooCommerceサブスクリプションの状態からユーザーのプランを取得する
 *
 * @param int $user_id ユーザーID
 * @return string プラン名(basic/standard/premium/none)
 */
function get_user_subscription_plan( $user_id ) {
    // WooCommerce Subscriptions プラグインが必要
    if ( ! function_exists( 'wcs_get_users_subscriptions' ) ) {
        return 'none';
    }

    $subscriptions = wcs_get_users_subscriptions( $user_id );

    // プランIDとプラン名のマッピング(WooCommerce商品IDを使用)
    $plan_map = [
        'basic'    => [ 101, 102 ], // ベーシックプランの商品ID
        'standard' => [ 201, 202 ], // スタンダードプランの商品ID
        'premium'  => [ 301, 302 ], // プレミアムプランの商品ID
    ];

    foreach ( $subscriptions as $subscription ) {
        // 有効なサブスクリプションのみ確認する
        if ( ! $subscription->has_status( 'active' ) ) {
            continue;
        }

        foreach ( $subscription->get_items() as $item ) {
            $product_id = $item->get_product_id();

            foreach ( $plan_map as $plan => $ids ) {
                if ( in_array( $product_id, $ids, true ) ) {
                    // サブスクリプションのユーザーメタを更新しキャッシュする
                    update_user_meta( $user_id, 'subscription_plan', $plan );
                    return $plan;
                }
            }
        }
    }

    // アクティブなサブスクリプションがない場合はメタをリセットする
    delete_user_meta( $user_id, 'subscription_plan' );
    return 'none';
}

// サブスクリプション状態変化時にユーザーメタを自動更新する
add_action( 'woocommerce_subscription_status_updated', function( $subscription, $new_status ) {
    $user_id = $subscription->get_user_id();
    get_user_subscription_plan( $user_id ); // メタを最新状態に更新する
}, 10, 2 );

注意事項

  • クローラーへの対応: Googlebotなどの検索クローラーには本文をフルで渡すとSEOに有利だが、柔軟ペイウォール(Flexible Sampling)ポリシーに従いJSON-LD isAccessibleForFreehasPart スキーマを設定すること
  • キャッシュとの競合: WP Super Cache・W3 Total Cacheは非ログインユーザーにキャッシュを返すため、ペイウォール対象ページはキャッシュ対象から除外すること
  • JavaScriptのフォールバック: JS無効ユーザーでもサーバーサイドで適切にコンテンツが制限されるように設計すること

まとめ

サーバーサイドの the_content フィルターによるコンテンツ切り詰めとWooCommerceサブスクリプション連携を組み合わせることで、JavaScriptを無効化しても突破できない堅牢なペイウォールを実装できます。関連記事:WordPress REST APIにJWT・Application Password認証を実装する方法

お気軽にご相談ください

お見積りへ お問い合わせへ