Tutorial

Analytics for the Stars: Using CounterAPI with Astro in 2025

Learn how to integrate CounterAPI with Astro's island architecture to build lightweight yet powerful analytics for your static and dynamic sites.

ST

Tutorial Expert

• 4 min read

CounterAPI Astro Analytics Performance Static Sites

Why Astro and CounterAPI Are a Perfect Match

Astro has revolutionized web development with its "islands architecture" that delivers zero JavaScript by default while allowing interactive islands where needed. This performance-first approach pairs perfectly with CounterAPI's lightweight analytics solution.

In this guide, we'll explore how to add powerful analytics to your Astro site without compromising its speed and efficiency.

Getting Started with CounterAPI in Astro

First, install the CounterAPI package:

npm install counterapi@latest

Setting Up a CounterAPI Service

Let's create a utility file for CounterAPI:

// src/lib/counter.js
import { Counter } from 'counterapi';

// For client-side components
export function createCounter() {
  return new Counter({
    workspace: import.meta.env.PUBLIC_COUNTER_WORKSPACE,
    accessToken: import.meta.env.PUBLIC_COUNTER_TOKEN,
  });
}

// For server-side operations
export async function serverCounter() {
  const counter = new Counter({
    workspace: import.meta.env.COUNTER_WORKSPACE,
    accessToken: import.meta.env.COUNTER_TOKEN,
  });
  
  return counter;
}

Add your environment variables in .env:

PUBLIC_COUNTER_WORKSPACE=your-workspace
PUBLIC_COUNTER_TOKEN=public-token
COUNTER_WORKSPACE=your-workspace
COUNTER_TOKEN=server-token

Server-Side Tracking with Astro

Page View Tracking with Astro SSR

Astro excels at server-side rendering. Here's how to track page views server-side:

---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
import { serverCounter } from '../lib/counter';

// Track page view on the server
let viewCount = 0;
try {
  const counter = await serverCounter();
  const result = await counter.up('home-page-view');
  viewCount = result.value;
} catch (error) {
  console.error('Analytics error:', error);
}
---

<Layout title="Welcome to My Site">
  <main>
    <h1>Welcome to My Amazing Site</h1>
    <p>This page has been viewed {viewCount} times.</p>
    
    <!-- Your page content -->
  </main>
</Layout>

Client-Side Interactive Components

Astro lets you create interactive islands with your favorite framework. Here's how to add analytics to a React component:

// src/components/LikeButton.jsx
import { useState, useEffect } from 'react';
import { createCounter } from '../lib/counter';

export default function LikeButton({ articleId }) {
  const [likes, setLikes] = useState(0);
  const [liked, setLiked] = useState(false);
  
  // Load initial count
  useEffect(() => {
    const fetchLikes = async () => {
      try {
        const counter = createCounter();
        const result = await counter.get(`article-${articleId}-likes`);
        setLikes(result.value);
      } catch (error) {
        console.error('Failed to fetch likes:', error);
      }
    };
    
    fetchLikes();
  }, [articleId]);
  
  const handleLike = async () => {
    if (liked) return;
    
    try {
      const counter = createCounter();
      const result = await counter.up(`article-${articleId}-likes`);
      setLikes(result.value);
      setLiked(true);
      
      // Store in localStorage to prevent multiple likes
      localStorage.setItem(`liked-${articleId}`, 'true');
    } catch (error) {
      console.error('Like tracking error:', error);
    }
  };
  
  // Check if already liked
  useEffect(() => {
    const hasLiked = localStorage.getItem(`liked-${articleId}`);
    if (hasLiked) setLiked(true);
  }, [articleId]);
  
  return (
    <button
      onClick={handleLike}
      disabled={liked}
      className={`like-button ${liked ? 'liked' : ''}`}
    >
      {liked ? 'Liked' : 'Like'} ({likes})
    </button>
  );
}

Use the component in your Astro page:

---
// src/pages/blog/[slug].astro
import Layout from '../../layouts/Layout.astro';
import LikeButton from '../../components/LikeButton.jsx';
import { serverCounter } from '../../lib/counter';
import { getArticle } from '../../lib/articles';

export async function getStaticPaths() {
  // Generate paths for all your articles
  // ...
}

const { slug } = Astro.params;
const article = await getArticle(slug);

// Track article view
try {
  const counter = await serverCounter();
  await counter.up(`article-${slug}-view`);
} catch (error) {
  console.error('Analytics error:', error);
}
---

<Layout title={article.title}>
  <article>
    <h1>{article.title}</h1>
    <div set:html={article.content} />
    
    <div class="article-actions">
      <LikeButton client:visible articleId={slug} />
    </div>
  </article>
</Layout>

Tracking Client-Side Navigation

For sites using Astro's View Transitions API, track navigation events:

// src/components/AnalyticsTracker.jsx
import { useEffect } from 'react';
import { createCounter } from '../lib/counter';

