九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、最近のマーケティングでは欠かせない存在と言えば、やはり、
SNS(YouTube、Instagram、X など)
ですよね。
というのも、もしSNS
にフォロワーさんが多ければ、
良いことばかりだからです。
例えば、
- 広告費なしで集客できる
- ファン化しているので成約率が高い
- 同じ理由から、高単価のものが売れる
- 同じ理由から、他の商品も買ってもらえる
- 事前に理念を共有できるので、質のいい採用ができる
なので、企業さんもSNS
に力を入れているようです。
(ただし、属人化してしまうのが悩みだそうです…担当者がやめちゃうので😅)
そして、この流れを考えると私も本格的にSNS
の勉強をするべき
という結論になり、尊敬する社長さんの有料教材を購入してみました。
📝 参考ページ(PR): Twitterマーケティングマスター講座
やはり有料ということで、これまで全く想像していなかったアプローチ方法
を学ぶことができて、とても満足してます。😊✨
そして、その教材の中ではX
で良ポストを探す方法を勉強するのですが、
毎回検索コマンドを入力するのが面倒でした。
じゃあ、使いやすい検索フォームをパパッと作っちゃおう!
というのが今回の趣旨です。
※ なお、X
の検索コマンドは公開されている情報を元にしてつくってます。念のため!
今回はシンプルなので、ぜひLaravel
の勉強をしたい方も
真似して作ってもらったらスキルアップになると思います!
ぜひ何かの参考になりましたら嬉しいです。😊✨
「有料の情報って
想像してたのと全然
違ってたなぁ(感心)」
開発環境: Laravel + Inertia + Vue 3(Composition API
)
環境を整える
Laravel + Inertia + Vue 3
が使えるようにするために
一番手っ取り早いのがLaravel breeze
をインストールすることです。
Laravel breeze
はログイン周りのパッケージですが、今回の環境をすべて
満たしているので先にこれをインストールしておきましょう。
以下のコマンドを実行して下さい。
composer require laravel/breeze --dev
するとパッケージがインストールされるので、
続けて以下のコマンドを実行して下さい。
php artisan breeze:install
すると、どのモードでインストールするか聞かれるので、
今回はVue with Inertia
を選択。
続くオプションは何も選択せずエンターキーを押します。
テストもそのまま(そのうちPest
も勉強しなきゃ…😅)
これでインストールが完了しました。
あとは、以下のコマンドを実行すれば完了です。
php artisan migrate npm install npm run dev
ちなみに表示URLは.env
内のAPP_URL
が適用されます。
.env
APP_URL=(ここにあなたの環境のURL。localhostとかl11x.testとかですね)
JS パッケージをインストールする
今回以下2つのパッケージを使うので、先にインストールしておきましょう。
- dayjs: 日付を管理する
- Lodash: 便利機能が満載なパッケージ
以下のコマンドを実行して下さい。
npm i lodash --save-dev npm i dayjs --save-dev
すると、パッケージがインストールされますが
まだこの状態ではVue
から呼び出すことができません。
以下に登録をしておきましょう。
resources/js/bootstrap.js
// 省略 // dayjs import dayjs from "dayjs"; import 'dayjs/locale/ja'; window.dayjs = dayjs; dayjs.locale('ja'); // Lodash import _ from 'lodash'; window._ = _;
これで、Vue
側からもdayjs
と_
にアクセスができるようになります。
Vue ファイルをつくる
では、今回はシンプルな構造なのでコントローラーは作らず
いきなりVue
ファイルを作ります。
※ もし実際にサイトをつくる場合はコントローラーを作ったほうがいいです。
resources/js/Pages/UsefulForm.vue
<script setup> import {computed, onBeforeMount, onMounted, ref} from 'vue'; import { Head } from '@inertiajs/vue3'; // Form const keyword = ref(''); const params = ref({ from: '', min_faves: 100, min_retweets: 100, min_replies: 100, since: '', until: '' }); const toggle = ref({}); onBeforeMount(() => { let toggleValues = {}; for(let key in params.value) { toggleValues[key] = false; } toggle.value = toggleValues; }); // Inputs const onInputChange = (key) => { toggle.value[key] = params.value[key] !== ''; }; const onClearKeywordClick = () => { keyword.value = ''; document.getElementById('keyword-input').focus(); }; const setDate = (paramKey, type) => { let dt = dayjs(); if (type === 'year') { dt = dt.subtract(1, 'year'); } else if (type === 'month') { dt = dt.subtract(1, 'month'); } else if (type === 'week') { dt = dt.subtract(1, 'week'); } else if (type === 'yesterday') { dt = dt.subtract(1, 'day'); } params.value[paramKey] = dt.format('YYYY-MM-DD'); toggle.value[paramKey] = true; }; // Search const searchUrl = computed(() => { let searchParams = []; if (keyword.value) { searchParams.push(keyword.value); } for (const [key, value] of Object.entries(params.value)) { if (toggle.value[key] && value) { searchParams.push(`${key}:${value}`); } } let searchUrl = `https://twitter.com/search?q=${searchParams.join(' ')}`; if(currentIncludeItems.value.length > 0) { searchUrl += ' filter:'; for(let i = 0; i < currentIncludeItems.value.length; i++) { searchUrl += currentIncludeItems.value[i]; if(i < currentIncludeItems.value.length - 1) { searchUrl += ' '; } } } return searchUrl; }); const currentIncludeItems = ref([]); const includingItems = ref([ { value: 'images', label: '画像を含む' }, { value: 'videos', label: '動画を含む' }, { value: 'links', label: 'リンクを含む' }, ]); // DataList const dataList = ref({ keyword: [], from: [], }); const onSearchButtonClick = () => { setDataList('keyword', keyword.value); setDataList('from', params.value.from); localStorage.setItem('dataList', JSON.stringify(dataList.value)); }; const setDataList = (dataListKey, text) => { if(text.length > 0) { const targetDataList = dataList.value[dataListKey]; let hasDataListItem = false; for(let i = 0; i < targetDataList.length; i++) { let dataListItem = targetDataList[i]; if(dataListItem.text === text) { dataListItem.count++; hasDataListItem = true; break; } } if(! hasDataListItem) { targetDataList.push({ text: text, count: 1 }); } } }; const dataListKeywords = computed(() => { return _.sortBy(dataList.value.keyword, 'count').reverse(); // 検索が多い順に並び替え }); const dataListFroms = computed(() => { return _.sortBy(dataList.value.from, 'count').reverse(); // 検索が多い順に並び替え }); onMounted(() => { const storage = localStorage.getItem('dataList'); if (storage) { try { dataList.value = JSON.parse(storage); } catch (e) {} } }); </script> <template> <Head title="X ポスト / リサーチ" /> <div class="max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8 bg-gray-200"> <div class="text-center text-2xl mb-6 font-bold"> X ポスト / リサーチ </div> <table class="mx-auto table-auto"> <tbody> <tr> <td colspan="3" class="pb-1"> <input id="keyword-input" list="keyword" v-model="keyword" type="text" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="キーワード"> <datalist id="keyword"> <option v-for="keyword in dataListKeywords" :value="keyword.text"></option> </datalist> <div class="text-right px-2 py-1 text-sm"> <a href="#" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer" @click.prevent="onClearKeywordClick">クリア</a> </div> </td> </tr> <tr> <td class="w-10 align-top"> <input type="checkbox" v-model="toggle.from" id="toggleFrom" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> </td> <td class="w-40 align-top"> <label for="toggleFrom" class="text-sm font-medium text-gray-700 text-nowrap">アカウントID:</label> </td> <td class="w-64"> <input v-model="params.from" list="from" type="text" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" @input="onInputChange('from')"> <datalist id="from"> <option v-for="from in dataListFroms" :value="from.text"></option> </datalist> <div class="pt-0.5 p-1 text-gray-600"> <small>例:elonmusk</small> </div> </td> </tr> <tr> <td> <input type="checkbox" v-model="toggle.min_faves" id="toggleMinFavorites" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> </td> <td> <label for="toggleMinFavorites" class="text-sm font-medium text-gray-700 text-nowrap">最小いいね数:</label> </td> <td> <input v-model="params.min_faves" type="number" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="0" @input="onInputChange('min_faves')"> </td> </tr> <tr> <td> <input type="checkbox" v-model="toggle.min_retweets" id="toggleMinRetweets" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> </td> <td> <label for="toggleMinRetweets" class="text-sm font-medium text-gray-700 text-nowrap">最小リツイート数:</label> </td> <td> <input v-model="params.min_retweets" type="number" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="0" @input="onInputChange('min_retweets')"> </td> </tr> <tr> <td> <input type="checkbox" v-model="toggle.min_replies" id="toggleMinReplies" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> </td> <td> <label for="toggleMinReplies" class="text-sm font-medium text-gray-700 text-nowrap">最小リプライ数:</label> </td> <td> <input v-model="params.min_replies" type="number" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="0" @input="onInputChange('min_replies')"> </td> </tr> <tr> <td class="align-top"> <input type="checkbox" v-model="toggle.since" id="toggleSince" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> </td> <td class="align-top"> <label for="toggleSince" class="text-sm font-medium text-gray-700 text-nowrap">開始日:</label> </td> <td> <input v-model="params.since" :max="params.until" type="date" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" @input="onInputChange('since')"> </td> </tr> <tr> <td colspan="3"> <div class="text-right p-1 text-gray-400"> <a href="#" @click="setDate('since', 'year')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1年前</a> / <a href="#" @click="setDate('since', 'month')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1ヶ月前</a> / <a href="#" @click="setDate('since', 'week')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1週間前</a> / <a href="#" @click="setDate('since', 'yesterday')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">昨日</a> / <a href="#" @click="setDate('since', 'today')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">今日</a> </div> </td> </tr> <tr> <td class="align-top"> <input type="checkbox" v-model="toggle.until" id="toggleUntil" class="w-6 h-6 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> </td> <td class="align-top"> <label for="toggleUntil" class="text-sm font-medium text-gray-700 text-nowrap">終了日:</label> </td> <td> <input v-model="params.until" :min="params.since" type="date" class="w-full border border-gray-300 px-3 py-2 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" @input="onInputChange('until')"> </td> </tr> <tr> <td colspan="3"> <div class="text-right p-1 text-gray-400"> <a href="#" @click="setDate('until', 'year')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1年前</a> / <a href="#" @click="setDate('until', 'month')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1ヶ月前</a> / <a href="#" @click="setDate('until', 'week')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">1週間前</a> / <a href="#" @click="setDate('until', 'yesterday')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">昨日</a> / <a href="#" @click="setDate('until', 'today')" class="text-indigo-600 hover:text-indigo-800 text-sm cursor-pointer">今日</a> </div> </td> </tr> <tr> <td colspan="3" class="pt-4"> <div class="text-sm font-medium text-gray-700 mb-2">含むアイテム:</div> <div class="flex flex-wrap"> <div class="mr-2" v-for="item in includingItems"> <label class="text-sm font-medium text-gray-700 ml-1 flex items-center"> <input type="checkbox" v-model="currentIncludeItems" :value="item.value" class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500 mr-1"> {{ item.label }} </label> </div> </div> </td> </tr> <tr> <td colspan="3" class="pt-3 text-right"> <a :href="searchUrl" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" target="_blank" @click="onSearchButtonClick"> 検索 </a> </td> </tr> </tbody> </table> </div> </template>
ちなみに、今回実装している内容は次のとおりです。
- 検索したキーワードが LocalStorage に保存されて、次回から入力補完ができる
- 検索回数が多い順に自動で並べ替える
- アカウントIDも同じ形で入力補完できる
- チェックボックスで検索したい内容を選択できる
- 日付は選択が面倒なのでワンクリックで完了できるようにする
- その他オプションも選択可能にする
ちなみに、その他のコマンドはこちらがわかりやすいです。
📝 参考ページ: 【Twitterユーザー必見】便利な検索コマンド25選!いいね数や特定ユーザーで検索
そして、LocalStorage
は、以下の2パターンのみです。
- ページ読み込み時に取得
- 検索ボタンを押したら保存
ルートをつくる
今回ルートはとてもシンプルになっています。
routes/web.php
<?php use Illuminate\Support\Facades\Route; use Inertia\Inertia; Route::get('x_useful_form', fn() => Inertia::render('UsefulForm'));
※ なお、Laravel Breeze
をインストールするといくつかルートが追加されていますが、今回は不要なので削除しています。
さぁ、これで作業は完了です。
お疲れ様でした。😊
テストしてみる
では、実際のテストしてみましょう❗
ブラウザで「https://******/x_useful_form」へアクセスします。
すると・・・・・・
はい❗
検索用のフォームが表示されました。
では、キーワードに「カレーうどん」と入力し、
最小いいね数にチェックを入れて「検索」ボタンをクリックしてみます。
どうなるでしょうか・・・・・・
はい!
ページ移動した先の検索ワードが想定通りになっていました。
では、次に入力補完のチェックです。
「豚骨ラーメン」で2回チェックして一番上に来ているか
確認しておきましょう。
うまくいくでしょうか・・・・・・
はい❗
豚骨ラーメンは2回検索されたので、一番上に表示されています。
すべて成功です。😊✨
実際に公開しました
今回のコードを実際に体験できるように
ページを公開しました。
ぜひ以下から試してみて下さい。
📝 公開ページ: https://social-digger.capilano-fw.com/
企業様へのご提案
昨今、X
を始めとするSNS
の重要性は
より大きくなってきています。
しかし、それらの運用はなかなか時間がかかってしまします。
そのため、今回のように運用を支援するツールを
ご用意になってみてはいかがでしょうか。
貴社やその業界にカスタマイズした専用リサーチツールを
つくっておくと集客の効率化に役に立つことが期待できます。
ぜひそういったツールをご希望でしたら、
いつでもお気軽にご相談下さい。
お待ちしております。😊✨
おわりに
ということで、今回はSNS
マーケティングを支援するツールとして
X
の便利な検索フォームをつくってみました。
常に、優秀なものに触れるのはスキルアップにつながると思いますので、
ぜひ皆さんも今回のフォームを活用していただけると嬉しいです。
また、ご自身の業界や状況によって必要な項目も出てくるかと思うので、
その場合は拡張して入力項目を増やしてみてはいかがでしょうか。
私もまだまだ勉強します!
ではでは〜❗
「ついでに、
(PR) ビジネスモデルマスター講座
も買っちゃった 😊👍」