
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、このブログでは(もちろん)技術者さん達へ向けて記事を書いているわけですが、同時に経営者側の方々に向けての内容も盛り込むようにしています。
個人的にはしっかり経営というものに携わったことがないので、実際には外していることも多いのかもしれませんが、お付き合いのある「あの人だったらどう考えるだろう」という視点で記事テーマを見つけたりもします。
そしてそんな中、とある機能のことが頭に浮かびました。
それが・・・・・・
ランディングページのどこで多く離脱しているかチェックする
機能です。
ランディングページとは、とてもシンプルに言うと「売り込むページ」です。
皆さんも見たことがあると思いますが、
- 「この商品はこんなことができますよ
」
- 「こんな問題を解決できますよ
」
というように営業マンさんの代わりになるようなページのことで、通常はトップページが多いんじゃないでしょうか。
そして、経営者側からすると訪問ユーザー全てに「登録 or 購入」をしてもらいたいわけですが、もしそうではなかった場合、「一体どのテキスト、画像を見た後にユーザーが見るのをやめてしまったか」を改善する必要があるはずです。
そこで
この「どこで離脱したのか」が分かるようにシステムを実装してみようと考えました。
ぜひ何かの参考になりましたら嬉しいです。
「ヒカルくんの YouTube で
大富豪のお宅を見てたら、
勤労意欲を失いました…(笑)」
開発環境: Laravel 8.x
目次 [非表示]
実装する仕組み
ランディングページに、JavaScript
の「スクロールイベント」を用意し、移動する度にその位置情報を送信&保存することで実装します。
そして、もしユーザーが「誘導したいページ」に移動したら、離脱はしていないので、その情報は(論理)削除するようにします。
では、実際に作業をしていきましょう
必要なファイルを用意する
では、いつものように先に必要なファイルをコマンドで用意します。
以下のコマンド2つを実行してください。
まずは「離脱」情報を保存するためのモデル&マイグレーションです。
php artisan make:model Withdrawal -m
そして、ランディングページ用のコントローラーです。
php artisan make:controller LandingPageController
これで、3つファイルが作成されました。
では、ひとつずつ中身を変更していきましょう。
マイグレーションを変更する
まずは「離脱」情報を保存することになるDBテーブルの設計です。
database/migrations/****_**_**_******_create_withdrawals_table.php
// 省略
public function up()
{
Schema::create('withdrawals', function (Blueprint $table) {
$table->id();
$table->ipAddress('ip')->comment('IP アドレス');
$table->unsignedInteger('scroll_top')->comment('離脱した位置:縦');
$table->unsignedInteger('scroll_left')->comment('離脱した位置:横');
$table->softDeletes();
$table->timestamps();
});
}
では、今回はテストデータはいらないので、この状態でマイグレーションを実行しておきましょう。
以下のコマンドを実行してください。
php artisan migrate
すると、実際のテーブルはこうなりました。
モデルを変更する
続いてモデルです。
app/Models/Withdrawal.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Withdrawal extends Model
{
use HasFactory, SoftDeletes;
protected $guarded = ['id']; //
ここを追加しました
}
なお、$guarded
を追加しているのは、Laravel
のあるあるエラー「mass assignment」に対応するためです。
また、ソフトデリート(論理削除)を有効にするためSoftDeletes
トレイトを読み込んでいます。
コントローラーを変更する
そして、コントローラーに用意するのは以下3つのメソッドです。
- index(): ランディングページ(ここのスクロール位置を取得&送信する)
- withdraw(): 離脱位置を保存(ランディングページのスクロール位置を保存)
- goal(): ゴール(離脱しなかった場合、データを削除)
app/Http/Controllers/LandingPageController.php
<?php
namespace App\Http\Controllers;
use App\Models\Withdrawal;
use Illuminate\Http\Request;
class LandingPageController extends Controller
{
public function index()
{
return view('landing_page.index');
}
public function withdrawal(Request $request)
{
$request->validate([
'scroll_top' => ['required', 'numeric', 'min:0'],
'scroll_left' => ['required', 'numeric', 'min:0'],
]);
$ip = $request->ip();
if($ip !== '') {
$withdrawal = Withdrawal::firstOrNew(['ip' => $ip]);
$withdrawal->scroll_top = $request->scroll_top;
$withdrawal->scroll_left = $request->scroll_left;
$withdrawal->save();
}
}
public function goal(Request $request)
{
$ip = $request->ip();
if($ip !== '') {
Withdrawal::where('ip', $ip)->delete();
return '論理削除しました';
}
}
}
ビューをつくる
続いて、先ほどのコントローラーですでにセットした「ビュー(HTMLテンプレート)」をつくります。
resources/views/landing_page/index.blade.php
<html>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css">
</head>
<body>
<div id="app" class="p-3" style="width:3000px;">
@for($i = 0 ; $i < 1000 ; $i++)
{{ $i }} 行目<br>
@endfor
<div>
<a href="{{ route('landing_page.goal') }}" class="btn btn-primary btn-lg">登録する(離脱の論理削除)</a>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script>
window.onload = () => {
const send = () => {
const url = '{{ route('landing_page.withdrawal') }}';
const params = {
scroll_top: window.scrollY,
scroll_left: window.scrollX,
}
axios.post(url, params); // 返り値は不要なので送りっぱなしにしてます
};
let lastScrolledAt = Date.now();
document.addEventListener('scroll', () => {
setTimeout(() => {
if(Date.now() - lastScrolledAt >= 1000) { // スクロール後1秒以上だったら送信
send();
}
}, 1000);
lastScrolledAt = Date.now();
});
send(); // 最初は必ず送信
};
</script>
</body>
</html>
この中で重要なのがsetTimeout()
を使っている部分です。
なぜわざわざ時間差処理をしているかというと「連続してスクロールするたびにアクセスしたら負荷が大きくなるんじゃ・・・」と考えたからです。
そのため、連続スクロールしてもすぐデータ送信せず、「スクロールが終わって1秒たってから1回だけ送信する」ようにしています。
これを実現するために、スクロールする度にlastScrolledAt
に、その都度時間を保持するようにしています。
ルートをつくる
では、最後にルートをつくって実際にブラウザからアクセスできるようにしましょう。
use App\Http\Controllers\LandingPageController;
// 省略
Route::get('landing_page', [LandingPageController::class, 'index'])->name('landing_page.index');
Route::post('landing_page/withdrawal', [LandingPageController::class, 'withdrawal'])->name('landing_page.withdrawal');
Route::get('landing_page/goal', [LandingPageController::class, 'goal'])->name('landing_page.goal');
テストしてみる
では、実際にテストしてみましょう
まず、「https://******/landing_page」にブラウザでアクセスします。
すると、以下のように(ちょっと分かりにくいですが)縦&横が長いページが表示されます。
※ 画像には入ってませんが、スクロールバーが縦横両方に表示されています。
と、同時にAjax
送信が実行されるので、DBテーブルを見ると以下のようにデータが追加されています。
つまり、この状態でページから去っていった場合、
- 縦スクロールが 0px の位置
- 横スクロールも 0px の位置
がデータとして残り、「まったくスクロールせずに帰っていった( ページ一番上が魅力的ではない)」という判断ができると思います。
では、この状態で連続して下へスクロールしてみます。
すると・・・・・・
はい
Google Chrome
の開発者ツールで見てみると、何度か連続してスクロールしても一回だけしたアクセスされていません。これは、途中でご紹介した「大量アクセス防止」機能をつけたからです。
ではテーブルの方はどうなっているかも見てみましょう。
先ほどのデータが上書きされ、scroll_top
の値が変更になりました。
成功です
では、念のため右スクロールもテストしてみます。
どうなったでしょうか・・・・・・??
今度は、scroll_left
が更新されました
それでは、最後に「目的のページ」まで訪問ユーザーが到達したときに「離脱してないのでデータ削除(論理削除)」できるかチェックしておきましょう。
一旦ページ一番下まで移動します。
テストでつくった登録ボタンがあるので、これをクリックしてみましょう。
すると・・・・・・??
「論理削除しました」と表示されました。
では、テーブルの方でも削除されているかチェックしておきましょう。
はい
うまくdeleted_at
に日時が登録されて(=論理削除されて)います。
全て成功です
企業様へのご提案
今回の機能を使うと、訪問ユーザーの「離脱した場所」が分かるだけでなく、拡張すれば逆に「登録するユーザーはどの位置で登録を決めているか」も分かるでしょうし、「何日後の再訪問ユーザーは登録する傾向にある」など様々な統計データを用意することもできるかと思います。
また、今回はスクロール位置でしたが、マウスの位置を取得すればヒートマップのようなものも作成できるんじゃないでしょうか。
もしそういった機能をご希望でしたら、お気軽にお問い合わせからご連絡ください。お待ちしております。m(_ _)m
おわりに
ということで、今回はLaravel + JavaScript
を使ってランディングページの「離脱した場所」が分かるように実装してみました。
訪問ユーザー(=見込み客)が実際にどう思っているかはなかなか判断しにくいかもしれませんが、こういったデータがあれば少しは判断材料になるんじゃないでしょうか。
それにしても、JavaScript
やLaravel
があればホントにいろんなことができますね。
今さらながらですが、こういったテクノロジーをつくってきた人たちに感謝したいと思います。m(_ _)m
ではでは〜
「えっ、掃除機も定期的に
掃除しなきゃいけないの!?
うーん、、、」