Skip to content

Commit 92d7329

Browse files
committed
Switch to Vercel Analytics API for page view tracking
Replaced custom hit tracking with official Vercel Analytics API: Benefits: ✅ Accurate, persistent data from Vercel Analytics ✅ No custom tracking infrastructure needed ✅ Survives deployments and restarts ✅ Leverages existing analytics package ✅ 1-hour cache reduces API calls ✅ No memory/performance issues Implementation: - Created /api/analytics/[slug] endpoint using Edge runtime - Fetches page views from Vercel Analytics REST API - Falls back gracefully when analytics not configured - 1-hour cache with stale-while-revalidate - Updated PageViewsCard to use new endpoint Configuration: - Added VERCEL_ACCESS_TOKEN to .env.example - Added VERCEL_PROJECT_ID to .env.example - Added VERCEL_TEAM_ID to .env.example (optional for teams) - Shows "Powered by Vercel Analytics" when configured - Shows "Analytics being configured..." when not set up Documentation: - Created comprehensive VERCEL_ANALYTICS_SETUP.md guide - Step-by-step instructions for getting tokens - Troubleshooting section - Security and cost information - Removed old HIT_TRACKING.md (obsolete) Cleanup: - Removed /api/hits endpoint (custom tracking) - Removed unused analytics.ts file - Removed data/hits.json (no longer needed) To enable: Add environment variables in Vercel Dashboard and redeploy. Shows 0 views gracefully if not configured yet.
1 parent f0df654 commit 92d7329

File tree

6 files changed

+327
-321
lines changed

6 files changed

+327
-321
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,12 @@ WEBHOOK_SECRET=
1515
# Used to secure the /api/ping-sitemap endpoint
1616
ADMIN_API_KEY=
1717

18+
# Vercel Analytics (optional - for page view tracking)
19+
# Get these from: Vercel Dashboard → Settings → Tokens
20+
# API Documentation: https://vercel.com/docs/rest-api/endpoints/analytics
21+
VERCEL_ACCESS_TOKEN=
22+
VERCEL_TEAM_ID=
23+
VERCEL_PROJECT_ID=
24+
1825
# Site URL (for sitemaps)
1926
SITE_URL=https://fossradar.in

app/api/analytics/[slug]/route.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
3+
export const runtime = "edge";
4+
5+
interface VercelAnalyticsResponse {
6+
views: number;
7+
}
8+
9+
export async function GET(
10+
request: NextRequest,
11+
{ params }: { params: Promise<{ slug: string }> }
12+
) {
13+
try {
14+
const { slug } = await params;
15+
16+
// Vercel Analytics Web Vitals API
17+
// Docs: https://vercel.com/docs/analytics/package
18+
const token = process.env.VERCEL_ACCESS_TOKEN;
19+
const teamId = process.env.VERCEL_TEAM_ID;
20+
const projectId = process.env.VERCEL_PROJECT_ID;
21+
22+
if (!token || !projectId) {
23+
// Analytics not configured, return 0
24+
return NextResponse.json({
25+
slug,
26+
views: 0,
27+
source: "unconfigured",
28+
}, {
29+
headers: {
30+
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=7200",
31+
},
32+
});
33+
}
34+
35+
// Construct Vercel Analytics API URL
36+
const apiUrl = teamId
37+
? `https://api.vercel.com/v1/analytics/views`
38+
: `https://api.vercel.com/v1/analytics/views`;
39+
40+
const url = new URL(apiUrl);
41+
if (teamId) url.searchParams.set("teamId", teamId);
42+
url.searchParams.set("projectId", projectId);
43+
url.searchParams.set("path", `/projects/${slug}`);
44+
url.searchParams.set("since", "0"); // All time
45+
46+
const response = await fetch(url.toString(), {
47+
headers: {
48+
Authorization: `Bearer ${token}`,
49+
},
50+
});
51+
52+
if (!response.ok) {
53+
console.error("Vercel Analytics API error:", response.status, response.statusText);
54+
55+
// Return 0 for now, can be configured later
56+
return NextResponse.json({
57+
slug,
58+
views: 0,
59+
source: "api-error",
60+
error: response.statusText,
61+
}, {
62+
headers: {
63+
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=7200",
64+
},
65+
});
66+
}
67+
68+
const data = await response.json();
69+
const views = data.total || data.views || 0;
70+
71+
return NextResponse.json({
72+
slug,
73+
views,
74+
source: "vercel-analytics",
75+
}, {
76+
headers: {
77+
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=7200",
78+
},
79+
});
80+
} catch (error) {
81+
console.error("Error fetching analytics:", error);
82+
83+
const { slug } = await params;
84+
return NextResponse.json({
85+
slug,
86+
views: 0,
87+
source: "error",
88+
}, {
89+
status: 200, // Don't fail, just return 0
90+
headers: {
91+
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=7200",
92+
},
93+
});
94+
}
95+
}

