WordPressの勉強として、プラグインを使わずに会員制サイトを自作しました。
この記事では、会員以外に記事を非公開にする方法、ログインや会員登録の機能など、会員制サイトの作り方を紹介します。
なお、私の理解が足りなくて間違っている部分があるかもしれません。全て自己責任で参考にしてください。
また、プラグインを使って会員制サイトを作る方法は以下の記事で紹介しています。
会員制サイトのデモ
以下のようなデモサイトをサンプルとして制作しました。
会員登録をしてログインすると、マイページが表示されます。
マイページにはログインしているときだけ閲覧できる記事が掲載されており、キャンペーン情報の告知やクーポンの配布などができます。
主な特徴は以下の通りです。
- ログイン、会員登録、パスワードの変更がサイト上でできる。
- 会員登録やパスワードの変更をすると、会員や管理者に通知メールが送信される。
- ログイン中とログアウト中で記事やメニューの内容を変える。
- マイページの会員限定コンテンツはログインしないと閲覧できない。
- 会員限定コンテンツに掲載した画像やPDFファイルはログインしないと閲覧できない。
- 会員登録やパスワードのリセットは、メールアドレスの本人確認をしないと完了しない。
ここからは、このデモサイトをベースに会員制サイトの作り方を紹介します。
会員制サイトを作るには、大きく分けて二つの作業が必要です。
- ログインとログアウト中で表示内容を変える
- ログインや会員登録などユーザーを管理する機能を作成する
この記事では、上記の一つ目について紹介します。なお、非公開の記事はnoindexを指定したり、SNSのシェアボタンを外したりと、細かい部分は省略しています。
記事を非公開にする
ログインしないと記事を閲覧できないようにする方法は、いくつか考えられます。
カテゴリーを使う
一つ目はWordPressに標準で用意されているカテゴリーを使う方法です。会員限定のカテゴリーを作り、それが割り当てられた記事を非公開にします。
個別記事を非公開にする
まずは、以下のように非公開の記事か判定する関数を作成します。
function is_secret_page($post_id) {
// 非公開にする親子カテゴリーを取得
// SECRET_CATEGORYは会員限定のカテゴリーIDを指定
$secret_category_list = array(SECRET_CATEGORY);
$secret_category_list = array_merge($secret_category_list, get_term_children(SECRET_CATEGORY, 'category'));
$is_secret_page = false;
if(in_category($secret_category_list, $post_id)){
$is_secret_page = true;
}
return $is_secret_page;
}
会員限定のカテゴリーIDをget_term_childrenを使用して親子両方とも取得して、それが割り当てられていればtrueを返しています。
この関数を例えば以下のように使います。
function secret_page_redirect() {
if(is_admin() || !is_main_query()) return;
if(!is_user_logged_in() && is_single() && is_secret_page(get_the_ID())) {
wp_safe_redirect('ログインページのURL');
exit();
}
}
add_action('template_redirect', 'secret_page_redirect');
is_user_logged_inでログイン状況を取得します。ログインしておらず、しかもis_secret_pageで非公開の記事と判定された場合は、wp_safe_redirectでログインページにリダイレクトしています。
一覧ページから非公開の記事を削除する
先ほどのプログラムで個別記事にアクセスしたときは非公開にできましたが、このままでは一覧ページに記事が表示されます。
もちろん、一覧ページに非公開の記事を表示させて、それをクリックしたときにログインページに移動するという仕様でも良いですが、もしも一覧ページから削除したい場合には追加の処理が必要です。
function exclude_secret_page($query) {
if(is_admin() || !is_main_query()) return;
$query->set('cat', '-' . SECRET_CATEGORY);
}
add_action('pre_get_posts', 'exclude_secret_page');
pre_get_postsのアクションフックを使用して、メインクエリにカテゴリーの絞り込み条件を追加しています。カテゴリーIDにマイナスをつけると、「除外する」という意味になります。
また、サイドバーのウィジェットなどにカテゴリー一覧を表示しているときに、会員限定のカテゴリーを削除するには以下のようなプログラムを記述します。
function custom_widget_categories_args($cat_args){
$cat_args['exclude'] = array(SECRET_CATEGORY);
return $cat_args;
}
add_filter('widget_categories_args', 'custom_widget_categories_args', 10);
widget_categories_argsというフィルターフックで、除外するカテゴリーIDをexcludeのパラメータに指定します。なお、親カテゴリーを指定すれば、自動的に子カテゴリーも無効になります。
他にも、最近の投稿やアーカイブのウィジェットなども対応する必要があり、サイト上から完全に非公開の記事を削除するのは結構面倒だと思います。
カスタムフィールドを使う
二つ目はカスタムフィールドを使う方法です。カスタムフィールドとは、記事に対して入力項目を新たに追加できる機能です。
非公開か非公開でないか選択できる項目を作り、記事ごとに設定します。
個別記事を非公開にする
非公開の記事か判定する関数は以下の通りです。
function is_secret_page($post_id) {
if(get_post_meta($post_id, 'is_secret', true) == 1) {
return true;
}
return false;
}
get_post_metaでカスタムフィールドの値を取得して、それが「1(非公開)」の場合にtrueを返しています。
なお、カスタムフィールドを作成する場合は「Advanced Custom Fields」というプラグインが便利です。
Advanced Custom Fieldsの設定方法は、以下を参考にしてください。
・フィールドラベル:非公開コンテンツかどうか
・フィールド名:is_secret
・フィールドタイプ:ラジオボタン
・必須か?:はい
・選択肢:
0 : 非公開でない
1 : 非公開
・レイアウト:水平
・返り値:Value
一覧ページから非公開の記事を削除する
一覧ページから削除する場合は以下のようなプログラムを記述します。
function exclude_secret_page($query) {
if(is_admin() || !is_main_query()) return;
if(is_archive() || is_home() || is_front_page() || is_search()) {
$query->set('meta_query', array(
'relation' => 'OR',
array(
'key' => 'is_secret',
'value' => 0,
'compare' => '=',
'type ' => 'NUMERIC'),
array(
'key' => 'is_secret',
'compare' => 'NOT EXISTS')));
}
}
add_action('pre_get_posts', 'exclude_secret_page');
カスタムフィールドの抽出条件はmeta_queryを使います。is_secretが「0(非公開でない)」または、is_secretが未設定の記事だけを取得しています。
カスタム投稿タイプを使う
三つ目はカスタム投稿タイプを使う方法です。カスタム投稿タイプとは、WordPressに標準で用意されている「投稿」と「固定ページ」の他に、新しい投稿の種類を追加できる機能です。
会員限定の記事を投稿するカスタム投稿タイプを作り、それらを非公開にします。
非公開の記事か判定する関数は以下の通りです。
function is_secret_page($post_id) {
if(get_post_type($post_id) === 'カスタム投稿タイプのスラッグ') {
return true;
}
return false;
}
get_post_typeで投稿タイプを取得して、それが会員限定のカスタム投稿タイプの場合にtrueを返しています。
ちなみに、今回のデモサイトではカスタム投稿タイプを使って記事を非公開にしています。固定ページを使ってマイページを作成して、カスタム投稿タイプの一覧を表示しています。
マイページにアクセスした場合もログインページにリダイレクトするため、以下のようにプログラムを修正しています。
function is_secret_page($post_id) {
if(get_post_type($post_id) === 'カスタム投稿タイプのスラッグ' ||
(get_post_type($post_id) === 'page' && get_post_field('post_name', $post_id) === 'マイページのスラッグ')) {
return true;
}
return false;
}
get_post_fieldで固定ページのスラッグを取得して、マイページのスラッグと一致した場合にもtrueを返しています。
もしも固定ページをリダイレクトの対象にする場合は、ログインページを除外しないと無限ループするので注意してください。
カスタム投稿タイプを作成する際は「Custom Post Type UI」というプラグインが便利です。
Custom Post Type UIの設定方法は、以下を参考にしてください。
- 投稿タイプスラッグ:member
- 複数形のラベル:会員限定
- 単数形のラベル:会員限定
- 一般公開:True
- 一般公開クエリー可:True
- UIを表示:True
- ナビゲーションメニューに表示:True
- Delete with user:False
- REST APIで表示:False
- アーカイブあり:False
- 検索から除外:True
- 権限タイプ:post
- 階層:False
- リライト:True
- カスタムリライトスラッグ:空欄
- フロントでのリライト:True
- クエリー変数:True
- カスタムクエリー変数スラッグ:空欄
- メニューの位置:5
- メニューに表示:True
「REST APIで表示」でTrueを選択すると、以下のようなURLにアクセスすると記事の情報を外部から取得できてしまいます。
(トップページのURL)/wp-json/wp/v2/(カスタム投稿タイプのスラッグ)
一方で、「REST APIで表示」でFalseを選択すると記事の情報は取得できませんが、REST APIに依存しているブロックエディターなどが使えなくなる可能性があります。
必要に応じて、ログアウト中のみREST APIを無効にするといったプログラムを記述してください。
現在のWordPressではREST APIが標準で有効になっています。カテゴリーやカスタムフィールドを使う方法においても、厳密に記事を非公開にしたい場合はREST APIを考慮する必要があります。
一覧ページから非公開の記事を削除する
カスタム投稿タイプの場合には、「アーカイブあり」をFalseにすれば一覧ページ自体が作成されません。
記事のコンテンツを非公開にする
先ほど自作したsecret_page_redirectという関数では記事自体を非公開にしましたが、ここでは部分的に非公開にする方法を紹介します。
今回のデモサイトでは、通常の投稿の下に会員登録を促すCTAを表示しています。
既に会員になっている方にCTAを表示する意味はないので、ログイン中は表示していません。
ログイン中とログアウト中で記事の内容を切り替えるには、ショートコードを使用します。
ショートコードとは、[logout_content]のように括弧つきのブロックのことです。これを記述すると、事前に定義されたプログラムが実行されます。
ログアウト中のみに表示するショートコードは以下の通りです。
function shortcode_logout_content($atts, $content = null) {
if(!is_user_logged_in()) {
return $content;
}
return;
}
add_shortcode('logout_content', 'shortcode_logout_content');
とても単純なプログラムです。is_user_logged_inでログイン状況を取得して、ログインしていない場合にコンテンツを返しています。
同様に、ログイン中のみに表示するショートコードは以下の通りです。
function shortcode_login_content($atts, $content = null) {
if(is_user_logged_in()) {
return $content;
}
return;
}
add_shortcode('login_content', 'shortcode_login_content');
ショートコードの使い方は、以下のようにログイン中またはログアウト中に表示したい内容をショートコードで囲むだけです。
ナビゲーションメニューを非公開にする
今回のデモサイトでは、ログイン中とログアウト中でナビゲーションメニューを自動で切り替えています。
ログアウト中は「会員登録」「ログイン」というメニューを表示して、ログイン中は「マイページ」「ログアウト」というメニューを表示しています。
上のように、メニューを作成する際にログイン中のみに表示するものはCSSクラスに「login」、ログアウト中のみに表示するものは「logout」を設定しています。
CSSクラスを設定する入力欄が表示されない場合は、画面上部にある「表示オプション」から「CSSクラス」にチェックを入れてください。
ナビゲーションメニューを切り替えるプログラムは以下の通りです。
function custom_wp_get_nav_menu_items($menu_items) {
// 管理画面は切り替えない
if(is_admin()) return $menu_items;
foreach($menu_items as $index=>$menu_item) {
// CSSクラスに「login」がある場合
if(in_array('login', $menu_item->classes, true)) {
if(!is_user_logged_in()) {
unset($menu_items[$index]);
continue;
}
}
// CSSクラスに「logout」がある場合
if(in_array('logout', $menu_item->classes, true)) {
if(is_user_logged_in()) {
unset($menu_items[$index]);
continue;
}
}
// CSSクラスに「login_logout」がある場合
if(in_array('login_logout', $menu_item->classes, true)) {
if(is_user_logged_in()) {
$menu_item->url = wp_logout_url(get_permalink());
$menu_item->title = 'ログアウト';
} else {
$menu_item->url = 'ログインページのURL';
$menu_item->title = 'ログイン';
}
}
}
return array_values($menu_items);
}
add_filter('wp_get_nav_menu_items', 'custom_wp_get_nav_menu_items');
wp_get_nav_menu_itemsというフィルターを使います。メニューの内容を取得してCSSクラスが設定されている場合には、ログイン状況に応じてメニューを削除しています。
上のように、一つのメニューで異なる内容を表示することも可能です。
先ほどのプログラムでは、「login_logout」というCSSクラスが設定されている場合には、ログイン中はログアウトのリンク、ログアウト中はログインのリンクを表示しています。
サムネイル画像を非公開にする
今回のデモサイトではログアウト中に会員限定記事の一覧ページを表示していないので関係ありませんが、もしも表示している場合には、サムネイル画像を非公開にすることもできます。
WordPressでサムネイル画像を表示するときは、get_the_post_thumbnailやthe_post_thumbnailといった関数を使うことが多いと思います。
post_thumbnail_htmlというフィルターフックは、それらの関数をカスタマイズできます。
function custom_post_thumbnail_html($html, $post_id) {
// SECRET_IMAGEは非公開を表す画像のURL
if(!is_user_logged_in() && is_secret_page($post_id)) {
$html = '<img src="' . SECRET_IMAGE . '" alt="非公開" />';
}
return $html;
}
add_filter('post_thumbnail_html', 'custom_post_thumbnail_html', 10, 2);
上記のプログラムでは、is_secret_pageという自作の関数で非公開の記事か判定して、ログアウト中かつ非公開の記事の場合には、専用の画像を表示しています。
function custom_wp_get_attachment_image_src($image) {
if(!is_user_logged_in() && is_secret_page(get_the_ID())) {
$image[0] = SECRET_IMAGE;
}
return $image;
}
add_filter('wp_get_attachment_image_src', 'custom_wp_get_attachment_image_src');
また、サムネイル画像を取得する関数にはwp_get_attachment_image_srcというものもあります。上記はこれを使っている場合に非公開の画像を表示するプログラムです。
ファイルを非公開にする
これまでの手順で記事は非公開にできましたが、画像やPDFなどのファイルは別です。そのため、ファイルのURLが分かってしまえば、ログインしなくても閲覧できます。
ファイル名を推測できない分かりにくい名前にする、そもそもファイルには大事なことは書かず記事に文字で書くといったことは、単純ですが効果的だと思います。
ここでは、非公開の記事にアップロードしたファイルを外部から閲覧できないように対策します。
主な流れとしては以下の通りです。
- 「secret」という名前のフォルダを作成する
- 非公開の記事からアップロードしたファイルは「secret」のフォルダに格納する
- 「secret」のフォルダの中は外部から閲覧できないようにする
非公開にするフォルダを作成
まずは、FTPソフトや「WP File Manager」などのプラグインを使用して、非公開にするフォルダを作成します。
通常、メディアライブラリからアップロードしたファイルは「wp-content/uploads/年/月/」という場所に格納されます(年月ベースのフォルダが有効な場合)。
そこで、場所はどこでも良いですが、例えば「wp-content/uploads/」の直下に非公開にするフォルダを作ります。
ここでは「secret」というフォルダ名にしていますが、少し露骨なのでセキュリティ的には良くないと思います。
ファイルのアップロード先を変更する
以下のプログラムを記述して、非公開の記事からアップロードしたファイルを「secret」のフォルダに格納します。
function custom_upload_dir($upload){
$post_id = sanitize_text_field($_REQUEST['post_id']);
if(is_secret_page($post_id)) {
$upload['path'] = $upload['basedir'] . "/secret". $upload['subdir'];
$upload['url'] = $upload['basedir'] . "/secret". $upload['subdir'];
}
return $upload;
}
add_filter('upload_dir', 'custom_upload_dir');
upload_dirというフィルターフックを使います。is_secret_pageで非公開の記事か判定して、非公開の場合にはアップロード先をsecretフォルダに変えています。
すると、非公開の記事からアップロードしたファイルは以下のURLに格納されていると思います。
wp-content/uploads/secret/年/月/
非公開の記事を開いた状態でメディアライブラリから追加しないとsecretフォルダにアップロードされません。例えば、管理画面の「メディア」から直接アップロードしたファイルは通常のパスに格納されます。
外部からファイルを閲覧できないようにする
secretフォルダのファイルを外部から閲覧できないようにするには、まずは以下のような「.htaccess」というファイルをsecretフォルダの直下に作成します。
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteRule (.*) https://{ドメイン名}/?file=$1 [L]
</IfModule>
ファイルの最後に空行を入れておくとエラーが少なくなるようです。
- <IfModule mod_rewrite.c>:リライトに必要なmod_rewriteという機能が有効なときのみ実行
- RewriteEngine on:リライトの機能を有効化
- RewriteRule:リライトのルール
- [L]:条件に該当したらここで終了
.htaccessの内容は、secretフォルダの配下にある全てのファイルに対して、アクセスがあった場合にサイトのトップページにリライトしています。
その際に、年月を含むファイルのパスをfileというクエリストリングに設定しています。
サイトのトップページにリライトした後の処理は以下の通りです。
function read_secret_file() {
if(is_admin() || !is_main_query()) return;
if(isset($_GET['file'])) {
if(!is_user_logged_in()) {
$file_path = SECRET_IMAGE;
}else {
$file = sanitize_text_field($_GET['file']);
$upload_dir = wp_upload_dir();
$file_path = $upload_dir['basedir'] . "/secret/". $file;
// 通常ファイルが存在しない
if(!is_file($file_path)) exit();
}
$filetype = wp_check_filetype($file_path);
if(!$filetype['type']) exit();
$file_list = explode("/", $file);
if(is_array($file_list)) {
$file_name = end($file_list);
}else {
// 年月フォルダがない場合
$file_name = $file_list;
}
header('Content-Type: ' . $filetype['type']);
header('Content-Length: ' . filesize($file_path));
header('Content-Disposition: inline; filename="' . $file_name . '"');
readfile($file_path);
exit();
}
return;
}
add_filter('init', 'read_secret_file');
fileというクエリストリングが渡されたときだけ処理を行います。ログイン状況を取得して、ログアウト中は非公開を表す画像、ログイン中は要求されたファイルをreadfileで出力しています。
毎回リライトを行うのでファイルが多いサイトは読み込みが遅くなるかもしれません。もっとスマートな方法があれば改善してください。
他には、以下のようにリファラーで判断する方法もあります。
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{HTTP_REFERER} !^http(s)?://{ドメイン名} [NC]
RewriteRule ¥.(jpg|png|gif|pdf)$ {非公開を表す代替画像} [NC,R,L]
</IfModule>
- RewriteCond:直後のRewriteRuleを適用する条件
- HTTP_REFERER:参照元URLを表すサーバーの変数
- [NC]:大文字と小文字を区別しない
- [R]:リダイレクトする
RewriteCondでリファラーとドメインを比較していますが、エクスクラメーションマーク(!)を付加することで否定しています。
つまり、直接ブラウザのアドレスバーにファイルのURLを入力した場合など、参照元URLが自サイトでない場合は代替画像を表示しています。
しかし、リファラーの偽装は簡単と言われており、インターネットで調べれば偽装のやり方やツールが多く紹介されています。
それが許容できるのであれば、最初のやり方よりは処理速度が速い気がします。
コメント(現在、質問は受け付けていません)