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.
Tutorial Expert
• 4 min read
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.