npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vistaz

v0.2.1

Published

Tiny, framework-agnostic page-view counter. Bring your own database (Upstash Redis). Works in Astro, Next.js, React, React Native and plain HTML.

Readme

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