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:
@@ -71,9 +71,9 @@ const title = 'font-display mb-4 flex items-center gap-3 text-sm font-bold track
|
||||
{blog.sidebar.featured}
|
||||
<span aria-hidden="true" class="bg-accent h-0.5 w-8"></span>
|
||||
</h2>
|
||||
<ol data-featured class="flex flex-col gap-4">
|
||||
<ol data-featured data-limit="3" class="flex flex-col gap-4">
|
||||
{featured.map((post, i) => (
|
||||
<li class="flex gap-3">
|
||||
<li class="flex gap-3" data-slug={post.id}>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="font-display text-accent-text text-2xl leading-none font-black"
|
||||
@@ -115,3 +115,37 @@ const title = 'font-display mb-4 flex items-center gap-3 text-sm font-bold track
|
||||
</section>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
<script>
|
||||
// Replace the build-time fallback with the most viewed posts when the API is reachable.
|
||||
const list = document.querySelector<HTMLOListElement>('ol[data-featured]');
|
||||
if (list) {
|
||||
const limit = Number(list.dataset.limit) || 3;
|
||||
fetch(`/api/views/top/?limit=${limit}`)
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((top: { slug: string; title: string; url: string }[]) => {
|
||||
if (!top.length) return;
|
||||
const template = list.querySelector('li');
|
||||
if (!template) return;
|
||||
const fallback = [...list.querySelectorAll<HTMLLIElement>('li')];
|
||||
const items = top.map((t) => {
|
||||
const li = template.cloneNode(true) as HTMLLIElement;
|
||||
li.dataset.slug = t.slug;
|
||||
const a = li.querySelector('a')!;
|
||||
a.href = t.url;
|
||||
a.textContent = t.title;
|
||||
return li;
|
||||
});
|
||||
// Top up with fallback posts not already listed.
|
||||
for (const li of fallback) {
|
||||
if (items.length >= limit) break;
|
||||
if (!items.some((x) => x.dataset.slug === li.dataset.slug)) items.push(li);
|
||||
}
|
||||
items.forEach((li, i) => {
|
||||
li.querySelector('span')!.textContent = String(i + 1).padStart(2, '0');
|
||||
});
|
||||
list.replaceChildren(...items);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user