Skip to content

bastndev/vistaz

Repository files navigation


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-prooflocalStorage dedup, 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.


1. Set up the database (once)

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-token

2. Add the API route (server)

Create 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/vercel and set output: "server" (or hybrid). A fully static build can't run endpoints.

🚀 Note for Astro / Vite Users

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);

3. Show the counter (client)

Pick whichever fits. All three talk to the same endpoint.

Web component — Astro / plain HTML (no React)

---
// 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>

React / Next.js / React Native / LynxJS

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,
});

Plain function (build your own UI)

import { trackView } from "vistaz";

const count = await trackView("blog"); // POST first visit, GET after
document.querySelector("#views").textContent = String(count);

Badge — works anywhere an image does (even Markdown)

<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.


Your private stats page (ranking)

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.


API

Client — vistaz

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.

Element — vistaz/element

defineViztasCounter(options?): void   // registers <vistaz-counter slug endpoint cooldown>

React — vistaz/react

useViews(slug, options?): { count: number | null; loading: boolean; error: Error | null }

Server — vistaz/server

createUpstashAdapter(options?): ViewsAdapter   // { url?, token?, key?, redis? }
createRouteHandlers(views, options?): { GET, POST }
createRankingHandler(views): { GET }
renderCountSvg(count, options?): string

Want a different database? Implement the ViewsAdapter interface (increment, get, getMany, getRanking) and pass it to the handlers.


How counting works

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.


📄 License

MIT © Gohit X — see LICENSE for details.



Built with Bun, TypeScript, and Web Components.

Website · GitHub · npm

About

Package npm / pnpm / bun (🟥)

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors