Español 🇪🇸 | 中文 🇨🇳 | Deutsch 🇩🇪 | Français 🇫🇷 | 日本語 🇯🇵 | 한국어 🇰🇷 | Português 🇧🇷 | Русский 🇷🇺 | Tiếng Việt 🇻🇳 | हिन्दी 🇮🇳 | العربية 🇸🇦...
A tiny, framework-agnostic page-view counter. One job: count real views per route, hand you the number, and let you style it however you like.
npm i vistaz- 🪶 Tiny & zero-dependency client — the core is just
fetch+ storage. - 🔁 Refresh-proof —
localStoragededup, so reloads don't inflate counts. - 🧩 Works everywhere — Astro, plain HTML, Next.js, React, React Native, LynxJS.
- 🎨 Your UI — get a number and render it your way, or drop in a one-line badge.
- 🏆 Per-page counts + ranking — see which article is winning, in one query.
- 📦 Bring Your Own Database — your data lives in your free Upstash Redis. The package hosts nothing.
You need: a free Upstash Redis database and a host that runs serverless endpoints (Vercel/Netlify/Cloudflare/Astro SSR). The endpoint holds the secret token; the browser never sees it.
Create a free database at console.upstash.com, then put
its REST credentials in .env (see .env.example):
UPSTASH_REDIS_REST_URL=https://your-db.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-rest-tokenCreate one endpoint. The slug comes from the URL, so use a catch-all route.
// Astro: src/pages/api/views/[...slug].ts
import { createUpstashAdapter, createRouteHandlers } from "vistaz/server";
const views = createUpstashAdapter(); // reads UPSTASH_* from env
export const { GET, POST } = createRouteHandlers(views);// Next.js App Router: app/api/views/[...slug]/route.ts
import { createUpstashAdapter, createRouteHandlers } from "vistaz/server";
const views = createUpstashAdapter();
export const { GET, POST } = createRouteHandlers(views);Astro note: API routes need SSR — add an adapter like
@astrojs/verceland setoutput: "server"(orhybrid). A fully static build can't run endpoints.
Because Astro and Vite securely expose environment variables through import.meta.env rather
than Node's global process.env, the automatic credential reading will not work out-of-the-box.
If you are using Astro or Vite, you must explicitly pass your Upstash credentials to the adapter:
import { createUpstashAdapter } from "vistaz/server";
const views = createUpstashAdapter({
url: import.meta.env.UPSTASH_REDIS_REST_URL,
token: import.meta.env.UPSTASH_REDIS_REST_TOKEN
});
export const { GET, POST } = createRouteHandlers(views);Pick whichever fits. All three talk to the same endpoint.
---
// any .astro page
---
<p>Views: <vistaz-counter slug="blog">…</vistaz-counter></p>
<script>
import { defineViztasCounter } from "vistaz/element";
defineViztasCounter();
</script>
<style>
vistaz-counter { font-weight: 600; } /* style it like any element */
</style>import { useViews } from "vistaz/react";
export function Views({ slug }: { slug: string }) {
const { count, loading } = useViews(slug);
return <span>{loading ? "…" : count}</span>;
}React Native (no localStorage, and the endpoint must be absolute):
import AsyncStorage from "@react-native-async-storage/async-storage";
const { count } = useViews("blog", {
endpoint: "https://yoursite.com/api/views",
storage: AsyncStorage,
});import { trackView } from "vistaz";
const count = await trackView("blog"); // POST first visit, GET after
document.querySelector("#views").textContent = String(count);<img src="https://yoursite.com/api/views/blog.svg" alt="views" />Zero JS, but every load counts and you can't style an image — use a native option above when you want accuracy + your own CSS.
Add a second route to read every page ordered by traffic:
// Astro: src/pages/api/views/ranking.ts (Next: app/api/views/ranking/route.ts)
import { createUpstashAdapter, createRankingHandler } from "vistaz/server";
export const { GET } = createRankingHandler(createUpstashAdapter());GET /api/views/ranking → [{ "slug": "blog", "count": 152 }, ...] (supports ?limit=10).
Render it in a private /admin page for an instant mini-dashboard.
trackView(slug, options?): Promise<number>
createTracker(options?): { track(slug): Promise<number> }options: endpoint (default "/api/views"), cooldown (default "24h"; ms number
or "30m"/"24h"/"7d"), storage (default localStorage → memory fallback),
fetch (custom fetch). trackView never throws into your UI — on error it resolves 0.
defineViztasCounter(options?): void // registers <vistaz-counter slug endpoint cooldown>useViews(slug, options?): { count: number | null; loading: boolean; error: Error | null }createUpstashAdapter(options?): ViewsAdapter // { url?, token?, key?, redis? }
createRouteHandlers(views, options?): { GET, POST }
createRankingHandler(views): { GET }
renderCountSvg(count, options?): stringWant a different database? Implement the ViewsAdapter interface
(increment, get, getMany, getRanking) and pass it to the handlers.
Browser / App ──fetch──▶ /api/views/[slug] ──▶ Upstash Redis (your DB)
localStorage dedup your serverless route one sorted set:
(1 count per cooldown) (holds the token) count + ranking
First visit within the cooldown sends POST (increment); later visits send GET
(read-only). Counts live in a single Redis sorted set, so per-page totals and the
full ranking come from the same structure.
MIT © Gohit X — see LICENSE for details.