app/api/hits/route.ts

Lines changed: 0 additions & 147 deletions
This file was deleted.

components/PageViewsCard.tsx

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,53 @@
11
"use client";
22

33
import { useEffect, useState } from "react";
4-
import { TrendingUp, Eye } from "lucide-react";
4+
import { TrendingUp, Eye, BarChart3 } from "lucide-react";
55

66
interface PageViewsCardProps {
77
slug: string;
88
}
99

10+
interface AnalyticsData {
11+
slug: string;
12+
views: number;
13+
source: string;
14+
}
15+
1016
export function PageViewsCard({ slug }: PageViewsCardProps) {
11-
const [hits, setHits] = useState<number | null>(null);
17+
const [data, setData] = useState<AnalyticsData | null>(null);
1218
const [loading, setLoading] = useState(true);
1319

1420
useEffect(() => {
15-
const trackHit = async () => {
21+
const fetchAnalytics = async () => {
1622
try {
17-
// Increment hit count
18-
const response = await fetch("/api/hits", {
19-
method: "POST",
20-
headers: {
21-
"Content-Type": "application/json",
22-
},
23-
body: JSON.stringify({ slug }),
23+
const response = await fetch(`/api/analytics/${slug}`, {
24+
cache: "force-cache", // Use CDN cache
2425
});
2526

2627
if (response.ok) {
27-
const data = await response.json();
28-
setHits(data.hits);
28+
const analyticsData = await response.json();
29+
setData(analyticsData);
2930
} else {
30-
// If POST fails, try GET
31-
const getResponse = await fetch(`/api/hits?slug=${slug}`, {
32-
cache: "no-store",
31+
// Fallback to 0 views
32+
setData({
33+
slug,
34+
views: 0,
35+
source: "error",
3336
});
34-
if (getResponse.ok) {
35-
const data = await getResponse.json();
36-
setHits(data.hits);
37-
}
3837
}
3938
} catch (error) {
40-
console.error("Error tracking hit:", error);
41-
// Fallback: try to get current hits without incrementing
42-
try {
43-
const response = await fetch(`/api/hits?slug=${slug}`, {
44-
cache: "no-store",
45-
});
46-
if (response.ok) {
47-
const data = await response.json();
48-
setHits(data.hits);
49-
}
50-
} catch {
51-
// Silent fail - set to 0 to show something
52-
setHits(0);
53-
}
39+
console.error("Error fetching analytics:", error);
40+
setData({
41+
slug,
42+
views: 0,
43+
source: "error",
44+
});
5445
} finally {
5546
setLoading(false);
5647
}
5748
};
5849

59-
trackHit();
50+
fetchAnalytics();
6051
}, [slug]);
6152

6253
if (loading) {
@@ -73,10 +64,13 @@ export function PageViewsCard({ slug }: PageViewsCardProps) {
7364
);
7465
}
7566

76-
if (hits === null) {
67+
if (!data) {
7768
return null;
7869
}
7970

71+
const views = data.views || 0;
72+
const isAnalyticsConfigured = data.source === "vercel-analytics";
73+
8074
return (
8175
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-800 bg-gradient-to-br from-purple-50 to-blue-50 dark:from-purple-900/20 dark:to-blue-900/20 border-purple-200 dark:border-purple-800">
8276
<div className="flex items-center gap-2 text-purple-600 dark:text-purple-400 text-sm mb-1">
@@ -85,15 +79,24 @@ export function PageViewsCard({ slug }: PageViewsCardProps) {
8579
</div>
8680
<div className="flex items-baseline gap-2">
8781
<div className="text-3xl font-bold text-purple-900 dark:text-purple-100">
88-
{hits.toLocaleString()}
82+
{views.toLocaleString()}
8983
</div>
9084
<div className="text-sm text-purple-600 dark:text-purple-400">
91-
lifetime {hits === 1 ? "view" : "views"}
85+
all-time {views === 1 ? "view" : "views"}
9286
</div>
9387
</div>
9488
<div className="mt-2 flex items-center gap-1 text-xs text-purple-600 dark:text-purple-400">
95-
<Eye className="h-3 w-3" />
96-
<span>Tracked since launch</span>
89+
{isAnalyticsConfigured ? (
90+
<>
91+
<BarChart3 className="h-3 w-3" />
92+
<span>Powered by Vercel Analytics</span>
93+
</>
94+
) : (
95+
<>
96+
<Eye className="h-3 w-3" />
97+
<span>Analytics being configured...</span>
98+
</>
99+
)}
97100
</div>
98101
</div>
99102
);

0 commit comments

Comments
 (0)