// This component doesn't render anything visible
export default function AnalyticsTracker() {
  useEffect(() => {
    const counter = createCounter();
    
    // Track initial page view
    const trackPage = (path) => {
      const cleanPath = path === '/' ? 'home' : path.substring(1).replace(/\//g, '-');
      counter.up(`page-${cleanPath}`)
        .catch(err => console.error('Analytics error:', err));
    };
    
    // Track the initial page
    trackPage(window.location.pathname);
    
    // Listen for Astro view transitions
    document.addEventListener('astro:page-load', (event) => {
      trackPage(new URL(event.target.baseURI).pathname);
    });
    
    // Cleanup
    return () => {
      document.removeEventListener('astro:page-load', trackPage);
    };
  }, []);
  
  // Component doesn't render anything visible
  return null;
}

Add this tracker to your main layout:

---
// src/layouts/Layout.astro
import { ViewTransitions } from 'astro:transitions';
import AnalyticsTracker from '../components/AnalyticsTracker.jsx';
const { title } = Astro.props;
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
    <ViewTransitions />
  </head>
  <body>
    <slot />
    <AnalyticsTracker client:only="react" />
  </body>
</html>

Creating an API Endpoint for Analytics Data

Astro's server endpoints are perfect for creating an analytics API:

---
// src/pages/api/analytics.js
import { serverCounter } from '../../lib/counter';

export async function GET({ request }) {
  // In a real app, add authentication here!
  
  const url = new URL(request.url);
  const counters = url.searchParams.get('counters')?.split(',');
  
  if (!counters || counters.length === 0) {
    return new Response(
      JSON.stringify({ error: 'No counters specified' }),
      { status: 400, headers: { 'Content-Type': 'application/json' } }
    );
  }
  
  try {
    const counter = await serverCounter();
    const results = {};
    
    // Fetch all requested counters
    for (const name of counters) {
      const result = await counter.get(name);
      results[name] = result.value;
    }
    
    return new Response(
      JSON.stringify({ results }),
      { headers: { 'Content-Type': 'application/json' } }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ error: 'Failed to fetch analytics data' }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    );
  }
}
---

Building an Analytics Dashboard

Create a simple analytics dashboard using Astro's hybrid rendering:

---
// src/pages/admin/analytics.astro
import Layout from '../../layouts/Layout.astro';
import { serverCounter } from '../../lib/counter';

// Protect this route with authentication in a real app!

// Define key metrics to display
const metrics = [
  'home-page-view',
  'blog-page-view',
  'contact-page-view',
  'newsletter-signup',
  'feature-usage'
];

// Fetch all metrics
const analyticsData = {};
try {
  const counter = await serverCounter();
  
  for (const metric of metrics) {
    const result = await counter.get(metric);
    analyticsData[metric] = result.value;
  }
} catch (error) {
  console.error('Failed to fetch analytics:', error);
}
---

<Layout title="Analytics Dashboard">
  <div class="dashboard">
    <h1>Site Analytics</h1>
    
    <div class="metrics-grid">
      {Object.entries(analyticsData).map(([key, value]) => (
        <div class="metric-card">
          <h3>{key.replace(/-/g, ' ')}</h3>
          <div class="metric-value">{value}</div>
        </div>
      ))}
    </div>
    
    <div class="dashboard-controls">
      <button id="refresh-btn">Refresh Data</button>
    </div>
  </div>
</Layout>

<script>
  // Client-side script to refresh data
  document.getElementById('refresh-btn').addEventListener('click', async () => {
    const metrics = [
      'home-page-view',
      'blog-page-view',
      'contact-page-view',
      'newsletter-signup',
      'feature-usage'
    ].join(',');
    
    try {
      const response = await fetch(`/api/analytics?counters=${metrics}`);
      const data = await response.json();
      
      if (data.results) {
        // Update the UI with new values
        Object.entries(data.results).forEach(([key, value]) => {
          const card = document.querySelector(`.metric-card h3:contains("${key.replace(/-/g, ' ')}")`).parentNode;
          card.querySelector('.metric-value').textContent = value;
        });
      }
    } catch (error) {
      console.error('Failed to refresh analytics:', error);
    }
  });
</script>

<style>
  .dashboard {
    padding: 2rem;
  }
  
  .metrics-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 1.5rem;
    margin: 2rem 0;
  }
  
  .metric-card {
    padding: 1.5rem;
    background: #f9f9f9;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
  
  .metric-value {
    font-size: 2.5rem;
    font-weight: bold;
    margin-top: 0.5rem;
  }
  
  .dashboard-controls {
    margin-top: 2rem;
  }
</style>

Conclusion

Astro and CounterAPI make a perfect pair for modern web development. By leveraging Astro's island architecture, you can add powerful analytics while maintaining excellent performance. CounterAPI's lightweight approach ensures that your analytics don't slow down your site, and the server-side capabilities make it easy to track important metrics without loading unnecessary JavaScript.

Whether you're building a static blog, a documentation site, or a dynamic web application with Astro, CounterAPI offers a simple yet powerful way to understand your users and track important interactions.

For more information about CounterAPI and its features, check out the official documentation.