見出し画像

【ITエンジニア編】Noteの「スキ」通知メールを自動集計!自分だけの分析ダッシュボード

noteを書いていると、メールで届く「〇〇さんにスキされました!」の通知。

記事が読者に届いた実感が湧いて、すごく嬉しい瞬間ですよね。

でも、こんな風に思ったことはありませんか?

  • 「過去にどの記事が一番スキされたのか、パッと比較したい」

  • 「毎月のスキ数の推移や、よく読まれる時間帯を知りたい」

  • 「何度もスキしてくれる”リピーターの皆様”を大切にしたい」

実は、普段届いているGmailの通知メールを活用すれば、誰でも簡単に「自分だけのスキ分析ダッシュボード」が作れます!Gmail限定でごめんなさい。

今回は、GoogleスプレッドシートとGoogle Apps Script(GAS)を使って、届いた通知を自動で集計し、グラフで分析できるツールを完全無料公開します。コピー&ペーストだけで完成するので、基本的にプログラミングの知識は不要です。プログラミングをしたことのない方もぜひチャレンジ試してみてください!

ダッシュボードのイメージ図

動作を完全に保証することはできません。また、読者の皆様が使用するためのフォローアップも基本的にいたしかねますので、ご理解いただければ幸いです。

🔒 安心してお使いいただくために(セキュリティについて)

プログラムを導入する前に、安全面について3つのポイントをお伝えします。

  1. 個人情報の外部送信は一切ありません

    1. 本ツールのプログラムは、あなたのGoogleアカウント内部(Gmailとスプレッドシートの間)だけで動き、外部のサーバー等にデータが送信されることは絶対にありません。

  2. ウェブ公開設定は「自分のみ」で安全管理

    1. ダッシュボード画面をウェブアプリとして開く場合、アクセス権限を「自分のみ」に設定すれば、他の人に分析データが見えることはありません。

  3. SNSへのスクショ投稿時はお気遣いを

    1. ダッシュボードの分析画面には「スキしてくれた方のユーザー名」が表示されます。SNSなどに画面のスクショを投稿する際は、ユーザー名にモザイクをかけるなど、読者さんへのご配慮をお願いします。

🛠 導入手順まとめ(所要時間:約10分)

手順は4ステップです!

  1. Googleスプレッドシートを新規作成する

  2. 3つのコード(初期化用・毎日自動更新用・画面デザイン用)をコピペする

  3. アクセス承認を行い、過去の通知を全件取得する

  4. 「毎日の自動更新タイマー」をワンクリックで設定する

ステップ1:スプレッドシートを準備する

  1. 新しいGoogleスプレッドシートを新規作成します。

  2. 画面下部のシート名を「シート1」から スキ分析 に変更します(※漢字や大文字小文字も含め、正確に入力してください)。

  3. 画面上のメニューから 「拡張機能」>「Apps Script」 をクリックします。これでプログラムの編集画面(スクリプトエディタ)が開きます。

ステップ2:プログラムを3つコピペする

スクリプトエディタが開いたら、用途に合わせて3つのファイルを作成してコードを貼り付けます。

① 過去のスキ通知を全件一括で読み込むコード (コード.gs)

元からある コード.gs の中身を空にして、以下のコードをそのまま貼り付けます。

// ====== 【コード.gs】Note スキ通知メール → スプレッドシート全件取得スクリプト ======

const BATCH_SIZE = 100;
const MAX_EXECUTION_TIME_MS = 4.5 * 60 * 1000;

