開発・個人開発

Supabaseでアクセス数カウンターを作る 静的サイトに動的機能を追加

Supabaseでアクセス数カウンターを作る 静的サイトに動的機能を追加

この記事の要点

静的サイトにページビューカウンターを実装するには、Supabaseのデータベースとクライアントサイドのリクエストを組み合わせる。Astro + Supabaseで実際に動くカウンターを作る手順を解説する。

結論

Astro + Supabaseでアクセスカウンターを実装するには、Supabaseにpage_viewsテーブルを作り、記事ページが表示されたときにクライアントサイドのJavaScriptからAPIを呼び出してカウントを更新する。管理画面でカウントを表示することで、Google Analyticsなしに自前の計測が完成する。

テーブル設計

SupabaseのSQL Editorで以下を実行してテーブルを作成する。

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 page_views ENABLE ROW LEVEL SECURITY;

-- 全員が読み取り可能
CREATE POLICY "Allow select" ON page_views
  FOR SELECT USING (true);

-- 全員が挿入可能
CREATE POLICY "Allow insert" ON page_views
  FOR INSERT WITH CHECK (true);

-- 全員が更新可能(カウンターのため)
CREATE POLICY "Allow update" ON page_views
  FOR UPDATE USING (true);

slugカラムにUNIQUE制約を設けることで、記事ごとに1レコードが対応する設計になる。

カウントのUPSERT関数をSQL Functionとして定義

UPSERTとは「存在すれば更新、なければ挿入」の操作だ。記事のスラッグが初回アクセスのときは新規レコードを作り、2回目以降はカウントをインクリメントする。

Supabaseのダッシュボード → Database → Functionsで以下を作成する:

CREATE OR REPLACE FUNCTION increment_view(page_slug text)
RETURNS void AS $$
BEGIN
  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;

この関数を呼び出すだけでUPSERTが実行される。アプリ側のコードをシンプルに保てる。

Astroでの実装

1. Supabaseクライアントの設定

src/lib/supabase.ts(またはjs)を作る。

import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  import.meta.env.PUBLIC_SUPABASE_URL,
  import.meta.env.PUBLIC_SUPABASE_ANON_KEY
);

Astroではクライアントサイドで使う環境変数にPUBLIC_プレフィックスをつける必要がある。

.envに追加:

PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...

2. カウンターコンポーネントの作成

src/components/ViewCounter.astroを作成する。

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

<span id="view-count" data-slug={slug}>
  <!-- カウントはJS側で入れる -->
</span>

<script>
  import { supabase } from '../lib/supabase';

  const el = document.getElementById('view-count');
  const slug = el?.dataset.slug;
  if (!slug) return;

  // 自分のアクセスをスキップするフラグ
  const isOwner = localStorage.getItem('owner') === 'true';

  // カウントアップ(オーナーでない場合)
  if (!isOwner) {
    await supabase.rpc('increment_view', { page_slug: slug });
  }

  // 現在のカウントを取得して表示
  const { data, error } = await supabase
    .from('page_views')
    .select('count')
    .eq('slug', slug)
    .single();

  if (data && !error) {
    el.textContent = `${data.count.toLocaleString()} views`;
  }
</script>

3. 記事ページへの組み込み

src/pages/articles/[slug].astro(または記事レイアウト)にViewCounterを追加する。

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

<article>
  <ViewCounter slug={slug} />
  <!-- 記事本文 -->
  <slot />
</article>

自分のアクセスを除外する管理者フラグ

自分が記事を閲覧したときにカウントが増えると、管理者の閲覧がデータを汚染する。localStorageを使ったフラグで対処する。

管理画面またはブラウザのコンソールから:

// 管理者フラグをオン(以降カウントされない)
localStorage.setItem('owner', 'true');

// フラグをオフ(通常ユーザーとして計測される)
localStorage.removeItem('owner');

このフラグはブラウザごとに保存されるため、デバイスをまたいでの除外はできない。複数デバイスで閲覧する場合はそれぞれで設定が必要だ。

管理画面でカウントを確認する

src/pages/admin/views.astroを作成して、記事別のアクセス数一覧を表示する。

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

const { data: views } = await supabase
  .from('page_views')
  .select('slug, count, updated_at')
  .order('count', { ascending: false });
---

<h1>アクセスランキング</h1>
<table>
  <thead>
    <tr><th>記事</th><th>PV</th><th>最終更新</th></tr>
  </thead>
  <tbody>
    {views?.map(v => (
      <tr>
        <td><a href={`/articles/${v.slug}/`}>{v.slug}</a></td>
        <td>{v.count.toLocaleString()}</td>
        <td>{new Date(v.updated_at).toLocaleDateString('ja-JP')}</td>
      </tr>
    ))}
  </tbody>
</table>

この管理ページは/admin/views/でアクセスできる。認証をかける場合はSupabase Authと組み合わせる。

本番環境での注意点

ボットのカウントを除外する:Google・Bing・各種クローラーのアクセスもカウントされる。User-Agentでのフィルタリングをクライアントサイドで行うことは難しいため、異常に多いカウントは手動確認が必要になる。より精密な管理が必要な場合はEdge Functionsを使うと対処しやすい。

大量アクセス時の考慮:Freeプランは月200万件のAPIリクエストまで無料だ。月10万PV・1ページ1リクエストとして月10万件のリクエストになる。Freeプランの範囲内に十分収まるが、100万PVを超えるようになったら有料プランへの移行を検討する。

RLSのセキュリティ:今回の実装ではRLSでanon userに更新を許可しているため、意図的なカウント操作(カウントの大量インクリメント)は理論的には可能だ。精密な防御が必要な場合は、RateLimitの実装やEdge Functionsへの移行を検討する。

Supabaseのセットアップ全体についてはSupabaseの始め方を、静的サイトに動的機能を追加する設計方針は静的サイトに動的機能を足す設計(JAMstack)で解説している。

まとめ

Astro + Supabaseのアクセスカウンターは、SQL Functionを1つ作ってクライアントサイドからrpc()を呼び出す形で実装できる。自分のアクセスをlocalStorageフラグで除外すると、開発中のカウント汚染を防げる。Google Analyticsなしで自前のアクセス計測を持てることは、プライバシーの観点でも差別化になる。

よくある質問

静的サイトでアクセスカウンターを作るにはなぜSupabaseが必要ですか

静的サイトはHTMLをそのまま配信するため、サーバーサイドでデータを処理する仕組みがありません。Supabaseのようなバックエンドサービスを使うことで、ブラウザ(クライアントサイド)からAPI経由でデータベースを読み書きできます。

Google Analyticsの代わりにSupabaseでアクセス計測できますか

できます。SupabaseにアクセスデータをAPIで記録することで、サードパーティーのトラッキングスクリプトなしに自前の計測が実現できます。ただし、ボットのアクセスを除外したり、ユニークユーザーを識別したりする処理は自分で実装する必要があります。

自分のアクセスもカウントされてしまうのを防げますか

localStorageに「管理者フラグ」を保存して、フラグがある場合はカウントをスキップする方法が簡単です。管理画面からフラグをオン・オフできるようにすると実用的です。

アクセスカウンターをUPSERT(なければ作る・あれば更新)で実装するにはどうしますか

Supabaseのupsertメソッドを使います。slugをユニークキーにして、既存レコードがあればcountをインクリメント、なければ新規作成するSQL関数を定義することで実現できます。