feat: add anonymous view counter with most-read sidebar
- node:sqlite storage in $DATA_DIR/views.db (no native deps) - POST /api/views/<slug>/ counts once per visitor per 24h via daily-salted hash; IPs are never stored, bots are skipped - GET /api/views/top/ feeds the sidebar's featured list, falling back to frontmatter-featured and latest posts when unavailable Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getPosts } from '../../../lib/posts';
|
||||
import { clientIp, isBot, recordView } from '../../../lib/views';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
// POST /api/views/<slug>/ — counts one view (sent with navigator.sendBeacon).
|
||||
export const POST: APIRoute = async ({ params, request, clientAddress }) => {
|
||||
const slug = params.slug ?? '';
|
||||
const posts = await getPosts();
|
||||
if (!posts.some((p) => p.id === slug)) return new Response(null, { status: 404 });
|
||||
if (!isBot(request.headers.get('user-agent'))) {
|
||||
recordView(slug, clientIp(request, clientAddress));
|
||||
}
|
||||
return new Response(null, { status: 204, headers: { 'Cache-Control': 'no-store' } });
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getPosts, postUrl } from '../../../lib/posts';
|
||||
import { topViews } from '../../../lib/views';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
// GET /api/views/top/?limit=3 — most viewed published posts.
|
||||
export const GET: APIRoute = async ({ url }) => {
|
||||
const limit = Math.min(10, Math.max(1, Number(url.searchParams.get('limit')) || 3));
|
||||
const posts = new Map((await getPosts()).map((p) => [p.id, p]));
|
||||
const items = topViews(limit * 2) // headroom for deleted/draft slugs
|
||||
.filter((v) => posts.has(v.slug))
|
||||
.slice(0, limit)
|
||||
.map((v) => {
|
||||
const post = posts.get(v.slug)!;
|
||||
return { slug: v.slug, title: post.data.title, url: postUrl(post), count: v.count };
|
||||
});
|
||||
return Response.json(items, { headers: { 'Cache-Control': 'public, max-age=300' } });
|
||||
};
|
||||
Reference in New Issue
Block a user