SYSTEM NOTICE

Auto translation by AI. Be sure, accuracy, nuances and authorial intent may not be fully reflected.
見出し画像

[For IT Engineers] Automatically Aggregate Note 'Like' Notification Emails! Your Own Personal Analytics Dashboard

When you write on Note, you receive email notifications saying, 'You received a like from [Name]!'

It's a wonderful moment that makes you feel like your article has truly reached your readers.

But have you ever thought about things like this?

  • 'I want to quickly compare which articles have received the most likes in the past.'

  • 'I want to know the monthly trends in likes and the times of day when my articles are read most.'

  • 'I want to cherish the "repeat readers" who like my posts multiple times.'

Actually, if you utilize the Gmail notification emails you receive regularly, anyone can easily create their own 'personal like analytics dashboard'! My apologies, but this is for Gmail users only.

In this article, I am releasing a completely free tool that uses Google Sheets and Google Apps Script (GAS) to automatically aggregate the notifications you receive and analyze them with charts.It can be completed just by copying and pasting, so basically no programming knowledge is required. Even if you have never programmed before, please give it a try!

Dashboard concept image

I cannot guarantee the operation of this tool. Also, please understand that I am generally unable to provide follow-up support for readers using it.

🔒 For your peace of mind (About security)

Before installing the program, I would like to share three points regarding safety.

  1. No personal information is sent externally

    1. The program for this tool runs only within your Google account (between Gmail and Google Sheets), and data is never sent to external servers or similar locations.

  2. Manage safety by setting web access to 'Only myself'

    1. When opening the dashboard screen as a web app, if you set the access permissions to 'Only myself,' your analytical data will not be visible to others.

  3. Please be considerate when posting screenshots to SNS

    1. The dashboard analysis screen displays the usernames of those who liked your posts. When posting screenshots of the screen to SNS or similar platforms, please be considerate of your readers by blurring out usernames.

🛠 Installation Steps Summary (Estimated time: approx. 10 minutes)

There are 4 steps!

  1. Create a new Google Spreadsheet

  2. Copy and paste the three codes (for initialization, daily automatic updates, and screen design)

  3. Authorize access and retrieve all past notifications

  4. Set up the 'Daily Automatic Update Timer' with a single click

Step 1: Prepare the Spreadsheet

  1. Create a new Google Spreadsheet.

  2. Change the sheet name at the bottom of the screen from 'Sheet1' to Like Analysis (please enter it exactly, including kanji and capitalization).

  3. From the menu at the top of the screen, click 'Extensions' > 'Apps Script'. This will open the program editing screen (script editor).

Step 2: Copy and Paste the Three Programs

Once the script editor is open, create three files according to their purpose and paste the code into them.

1. Code to import all past 'Like' notifications at once (Code.gs)

Clear the contents of the existing Code.gs file and paste the following code as is.

// ====== 【コード.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 スキ分析ダッシュボード');
}

2. Code to add 'only new notifications' every day (dailyget.gs)

On the left side of the script editor, click the 'plus' mark to the right of 'Files' > 'Script' and name the new file dailyget. Paste the following code there.

// ====== 【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);
    }
  }
}

3. HTML to create the dashboard screen (Index.html)

Click the 'plus' mark > 'HTML' on the left again and name the file Index. Delete all existing text and paste the following code.

<!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>

Step 3: First-time Access Authorization & Retrieving Past Emails

Once you have saved all three codes, it is finally time to retrieve all past notifications at once!

  1. Select importNoteSkiData from the select box at the top of the screen and click the 'Run' button.

  2. Only the first time, an "Authorization Required" screen will appear.

    • Click "Review Permissions" > Select your Google account.

    • A warning screen saying "This app isn't verified by Google" will appear, but this is something that always shows up for scripts you create yourself.

    • Click "Advanced" at the bottom of the screen > Click "Go to (unsafe page)".

    • Finally, click "Allow" and you're done!

💡 Tip

If there are many past emails, it may not finish in one go. It is designed safely to automatically pause after about 4.5 minutes and resume automatically from where it left off after a few seconds. Please wait a little while until "All items complete!" appears in the notification at the bottom right.

Step 4: Set up daily "Automatic Latest Data Update"

Once past data is in the spreadsheet, set it up to automatically add new "Likes" that arrive from tomorrow morning every day.

  1. Select setupDailyTrigger from the top of the script editor and press the "Run" button.

  2. If "Trigger set to automatically add differences daily between 1:00 AM and 2:00 AM" is displayed in the bottom log, you're done! (*It doesn't search everything from the past every time; it intelligently finds only the latest new data and appends it without duplication.)

📊 Let's display the dashboard

There are two ways to view the dashboard.

Method A: For easy viewing (open from within the spreadsheet)

Just select openDashboardDialog at the top of the script editor and run it!

A beautiful analytics dashboard window will pop up on the screen of the spreadsheet you currently have open.

Method B: To open via URL anytime (Web App)

If you want to open it instantly from your browser favorites, deploy (publish) it as a Web App.

  1. Click "Deploy" > "New deployment" at the top right of the script editor.

  2. Select "Web app" from the gear icon on the left.

  3. Set the accessible users to Only myself and click 'Deploy'.

  4. Once you open the issued web app URL, you can access your dashboard at any time!

Visualize reader reactions and make writing even more fun!

By creating this dashboard, I have a clearer understanding of which articles my followers are reading and which creators are giving me the most likes, which has deepened my sense of gratitude. To all my readers, thank you so much!

Please incorporate spreadsheets and dashboards to help make your note creation life more comfortable!

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