function importNoteSkiData() {
  const startTime = new Date().getTime();
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('スキ分析');
  const props = PropertiesService.getScriptProperties();

  let start = parseInt(props.getProperty('NOTE_SKI_START_INDEX') || '0', 10);
  let totalProcessed = parseInt(props.getProperty('NOTE_SKI_PROCESSED_COUNT') || '0', 10);

  if (start === 0) {
    sheet.clearContents();
    sheet.appendRow(['受信日時', 'スキしてくれた人', '記事タイトル', '記事URL', '元の件名']);
    sheet.getRange(1, 1, 1, 5).setFontWeight('bold');
    SpreadsheetApp.flush();
  }

  Logger.log(`メール検索を開始します... (開始インデックス: ${start})`);

  let currentStart = start;
  const currentRunResults = [];

  while (true) {
    if (new Date().getTime() - startTime > MAX_EXECUTION_TIME_MS) {
      if (currentRunResults.length > 0) {
        const lastRow = sheet.getLastRow();
        sheet.getRange(lastRow + 1, 1, currentRunResults.length, 5).setValues(currentRunResults);
        SpreadsheetApp.flush();
      }

      const newTotal = totalProcessed + currentRunResults.length;
      props.setProperty('NOTE_SKI_START_INDEX', currentStart.toString());
      props.setProperty('NOTE_SKI_PROCESSED_COUNT', newTotal.toString());

      createTrigger();
      ss.toast(`処理時間が上限に達したため一時中断します。自動で続きを実行します。(現在 ${newTotal} 件処理済み)`, '自動継続中', 10);
      return;
    }

    const threads = GmailApp.search('from:noreply@note.com subject:スキされました', currentStart, BATCH_SIZE);
    if (threads.length === 0) break;

    const messages2D = GmailApp.getMessagesForThreads(threads);

    for (const messages of messages2D) {
      for (const message of messages) {
        const date = message.getDate();
        const subject = message.getSubject();
        const body = message.getPlainBody();

        const parsed = parseSkiEmail(subject, body, message);
        if (parsed) {
          currentRunResults.push([
            Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss'),
            parsed.userName,
            parsed.articleTitle,
            parsed.articleUrl,
            subject
          ]);
        }
      }
    }

    currentStart += threads.length;
    if (threads.length < BATCH_SIZE) break;
  }

  if (currentRunResults.length > 0) {
    const lastRow = sheet.getLastRow();
    sheet.getRange(lastRow + 1, 1, currentRunResults.length, 5).setValues(currentRunResults);
    SpreadsheetApp.flush();
  }

  const finalTotal = totalProcessed + currentRunResults.length;

  deleteTriggers();
  props.deleteProperty('NOTE_SKI_START_INDEX');
  props.deleteProperty('NOTE_SKI_PROCESSED_COUNT');

  const lastRow = sheet.getLastRow();
  if (lastRow > 1) {
    sheet.getRange(2, 1, lastRow - 1, 5).sort({ column: 1, ascending: true });
    sheet.autoResizeColumns(1, 5);
    ss.toast(`全件完了! ${finalTotal} 件のスキデータを取得しました。`, '完了', 10);
  } else {
    ss.toast('データが見つかりませんでした。', '注意', 5);
  }
}

function parseSkiEmail(subject, body, message) {
  const subjectMatch = subject.match(/^(.+?)さんにスキされました/);
  if (!subjectMatch) return null;

  const userName = subjectMatch[1].trim();

  let articleTitle = '';
  const titleMatch = body.match(/作品が読者に届いています!\s+([^\r\n]+)/);
  if (titleMatch) {
    articleTitle = titleMatch[1].trim();
  } else {
    const fallback = body.match(/スキしました!\s+([^\r\n]+)/);
    if (fallback) {
      let temp = fallback[1].trim();
      if (temp !== '作品が読者に届いています!') {
        articleTitle = temp;
      }
    }
  }

  let articleUrl = '';
  try {
    const htmlBody = message.getBody();
    const urlMatch = htmlBody.match(/href="(https:\/\/note\.com\/[^/]+\/n\/[^"]+)"/);
    if (urlMatch) articleUrl = urlMatch[1];
  } catch(e) {}

  return { userName, articleTitle, articleUrl };
}

function createTrigger() {
  deleteTriggers();
  ScriptApp.newTrigger('importNoteSkiData').timeBased().after(5000).create();
}

function deleteTriggers() {
  const triggers = ScriptApp.getProjectTriggers();
  for (const trigger of triggers) {
    if (trigger.getHandlerFunction() === 'importNoteSkiData') {
      ScriptApp.deleteTrigger(trigger);
    }
  }
}

function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('Note スキ分析ダッシュボード')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function getSkiDataForDashboard() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('スキ分析');
  const data = sheet.getDataRange().getValues();

  if (data.length <= 1) return [];

  const rows = data.slice(1);
  return rows.map(row => {
    let dateStr = row[0] instanceof Date ? Utilities.formatDate(row[0], 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss') : String(row[0]);
    return {
      date: dateStr,
      user: String(row[1] || ''),
      title: String(row[2] || ''),
      url: String(row[3] || '')
    };
  });
}

function openDashboardDialog() {
  const html = HtmlService.createHtmlOutputFromFile('Index').setWidth(1200).setHeight(800);
  SpreadsheetApp.getUi().showModalDialog(html, 'Note スキ分析ダッシュボード');
}

② 毎日「新しい通知だけ」を追加するコード (dailyget.gs)

スクリプトエディタ左側の 「ファイル」の右にある「+」マーク >「スクリプト」 を押して、新しいファイル名を dailyget とします。そこに以下のコードを貼り付けてください。

// ====== 【dailyget.gs】Note スキ通知メール 毎日の差分(最新のみ)追加スクリプト ======

function importDailyNoteSkiData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('スキ分析');
  
  if (sheet.getLastRow() === 0) {
    sheet.appendRow(['受信日時', 'スキしてくれた人', '記事タイトル', '記事URL', '元の件名']);
    sheet.getRange(1, 1, 1, 5).setFontWeight('bold');
  }

  const lastRow = sheet.getLastRow();
  let latestDate = new Date(0);
  let afterDateQuery = '';
  const existingKeys = new Set();

  if (lastRow > 1) {
    const existingData = sheet.getRange(2, 1, lastRow - 1, 3).getValues();
    
    existingData.forEach(row => {
      let dateObj = row[0] instanceof Date ? row[0] : new Date(row[0]);
      if (dateObj > latestDate) latestDate = dateObj;
      const dateStr = Utilities.formatDate(dateObj, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
      existingKeys.add(`${dateStr}_${row[1]}_${row[2]}`);
    });

    const queryDate = new Date(latestDate.getTime() - 24 * 60 * 60 * 1000);
    afterDateQuery = ` after:${Utilities.formatDate(queryDate, 'Asia/Tokyo', 'yyyy/MM/dd')}`;
  }

  const searchQuery = `from:noreply@note.com subject:スキされました${afterDateQuery}`;
  const threads = GmailApp.search(searchQuery);
  if (threads.length === 0) return;

  const messages2D = GmailApp.getMessagesForThreads(threads);
  const newResults = [];

  for (const messages of messages2D) {
    for (const message of messages) {
      const msgDate = message.getDate();
      if (msgDate < latestDate) continue;

      const dateStr = Utilities.formatDate(msgDate, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
      const subject = message.getSubject();
      const body = message.getPlainBody();

      const parsed = parseSkiEmail(subject, body, message);
      if (!parsed) continue;

      const key = `${dateStr}_${parsed.userName}_${parsed.articleTitle}`;

      if (!existingKeys.has(key)) {
        newResults.push([
          dateStr,
          parsed.userName,
          parsed.articleTitle,
          parsed.articleUrl,
          subject
        ]);
        existingKeys.add(key);
      }
    }
  }

  if (newResults.length > 0) {
    newResults.sort((a, b) => new Date(a[0]) - new Date(b[0]));
    const appendStartRow = sheet.getLastRow() + 1;
    sheet.getRange(appendStartRow, 1, newResults.length, 5).setValues(newResults);
    sheet.getRange(2, 1, sheet.getLastRow() - 1, 5).sort({ column: 1, ascending: true });
    sheet.autoResizeColumns(1, 5);
  }
}

function setupDailyTrigger() {
  removeDailyTrigger();
  ScriptApp.newTrigger('importDailyNoteSkiData')
    .timeBased()
    .everyDays(1)
    .atHour(1)
    .create();
  Logger.log('毎日 午前1時〜2時 に自動で差分追加するトリガーを設定しました。');
}

function removeDailyTrigger() {
  const triggers = ScriptApp.getProjectTriggers();
  for (const trigger of triggers) {
    if (trigger.getHandlerFunction() === 'importDailyNoteSkiData') {
      ScriptApp.deleteTrigger(trigger);
    }
  }
}

③ ダッシュボードの画面を作るHTML (Index.html)

再度左側の 「+」マーク >「HTML」 をクリックし、ファイル名を Index にしてください。元からあるテキストを全て消し、以下のコードを貼り付けます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Note スキ分析ダッシュボード</title>
  <script src="/https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    :root {
      --primary: #2cb696;
      --bg-color: #f4f6f8;
      --card-bg: #ffffff;
      --text-main: #333333;
      --text-sub: #666666;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif;
      background-color: var(--bg-color);
      color: var(--text-main);
      padding: 20px;
    }
    .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 15px;
    }
    .header h1 { font-size: 24px; color: var(--text-main); }
    .btn-refresh {
      background-color: var(--primary);
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 6px;
      cursor: pointer;
      font-weight: bold;
    }
    .btn-refresh:hover { opacity: 0.9; }
    .filter-section {
      display: flex;
      align-items: center;
      gap: 10px;
      background: var(--card-bg);
      padding: 15px 20px;
      border-radius: 10px;
      margin-bottom: 20px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.05);
    }
    .filter-section label { font-weight: bold; font-size: 14px; }
    .filter-section input[type="date"] {
      padding: 6px;
      border: 1px solid #ccc;
      border-radius: 4px;
      font-family: inherit;
    }
    .btn-filter, .btn-clear {
      color: white;
      border: none;
      padding: 6px 16px;
      border-radius: 4px;
      cursor: pointer;
      font-weight: bold;
    }
    .btn-filter { background-color: var(--text-main); }
    .btn-clear { background-color: #bdc3c7; }
    .btn-filter:hover, .btn-clear:hover { opacity: 0.8; }
    .kpi-container {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 15px;
      margin-bottom: 20px;
    }
    .kpi-card {
      background: var(--card-bg);
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.05);
    }
    .kpi-card .title { font-size: 13px; color: var(--text-sub); margin-bottom: 8px; }
    .kpi-card .value { font-size: 28px; font-weight: bold; color: var(--primary); }
    .charts-container {
      display: grid;
      grid-template-columns: 1fr;
      gap: 20px;
    }
    .chart-card {
      background: var(--card-bg);
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.05);
    }
    .chart-card h2 { font-size: 16px; margin-bottom: 15px; border-left: 4px solid var(--primary); padding-left: 8px; }
    .chart-box { position: relative; height: 300px; }
    #loading {
      position: fixed;
      top: 0; left: 0; width: 100%; height: 100%;
      background: rgba(255,255,255,0.8);
      display: flex;
      justify-content: center;
      align-items: center;
      font-weight: bold;
      font-size: 18px;
      z-index: 1000;
    }
  </style>
</head>
<body>

  <div id="loading">データを集計・読み込み中...</div>

  <div class="header">
    <h1>💚 Note スキ分析ダッシュボード</h1>
    <button class="btn-refresh" onclick="loadData()">最新データを取得</button>
  </div>

  <div class="filter-section">
    <label for="startDate">期間絞り込み:</label>
    <input type="date" id="startDate">
    <span>〜</span>
    <input type="date" id="endDate">
    <button class="btn-filter" onclick="applyFilter()">絞り込む</button>
    <button class="btn-clear" onclick="clearFilter()">解除</button>
  </div>

  <div class="kpi-container">
    <div class="kpi-card">
      <div class="title">総スキ数</div>
      <div class="value" id="kpi-total-likes">-</div>
    </div>
    <div class="kpi-card">
      <div class="title">ユニークユーザー数</div>
      <div class="value" id="kpi-unique-users">-</div>
    </div>
    <div class="kpi-card">
      <div class="title">対象記事数</div>
      <div class="value" id="kpi-unique-articles">-</div>
    </div>
    <div class="kpi-card">
      <div class="title">ファンリピート率</div>
      <div class="value" id="kpi-repeat-rate">-</div>
    </div>
  </div>

  <div class="charts-container">
    <div class="chart-card">
      <h2>月別スキ数の推移</h2>
      <div class="chart-box"><canvas id="chartTrend"></canvas></div>
    </div>
    <div class="chart-card">
      <h2>時間帯別スキ獲得傾向(時)</h2>
      <div class="chart-box"><canvas id="chartHourly"></canvas></div>
    </div>
    <div class="chart-card">
      <h2>人気記事 TOP 10</h2>
      <div class="chart-box"><canvas id="chartArticles"></canvas></div>
    </div>
    <div class="chart-card">
      <h2>TOP 10 (リピーター)</h2>
      <div class="chart-box"><canvas id="chartUsers"></canvas></div>
    </div>
  </div>

  <script>
    let charts = {};
    let allData = []; 

    window.onload = function() { loadData(); };

    function loadData() {
      document.getElementById('loading').style.display = 'flex';
      google.script.run
        .withSuccessHandler(data => {
          if (!data || data.length === 0) {
            alert('データが存在しません。');
            document.getElementById('loading').style.display = 'none';
            return;
          }
          allData = data; 
          applyFilter();  
        })
        .withFailureHandler(err => {
          alert('データ読み込みエラー: ' + err);
          document.getElementById('loading').style.display = 'none';
        })
        .getSkiDataForDashboard();
    }

    function applyFilter() {
      const startDate = document.getElementById('startDate').value;
      const endDate = document.getElementById('endDate').value;
      let filteredData = allData;

      if (startDate || endDate) {
        filteredData = allData.filter(item => {
          const d = new Date(item.date);
          if (isNaN(d.getTime())) return false;
          const y = d.getFullYear();
          const m = String(d.getMonth() + 1).padStart(2, '0');
          const day = String(d.getDate()).padStart(2, '0');
          const itemDateStr = `${y}-${m}-${day}`;
          if (startDate && itemDateStr < startDate) return false;
          if (endDate && itemDateStr > endDate) return false;
          return true;
        });
      }
      renderDashboard(filteredData);
    }

    function clearFilter() {
      document.getElementById('startDate').value = '';
      document.getElementById('endDate').value = '';
      applyFilter();
    }

    function renderDashboard(data) {
      const chartIds = ['chartTrend', 'chartHourly', 'chartArticles', 'chartUsers'];
      chartIds.forEach(id => { if (charts[id]) charts[id].destroy(); });

      if (data.length === 0) {
        document.getElementById('kpi-total-likes').innerText = '0';
        document.getElementById('kpi-unique-users').innerText = '0';
        document.getElementById('kpi-unique-articles').innerText = '0';
        document.getElementById('kpi-repeat-rate').innerText = '0%';
        document.getElementById('loading').style.display = 'none';
        return;
      }

      const totalLikes = data.length;
      const usersMap = {};
      const articlesMap = {};
      const articleFirstDateMap = {}; 
      const monthlyMap = {};
      const hourlyMap = Array(24).fill(0);

      data.forEach(item => {
        usersMap[item.user] = (usersMap[item.user] || 0) + 1;
        const titleKey = item.title || item.url || 'タイトル不明';
        articlesMap[titleKey] = (articlesMap[titleKey] || 0) + 1;

        const d = new Date(item.date);
        if (!isNaN(d.getTime())) {
          if (!articleFirstDateMap[titleKey] || d < articleFirstDateMap[titleKey]) {
            articleFirstDateMap[titleKey] = d;
          }
          const monthKey = `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}`;
          monthlyMap[monthKey] = (monthlyMap[monthKey] || 0) + 1;
          const hour = d.getHours();
          hourlyMap[hour]++;
        }
      });

      const uniqueUsers = Object.keys(usersMap).length;
      const uniqueArticles = Object.keys(articlesMap).length;
      const repeaters = Object.values(usersMap).filter(count => count > 1).length;
      const repeatRate = uniqueUsers > 0 ? ((repeaters / uniqueUsers) * 100).toFixed(1) + '%' : '0%';

      document.getElementById('kpi-total-likes').innerText = totalLikes.toLocaleString();
      document.getElementById('kpi-unique-users').innerText = uniqueUsers.toLocaleString();
      document.getElementById('kpi-unique-articles').innerText = uniqueArticles.toLocaleString();
      document.getElementById('kpi-repeat-rate').innerText = repeatRate;
      
      const sortedMonths = Object.keys(monthlyMap).sort();
      createChart('chartTrend', 'line', {
        labels: sortedMonths,
        datasets: [{
          label: 'スキ数',
          data: sortedMonths.map(m => monthlyMap[m]),
          borderColor: '#2cb696',
          backgroundColor: 'rgba(44, 182, 150, 0.1)',
          fill: true,
          tension: 0.3
        }]
      });

      createChart('chartHourly', 'bar', {
        labels: Array.from({length: 24}, (_, i) => `${i}時`),
        datasets: [{
          label: 'スキ数',
          data: hourlyMap,
          backgroundColor: '#3498db'
        }]
      });

      const topArticles = Object.entries(articlesMap).sort((a, b) => b[1] - a[1]).slice(0, 10);
      createChart('chartArticles', 'bar', {
        labels: topArticles.map(a => {
          const title = a[0];
          let dateStr = '';
          const firstDate = articleFirstDateMap[title];
          if (firstDate) {
            const y = firstDate.getFullYear();
            const m = String(firstDate.getMonth() + 1).padStart(2, '0');
            const day = String(firstDate.getDate()).padStart(2, '0');
            dateStr = `[${y}/${m}/${day}] `;
          }
          const truncated = title.length > 40 ? title.substring(0, 40) + '...' : title;
          return dateStr + truncated;
        }),
        datasets: [{ label: 'スキ獲得数', data: topArticles.map(a => a[1]), backgroundColor: '#e67e22' }]
      }, { 
        indexAxis: 'y',
        scales: { y: { ticks: { crossAlign: 'far' } } }
      });

      const topUsers = Object.entries(usersMap).sort((a, b) => b[1] - a[1]).slice(0, 10);
      createChart('chartUsers', 'bar', {
        labels: topUsers.map(u => u[0]),
        datasets: [{ label: 'スキ回数', data: topUsers.map(u => u[1]), backgroundColor: '#9b59b6' }]
      }, { 
        indexAxis: 'y',
        scales: { y: { ticks: { crossAlign: 'far' } } }
      });

      document.getElementById('loading').style.display = 'none';
    }

    function createChart(elementId, type, data, extraOptions = {}) {
      const ctx = document.getElementById(elementId).getContext('2d');
      charts[elementId] = new Chart(ctx, {
        type: type,
        data: data,
        options: {
          responsive: true,
          maintainAspectRatio: false,
          plugins: { legend: { display: false } },
          ...extraOptions
        }
      });
    }
  </script>
</body>
</html>

ステップ3:初めてのアクセス承認&過去メールを取得する

コードを3つ保存したら、いよいよ過去の通知を一気に取得します!

  1. 画面上部のセレクトボックスから importNoteSkiData を選択し、「実行」ボタンを押します。

  2. 初回だけ 「承認が必要です」 という画面が出ます。

    • 「権限を確認」をクリック > ご自身のGoogleアカウントを選択します。

    • 「このアプリは Google で確認されていません」 という警告画面が出ますが、自分が作ったスクリプトには必ず表示されるものです。

    • 画面下の 「詳細」 をクリック > 「(安全ではないページ)に移動」 をクリックします。

    • 最後に 「許可」 をクリックすれば完了です!

💡 ポイント

過去メールが多い場合は1回で終わらないことがあります。約4.5分で自動中断し、数秒後に「続き」から自動再開してくれる安全設計です。右下の通知に「全件完了!」と出るまで少しお待ちください。

ステップ4:毎日の「最新データ自動更新」を設定する

スプレッドシートに過去データが入ったら、明日の朝から届く新しい「スキ」を毎日自動で追加する設定を行います。

  1. スクリプトエディタ上部から setupDailyTrigger を選び、「実行」ボタンを押します。

  2. 下部ログに 「毎日 午前1時〜2時 に自動で差分追加するトリガーを設定しました」 と表示されれば完了! (※毎回過去から全検索するわけではなく、直前の新しいデータのみを賢く見つけて重複せず追記します)

📊 ダッシュボードを表示してみよう

ダッシュボードを見るには、2つの方法があります。

方法A:手軽に見るなら(スプレッドシート上から開く)

スクリプトエディタ上部で openDashboardDialog を選んで実行するだけ!

現在開いているスプレッドシートの画面上に、綺麗な分析ダッシュボードのウィンドウがポップアップで現れます。

方法B:いつでもURLで開くなら(ウェブアプリ化)

ブラウザのお気に入りから一発で開きたい場合は、Webアプリとしてデプロイ(公開)します。

  1. スクリプトエディタ右上の 「デプロイ」>「新しいデプロイ」 をクリック。

  2. 左側の歯車マークから「ウェブアプリ」を選択。

  3. アクセスできるユーザーを 「自分のみ」 に設定して「デプロイ」をクリック。

  4. 発行されたウェブアプリのURLを開けば、いつでも自分のダッシュボードにアクセスできます!

読者の反応を可視化して、書くことをもっと楽しく!

このダッシュボードを作って、私はフォローしてくださっている方がどの記事を読んでくださっているのか、スキをたくさん押してくださっているクリエーターさんが誰なのかより明確に分かって、感謝の気持ちが深まりました。読者の皆様、本当にありがとうございます!

ぜひスプレッドシートとダッシュボードを取り入れて、快適なnote創作ライフにお役立てください!

いいなと思ったら応援しよう!