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

@supaku/kinring

v0.2.2

Published

Build, host, and join webrings. Framework-agnostic core, a web component banner, a fetch-style server handler, and a Payload CMS collection.

Readme

kinring

CI npm License: MIT

Build, host, and join webrings. A small TypeScript library with no runtime dependencies, a banner web component, a fetch-style server handler, and a Payload CMS collection for managing members from an admin panel.

A webring is an ordered list of personal sites. Each site carries a banner with three links: previous, next, and random. The links point at the ring's host, which redirects to the right member. kinring is the part that does the redirecting, plus the pieces around it.

   ┌──────────┐      ┌──────────┐      ┌──────────┐
   │ alpha.ex │ ───▶ │ beta.ex  │ ───▶ │ gamma.ex │ ──┐
   └──────────┘      └──────────┘      └──────────┘   │
        ▲                                             │
        └─────────────────────────────────────────────┘

Sibling of kith. Kith and kin.


Which part do you need?

| You want to… | Use | | ---------------------------------------------------- | --------------------------------------- | | join a ring someone else runs | the embed snippet | | run a ring from a JSON file on any host | @supaku/kinring + @supaku/kinring/server | | run a ring and manage members in Payload CMS | add @supaku/kinring/payload | | compute prev/next/random yourself | @supaku/kinring (core) |

Node 20.18.1 or newer. ESM only.

pnpm add @supaku/kinring

Joining a ring

The ring's host gives you a snippet like this. Paste it where you want the banner to appear. It works with JavaScript off; the script turns the plain links into a small banner that shows the ring's name and size.

<kinring-banner ring="https://markkropf.com/ring" member="your-slug">
  <p>Member of <a href="https://markkropf.com/ring">Mark's ring</a></p>
  <a href="https://markkropf.com/ring/prev?from=your-slug" rel="nofollow">Previous</a>
  <a href="https://markkropf.com/ring/random?from=your-slug" rel="nofollow">Random</a>
  <a href="https://markkropf.com/ring/next?from=your-slug" rel="nofollow">Next</a>
</kinring-banner>
<script async src="https://markkropf.com/ring/embed.js"></script>

Prefer your own markup? Drop the custom element and the script and keep the three links. ?from=your-slug tells the ring who you are; without it the ring falls back to the Referer header, which works on most sites but not all.

Style the banner from the outside with shadow parts:

kinring-banner::part(banner) { border-style: dashed; }
kinring-banner::part(name)   { font-weight: 700; }
kinring-banner::part(link)   { text-decoration: none; }

Your button

Rings usually show an 88x31 button for each member. That size is from 1996 and it is still the size. Supply:

  • button.gif/.png/.webp/.svg at 88x31 pixels (required)
  • the same image at 176x62 for high density screens (optional, src2x)

Hosts render buttons at 88x31 CSS pixels and use srcset for the 2x file. Animated GIFs are fine. Keep it under 50 KB.


Running a ring

1. Define it

A ring is a JSON object. Order matters; it is the order of the ring.

{
  "id": "marks-ring",
  "name": "Mark's ring",
  "url": "https://markkropf.com/ring",
  "description": "Friends with personal sites.",
  "members": [
    {
      "id": "truman",
      "name": "Truman Boyes",
      "url": "https://www.truman.net",
      "feed": "https://www.truman.net/feed.xml",
      "button": { "src": "https://markkropf.com/buttons/truman.png", "src2x": "https://markkropf.com/buttons/[email protected]" }
    },
    { "id": "mark", "name": "Mark Kropf", "url": "https://markkropf.com" }
  ]
}

validateRing(json) checks it and returns every problem at once.

2. Serve it

createRingHandler is a (Request) => Promise<Response> function. Mount it under your ring's path in any framework that speaks the Fetch API.

Next.js App Router, at app/ring/[[...action]]/route.ts:

import { createRingHandler } from '@supaku/kinring/server'
import ring from './ring.json'

const handler = createRingHandler({ loadRing: () => ring })
export const GET = handler

That one route serves:

| Path | What | | ---------------------------- | ------------------------------------------------------ | | /ring/prev?from=<id> | 302 to the previous member | | /ring/next?from=<id> | 302 to the next member | | /ring/random?from=<id> | 302 to a random member, never the one you came from | | /ring/ring.json | the membership feed, CORS enabled | | /ring/members.opml | every member's RSS feed as OPML | | /ring/embed.js | the banner script |

Unknown origins are sent to the ring's home page rather than a 404. Redirects are no-store and noindex.

Other runtimes: Hono, Bun, Deno, Cloudflare Workers, and SvelteKit all take the same function.

3. Build the index page

The ring's home (ring.url) is yours to design. The core exports help:

import { embedSnippet, navigationUrls, ringFeed } from '@supaku/kinring'

embedSnippet(ring, { memberId: 'truman' }) // the HTML a member pastes
navigationUrls(ring, 'truman')              // { prev, next, random, index }
ringFeed(ring)                              // what ring.json serves

Payload CMS

Manage members in the admin panel instead of a JSON file.

// payload.config.ts
import { createRingMembersCollection } from '@supaku/kinring/payload'

export default buildConfig({
  collections: [
    Media,
    createRingMembersCollection({ slug: 'ring-members', mediaSlug: 'media' }),
  ],
})

The collection has name, slug, url, description, feed, button (upload), button2x (upload), active, and order. Add your own with extraFields, or replace anything with overrides.

// app/ring/[[...action]]/route.ts
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import { createRingHandler } from '@supaku/kinring/server'
import { loadRingFromPayload } from '@supaku/kinring/payload'

export const GET = createRingHandler({
  loadRing: async () =>
    loadRingFromPayload(await getPayload({ config: configPromise }), {
      collection: 'ring-members',
      ring: { id: 'marks-ring', name: "Mark's ring", url: 'https://markkropf.com/ring' },
    }),
})

loadRingFromPayload reads active members sorted by order, turns uploads into absolute URLs, validates the result, and throws with every issue listed if an admin edit made the ring invalid.


Core API

import {
  findMember, neighbors, pick, navigationUrls,
  ringFeed, ringOpml, validateRing, embedSnippet, plainSnippet,
  siteKey, sameSite,
} from '@supaku/kinring'
  • findMember(ring, { id?, url? }): by id first, then by site. A member registered at https://x.example/blog matches pages under /blog only.
  • neighbors(ring, id): { prev, next }, wrapping at the ends.
  • pick(ring, 'prev' | 'next' | 'random', { from?, rng? }): what a navigation link should land on, or undefined when there is nowhere to go.
  • validateRing(unknown): { ring?, issues[] }.

Layout

src/core      types, url matching, neighbours, validation, snippets
src/server    createRingHandler (Fetch API)
src/embed     the banner script as a string, and defineBanner()
src/payload   createRingMembersCollection, loadRingFromPayload
tests         vitest

Contributing

pnpm install
pnpm test
pnpm build

See CONTRIBUTING.md. Security reports go through SECURITY.md.

License

MIT. See LICENSE.