フロントエンドはHTMLとJavaScript、バックエンドはGoogleスプレッドシートとGoogle Apps Script(以下GAS)を使用しました。
ボタンをクリックすると、記事のタイトルとURLがGAS経由でスプレッドシートに書き込まれます。 書き込まれたURLは集計され、webサイトを開いたときにその集計結果が表示されます。
Googleのシステムを利用するため、Googleアカウントが必要です。
以降、Googleスプレッドシート、GAS、HTML、JavaScriptの順で説明します。
Googleスプレッドシート
Googleアカウントにログインした状態のブラウザで、Googleスプレッドシートを開きます。
シート名は初期値の「シート1」、「シート2」にしました。

シート名を変更した場合は、「シート2」に入力する関数のシート名も変更してください。
シート1
シート1は送られたデータが書き込まれます。
A1、B1、C1セルは見出しを入力します。
- A1セル:DATE
- B1セル:TITLE
- C1セル:URL
シート2
シート2でシート1のデータを集計します。
A1、B1セルは見出しにしました。
- A1セル:URL
- B1セル:COUNT
次に、A2、B2セルに関数を入力します。
A2セル:
=UNIQUE('シート1'!C2:C)
式の内容:A列のA2セル以降に、シート1のC列にあるURLを重複なしで集計します。
B2セル:(2025.2.13:変更)
=ARRAYFORMULA(IF(A2:A="",,IFERROR(VLOOKUP(A2:A,QUERY(ARRAYFORMULA(LOWER('シート1'!C2:C)),"select Col1, count(Col1) group by Col1 label count(Col1) 'Count'",0),2,FALSE),0)))
式の内容:B列のB2セル以降に、シート1のC列にある各URLの出現回数をカウントし、シート2のA列にある対応するURLの回数を表示します。
URLは大文字小文字を区別しません。以下は同じURLとして扱われます。(2025.2.13:変更)
https://example.com/apple
https://example.com/APPLE
データ量がそれほど多くない場合は、COUNTIFS関数とFILTER関数を組み合わせる方法も有効です。
=ARRAYFORMULA(IF(A2:A="",,COUNTIFS(ARRAYFORMULA(LOWER(FILTER('シート1'!C2:C,'シート1'!C2:C<>""))),LOWER(A2:A))))
大文字小文字を区別する場合:(2025.2.13:追加)
処理速度が遅くなりますが
=ARRAYFORMULA(IF(A2:A="",,MAP(A2:A,LAMBDA(検索値, SUMPRODUCT(EXACT(FILTER('シート1'!C2:C,'シート1'!C2:C<>""),検索値))))))
C2:C1000
など、シート1のC列の範囲を狭くすれば改善すると思います。
Google Apps Script(GAS)
GASは「デプロイ」という作業をします。はじめての方は、こちらのページを一読するとおおまかな手順が理解できます。😀
簡便なURLチェック機能を設けました。
「いいね」ボタンを設置したwebサイトのURLをYour_website_url
のところに入れてください。
例:https://example.com/
の場合は、example.com
と入力。
(最後のスラッシュ/
もいりません。)
function checkDomain(url, allowedHostname = "Your_website_url")
↓
function checkDomain(url, allowedHostname = "example.com")
Google Apps Script:(2025.2.13:変更)
/**
* webサイトから送られた記事のタイトル、URL、およびプログラムで取得した日時をスプレッドシートに書き込みます。
* 書き込まれたURLは集計され、webサイトを開いたときに集計された数が送信されます。
*
*/
/*! Copyright:2025 sutajp | Released under the MIT license | https://sutasutashiki.blogspot.com/p/mit-license.html */
function createJsonResponse(object) {
return ContentService.createTextOutput(JSON.stringify(object)).setMimeType(
ContentService.MimeType.JSON
);
}
function checkDomain(url, allowedHostname = "Your_website_url") {
try {
const match = url.match(/^https?:\/\/([^/]+)/);
if (match) {
const hostname = match[1];
if (hostname === allowedHostname) {
return true;
} else {
Logger.log(`Invalid domain: ${url} (Hostname: ${hostname})`);
return false;
}
} else {
Logger.log(`Invalid URL format: ${url}`);
return false;
}
} catch (error) {
Logger.log("URL処理エラー: " + error);
return false;
}
}
function doGet(e) {
try {
const url = e.parameter?.currentURL;
if (!url) {
return createJsonResponse({
status: "error",
message: "URLパラメータが不足しています。",
});
}
if (!checkDomain(url)) {
return createJsonResponse({
status: "Invalid domain.",
message: "不正なドメインです。",
});
}
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[1];
const lastRow = sheet.getLastRow();
let count = 0;
if (lastRow >= 2) {
const data = sheet.getRange(2, 1, lastRow - 1, 2).getValues();
const urlMap = new Map(data.map((row) => [row[0], row[1]]));
count = urlMap.get(url) || 0;
}
return createJsonResponse(count);
} catch (error) {
Logger.log("doGetエラー: " + error); // doGetのエラーログを明確化
return createJsonResponse({
status: "error",
message: "doGet処理でエラーが発生しました。",
}); // より一般的なエラーメッセージ
}
}
function doPost(e) {
try {
const title = e.parameter?.sendTitle;
const url = e.parameter?.sendURL;
if (!title || !url) {
// titleまたはurlがない場合のエラー処理を追加
return createJsonResponse({
status: "error",
message: "TitleまたはURLパラメータが不足しています。",
});
}
if (!checkDomain(url)) {
return createJsonResponse({
status: "Invalid domain.",
message: "不正なドメインです。",
});
}
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
const lastRow = sheet.getLastRow();
const nextRow = lastRow + 1;
const formattedDate = Utilities.formatDate(
new Date(),
Session.getScriptTimeZone(),
"yyyy/MM/dd HH:mm:ss"
);
sheet.getRange(nextRow, 1, 1, 3).setValues([[formattedDate, title, url]]);
return createJsonResponse({
status: "success",
message: "いいね!を送信しました。",
});
} catch (error) {
Logger.log("doPostエラー: " + error); // doPostのエラーログを明確化
return createJsonResponse({
status: "error",
message: "doPost処理でエラーが発生しました。",
}); // より一般的なエラーメッセージ
}
}
HTML
ボタンを表示する要素と、「いいね」の数を表示する要素です。
id="upVoteButton"
とid="upVoteCounter"
がJavaScriptに紐づいています。
id名を変更する場合は、JavaScriptの修正もお願いします。
HTMLの例です、好みの場所に設置してください。
<button id="upVoteButton" class="upvote-button" title="この記事にいいね" aria-label="この記事にいいね" aria-pressed="false" type="button">いいね</button>
<div id="upVoteCounter" class="upvote-counter" aria-live="polite"></div>
JavaScript
gas_WebApps_URL
のところにデプロイしたウェブアプリのURLをコピペしてください。
scriptURL = "gas_WebApps_URL",
↓
scriptURL = "https://script.google.com/.../exec",
BloggerなどHTML(XML)ファイルに直接書くときはJavaScriptのコードを<script>
タグではさんでください
<script>
JavaScriptのコード
</script>
また、Bloggerは<b:if cond='data:view.isSingleItem'>
ではさむと記事(投稿)のみで実行します。
<b:if cond='data:view.isSingleItem'>
<script>
JavaScriptのコード
</script>
</b:if>
設置場所は</body>
の上付近を想定しています。
コードの一番最後setupUpVote();
でJavaScriptを実行しています。
bodyタグの上の方に設置する場合はsetupUpVote();
を下のコードに書き換えることを検討してください。
document.addEventListener("DOMContentLoaded", function () {
setupUpVote();
});
JavaScript:
/**
* ボタンがクリックされたとき、記事のタイトルとURLをGoogle Apps Script(GAS)経由でスプレッドシートに書き込みます。
* 書き込まれたURLは集計され、webサイトを開いたときにその集計された数が表示されます。
*
* @param {Object} options - オプションオブジェクト
* @param {string} options.buttonId - ボタン要素のID
* @param {string} options.counterId - いいねの数を表示する要素のID
* @param {string} options.scriptURL - GASスクリプトのURL
*/
/*! Copyright:2025 sutajp | Released under the MIT license | https://sutasutashiki.blogspot.com/p/mit-license.html */
function setupUpVote(options) {
"use strict";
const {
buttonId = "upVoteButton",
counterId = "upVoteCounter",
scriptURL = "gas_WebApps_URL",
} = options || {};
const currentURL = window.location.origin + window.location.pathname;
const upVoteButton = document.getElementById(buttonId);
const upVoteCounter = document.getElementById(counterId);
async function getUpVoteCount() {
const param = { currentURL };
const query = new URLSearchParams(param);
try {
const response = await fetch(`${scriptURL}?${query}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const count = await response.json();
upVoteCounter.textContent = count;
} catch (error) {
console.error("Fetch Error:", error);
upVoteCounter.textContentL = "通信エラーが発生しました。";
}
}
async function sendTitleAndURL() {
if (upVoteButton.disabled) return;
upVoteButton.disabled = true;
upVoteButton.ariaPressed = true;
let count = parseInt(upVoteCounter.textContent, 10);
upVoteCounter.textContent = count + 1; // 楽観的更新
const params = new URLSearchParams({
sendTitle: document.title,
sendURL: currentURL,
});
try {
const response = await fetch(scriptURL, {
method: "POST",
headers: {
"Accept": "application/json", //JSON形式のレスポンスを明示
"Content-Type": "application/x-www-form-urlencoded",
},
body: params,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
console.log("いいね送信成功");
} catch (error) {
console.error("Error sending like:", error);
upVoteCounter.textContent = count;
//alert("いいね!の送信に失敗しました。");
}
}
getUpVoteCount();
upVoteButton.addEventListener("click", sendTitleAndURL);
}
// 初期化処理
setupUpVote();
setupUpVote()
関数にオプション引数(options
)を追加し、ボタンのID、カウンターのID、スクリプトURLを外部から設定できるようにしました。
これにより、複数のいいねボタンを設置する場合などに、コードを再利用しやすくなります。
ボタンIDやカウンターID、スクリプトURLを変更する場合は、以下のようにオプションオブジェクトを渡します。
setupUpVote({
buttonId: "myLikeButton",
counterId: "myLikeCounter",
scriptURL: "https://script.google.com/.../exec"
});
コメントなし: