開発・個人開発

静的サイトにアクセス解析を自作する Google Analytics不要の計測設計

静的サイトにアクセス解析を自作する Google Analytics不要の計測設計

この記事の要点

Supabaseを使って静的サイトのアクセスデータを自前で計測する仕組みを作れば、Google Analyticsのトラッキングスクリプトなしにページビュー・人気記事・流入元を把握できる。設計と実装を解説する。

結論

Supabaseを使った自前アクセス計測の最小構成は、page_viewsテーブルへのInsert + 管理ページでのSELECTだ。Google Analyticsのスクリプトを置き換えるには、計測の対象(何を記録するか)を決めて、クライアントサイドのJavaScriptから記録する実装を追加するだけだ。

計測する指標の設計

自前の計測で把握できる指標を設計する。

最低限(シンプルな実装で実現)

  • 記事別のページビュー数
  • 人気記事ランキング
  • 総アクセス数

追加できる指標(実装が少し複雑になる)

  • リファラー(流入元)
  • 閲覧デバイスの種類(モバイル/デスクトップ)
  • 国・地域(IPアドレスから)
  • ページ滞在時間(ページ退出時に記録)

個人開発のメディアサイトであれば、「記事別PVと人気ランキング」だけでも十分な情報が得られる。

データベース設計

Supabaseのダッシュボード(SQL Editor)で実行する。

-- 詳細なアクセスログを記録するテーブル
CREATE TABLE access_logs (
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  slug text NOT NULL,
  referrer text,
  device_type text CHECK (device_type IN ('mobile', 'tablet', 'desktop')),
  created_at timestamptz DEFAULT now()
);

-- 記事別の集計テーブル(高速なランキング取得用)
CREATE TABLE page_views (
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  slug text UNIQUE NOT NULL,
  count integer NOT NULL DEFAULT 0,
  updated_at timestamptz DEFAULT now()
);

-- RLS設定
ALTER TABLE access_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE page_views ENABLE ROW LEVEL SECURITY;

-- 全員がInsert可能(アクセス記録のため)
CREATE POLICY "Allow insert access_logs" ON access_logs
  FOR INSERT WITH CHECK (true);

CREATE POLICY "Allow read page_views" ON page_views
  FOR SELECT USING (true);

CREATE POLICY "Allow insert page_views" ON page_views
  FOR INSERT WITH CHECK (true);

CREATE POLICY "Allow update page_views" ON page_views
  FOR UPDATE USING (true);

UPSERTのSQL関数

アクセスのたびに呼び出す関数を作る。page_viewsテーブルへのUPSERT(なければInsert・あればUpdate)を1回のRPC呼び出しで行う。

CREATE OR REPLACE FUNCTION record_page_view(
  page_slug text,
  page_referrer text DEFAULT NULL,
  page_device text DEFAULT 'desktop'
)
RETURNS void AS $$
BEGIN
  -- 詳細ログに記録
  INSERT INTO access_logs (slug, referrer, device_type)
  VALUES (page_slug, page_referrer, page_device);
  
  -- 集計テーブルをUPSERT
  INSERT INTO page_views (slug, count)
  VALUES (page_slug, 1)
  ON CONFLICT (slug)
  DO UPDATE SET
    count = page_views.count + 1,
    updated_at = now();
END;
$$ LANGUAGE plpgsql;

クライアントサイドの実装

Astroの記事ページに計測スクリプトを追加する。src/components/Analytics.astroを作成する。

---
interface Props {
  slug: string;
}
const { slug } = Astro.props;
---

<script define:vars={{ slug }}>
  import { supabase } from '../lib/supabase';

  // 自分のアクセスをスキップ
  if (localStorage.getItem('owner') === 'true') return;

  // ボットの除外(簡易)
  const botPatterns = /Googlebot|Bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot/i;
  if (botPatterns.test(navigator.userAgent)) return;

  // デバイスタイプの判定
  const deviceType = window.innerWidth < 768 ? 'mobile' 
    : window.innerWidth < 1024 ? 'tablet' 
    : 'desktop';

  // リファラーの取得(外部からの流入のみ記録)
  const referrer = document.referrer && !document.referrer.includes(window.location.hostname)
    ? new URL(document.referrer).hostname
    : null;

  // アクセスを記録
  await supabase.rpc('record_page_view', {
    page_slug: slug,
    page_referrer: referrer,
    page_device: deviceType,
  });
</script>

記事レイアウトから呼び出す:

---
import Analytics from '../components/Analytics.astro';
const { slug } = Astro.params;
---

<Analytics slug={slug} />

管理ページでの可視化

src/pages/admin/analytics.astroを作成する。

---
import { supabase } from '../../lib/supabase';

// 人気記事ランキング
const { data: topPages } = await supabase
  .from('page_views')
  .select('slug, count, updated_at')
  .order('count', { ascending: false })
  .limit(20);

// 今日のアクセス数
const today = new Date().toISOString().split('T')[0];
const { count: todayCount } = await supabase
  .from('access_logs')
  .select('*', { count: 'exact', head: true })
  .gte('created_at', `${today}T00:00:00`);

// 流入元ランキング
const { data: referrers } = await supabase
  .from('access_logs')
  .select('referrer')
  .not('referrer', 'is', null)
  .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
---

<h1>アクセス解析</h1>

<section>
  <h2>今日のアクセス数</h2>
  <p>{todayCount} PV</p>
</section>

<section>
  <h2>人気記事トップ20</h2>
  <table>
    <thead><tr><th>記事</th><th>累計PV</th></tr></thead>
    <tbody>
      {topPages?.map(p => (
        <tr>
          <td><a href={`/articles/${p.slug}/`}>{p.slug}</a></td>
          <td>{p.count.toLocaleString()}</td>
        </tr>
      ))}
    </tbody>
  </table>
</section>

プライバシーへの配慮

自前の計測でもプライバシーへの配慮が必要だ。

収集しない情報:IPアドレスの完全な記録・氏名・メールアドレス・ログインユーザーの行動の追跡(管理者が閲覧者を特定できないようにする)

プライバシーポリシーへの記載:自前のアクセス計測を行っている場合、プライバシーポリシーにその旨を記載することを推奨する。

Supabaseのアクセスカウンターの基本実装はSupabaseでアクセス数カウンターを作るで、管理ページの認証実装はSupabase Authでメール認証ログインを実装するで解説している。

まとめ

静的サイトの自前アクセス解析は、SupabaseのページビューテーブルとクライアントサイドのJavaScriptだけで実現できる。Google Analyticsより収集できる情報は限られるが、データを自社で保有できることとトラッキングブロッカーの影響を受けにくいことが利点だ。管理ページで人気記事ランキングと日次PVを確認できれば、コンテンツ戦略の改善に必要な情報は十分得られる。

よくある質問

なぜGoogle Analyticsを使わずに自前の計測を作るのですか

Google Analyticsはユーザーの行動データをGoogleが収集します。プライバシーへの配慮からトラッキングブロッカーでブロックされる割合も増えており、実際のアクセス数を過小計上する傾向があります。自前の計測はデータを自社で保有でき、EUのGDPR準拠の観点でも管理しやすい利点があります。

Supabaseでアクセス解析を作るとGoogle Analyticsと同じ機能が使えますか

Google Analyticsは多機能な分析プラットフォームで、自前の計測では全機能を再現することは難しいです。ページビュー数・人気記事・簡単な流入元程度であれば自前で実現できます。詳細な行動分析が必要な場合は、プライバシーファーストな代替サービス(Plausible・Umami等)との組み合わせも選択肢です。

計測データはどのくらいの期間保存できますか

Supabaseのデータベースに保存するため、Freeプランの500MB制限内であれば無期限に保存できます。1日1,000PVのサイトで1年分のアクセスデータは数MB程度のため、個人規模では容量の心配は不要です。

ボットのアクセスを除外する方法はありますか

User-Agentでのフィルタリング(Googlebot・Bingbotなど主要クローラーを除外)をクライアントサイドで行う方法が基本です。ただしクライアントサイドの除外は不完全で、ボットによってはJavaScriptを実行して計測されてしまうケースもあります。完全な除外にはサーバーサイドでの処理が必要です。