@submitator/badges: the npm package for your directory badges

Open source · MIT · no dependencies

Your badges.
In your HTML.

For Next.js, Nuxt, SvelteKit and Astro. It fetches your badges on your server, checks our signature and every badge, and renders them with your framework's own elements, so directories find them in the HTML your server sends.

Last updated · npm i @submitator/badges

app/page.tsxTSX
// app/page.tsx: your homepage, a Server Component (no "use client")import { SubmitatorBadges } from "@submitator/badges/react"; export default function Home() {  return (    <footer>      <SubmitatorBadges project="YOUR_PROJECT_ID" />    </footer>  );}

Short version

  • Your server fetches your badges from our API with your public project ID. Your visitors' browsers never call our API.
  • The badges come as one signed list, a wall. The package checks our signature, then every badge: a badge that fails is left out, and a wall that fails is never shown.
  • It builds plain links, images and text with your framework's own elements: no HTML from our servers, no script tags, no cookies.
  • MIT licensed, no dependencies, code on GitHub.

Ask an AI to check it

You do not have to take our word for any of this. Ask an assistant to read this page and the package's source, and to tell you what it finds.

The question

Before I add the npm package @submitator/badges to my site, help me check it. Read https://submitator.com/badge-package and its source at https://github.com/submitator/badges, then tell me in plain words: what runs on my server, what it sends and to whom, whether it can put scripts or HTML into my pages, and what happens to my site if Submitator is down.

Each button opens the assistant with this question, ready to send:

What it does

  1. 1

    Fetches your badges on your server. It asks api.submitator.com for your project's wall and keeps it in memory. It refreshes the wall in the background every 60 seconds by default, while your pages keep using the copy they have. A server with no copy yet, or only an expired one, waits 2 seconds at most by default.

  2. 2

    Checks them. Our signature first, then every badge: see What it checks first, below.

  3. 3

    Renders them with your framework. React on Next.js, Vue on Nuxt, Svelte on SvelteKit, an Astro component on Astro, or an escaped HTML string on any other Node server. The badges are in the HTML your server sends, which is what directories and our own check read.

  4. 4

    Keeps them current. On a page your server renders on request, new badges show up within minutes, with nothing to redeploy. From day one it shows your Featured on Submitator badge; directory badges join it as we submit you to directories that ask for one, and so do badges you add in the app.

The request carries your project ID, the package version and your framework's name, and nothing about your visitors. At most once an hour we note when your server fetched and with which version and framework, to show your install on Configure Badges. Like every request to our API, each one is also written to our server log with your server's IP address, which our host keeps for up to 30 days.

What it never does

  • No calls from your visitors' browsers.

    Only your server talks to us. Run in a browser, the package's fetch returns nothing and sends nothing.

  • No HTML from our servers.

    We send data; the package builds every element itself, and every value is escaped, by your framework or by the package.

  • No script tag.

    The scrolling strip moves with CSS alone, pauses on hover and keyboard focus, and stands still for visitors who ask for reduced motion. With Next.js Server Components (App Router) and Astro, the badge component ships no JavaScript to the browser. On Nuxt, SvelteKit and the Next.js Pages Router it hydrates with the rest of your app, like any component.

  • No cookies.

    Its request carries no cookie and nothing about your visitors, and our answer sets none.

  • No secrets, no settings.

    It reads no secrets and no configuration. Of your environment variables it reads only NODE_ENV and NEXT_RUNTIME: to mark requests from development servers, and to spot Next.js, so it can use Next's cache.

  • No broken pages.

    If our API is slow, down or answers something unexpected, your server keeps its last good copy or renders the page without badges.

What your visitors' browsers load: each badge image, from the address in its embed code (usually the directory's own site). The Featured on Submitator badge is drawn inline and loads nothing.

What it checks first

Every wall we send is signed with an Ed25519 key. The package carries two public keys, the one we sign with and a backup we keep offline, and checks the signature before it reads anything else. A wall that is unsigned, changed on the way or signed by anyone else is refused, and no option changes that for our API: publicKeys, which swaps the keys for a local or test backend, is refused for any address on api.submitator.com. Then it checks that:

  • the wall is for your project ID and has not expired (each one is valid for 7 days);
  • each badge is made only of links, images, text and simple shapes (a, img, span, svg, path and circle), with attributes from a fixed list: no class, no id and no event handlers;
  • every link and image address starts with http:// or https:// (a few directories still use http): never javascript:, data: or a relative path;
  • each inline style passes declaration by declaration: no url(), image-set(), @import, expression(), behavior or position: fixed, no comments and no characters that could break out of the style;
  • shapes sit only inside an svg, and no link sits inside another link;
  • the response, the wall and each badge stay within fixed limits: 1 MiB per response, 200 badges per wall, 300 nodes and 16 levels of nesting per badge.

A badge that fails is left out, and the rest show. A wall that fails is never shown: your server keeps its last good copy until that copy expires. The package's own CSS is one style element and a style attribute on the wall's outer element, which repeats the wall's containment (position: relative, overflow: hidden, contain: paint, isolation: isolate), so no badge draws outside the wall even where a Content-Security-Policy blocks the style element. The only styles that come from our servers are the badges' inline styles, checked as above.

What the package adds to a badge, and nothing else: loading="lazy" on images that do not set it, a directory badge's name as alt where its image has none, tabindex="-1" on the links of the strip's hidden repeat, and a span around badges you add yourself. It never changes a link's or an image's address, or a link's rel or target.

The full rules, with every limit, are in CONTRACT.md in the repository.

Your project ID

The package needs one value: your project ID. It is public on purpose: it already sits in the link of your Featured on Submitator badge, and anyone can read your badges with it, which is the point of a badge. It cannot change your project, see your account or spend anything, so there is no key to hide, rotate or revoke.

Configure Badges in the app shows the snippet for your framework with your ID filled in.

Open source

The package is MIT licensed and has no dependencies: React, Vue, Svelte and Astro come from your own project. Each TypeScript file in the repository's src folder is compiled to one readable JavaScript file in dist, with no bundling and no minifying, and the Svelte and Astro components ship as their own source files, so you can read every file you install in your node_modules. Read the code on GitHub, or the exact files you would install from npm.

Install

Put the badges on your homepage, usually in its footer: that is the page directories check. Replace YOUR_PROJECT_ID with your ID, or copy the snippet from Configure Badges in the app, where it is filled in.

Terminal
npm i @submitator/badges

Next.js

App Router

app/page.tsxTSX
// app/page.tsx: your homepage, a Server Component (no "use client")import { SubmitatorBadges } from "@submitator/badges/react"; export default function Home() {  return (    <footer>      <SubmitatorBadges project="YOUR_PROJECT_ID" />    </footer>  );}

Next.js 16 with cacheComponents

app/badges.tsxTSX
// app/badges.tsx: render <Badges /> in your homepage footerimport { cacheLife } from "next/cache";import { SubmitatorBadges } from "@submitator/badges/react"; export async function Badges() {  "use cache";  cacheLife("minutes");  return <SubmitatorBadges project="YOUR_PROJECT_ID" />;}

Pages Router

pages/index.tsxTSX
// pages/index.tsximport { fetchWall, type Wall } from "@submitator/badges";import { BadgeWall } from "@submitator/badges/react"; export async function getStaticProps() {  return { props: { wall: await fetchWall({ project: "YOUR_PROJECT_ID" }) }, revalidate: 60 };} export default function Home({ wall }: { wall: Wall | null }) {  return (    <footer>      <BadgeWall wall={wall} />    </footer>  );}

Nuxt

server/api/submitator-badges.get.tsTypeScript
// server/api/submitator-badges.get.tsimport { fetchWall } from "@submitator/badges"; export default defineEventHandler(async () => ({  wall: await fetchWall({ project: "YOUR_PROJECT_ID", framework: "nuxt" }),}));
components/SubmitatorBadges.vueVue
<!-- components/SubmitatorBadges.vue: render it in your homepage footer --><script setup lang="ts">import { BadgeWall } from "@submitator/badges/vue"; const { data } = await useFetch("/api/submitator-badges");</script> <template>  <BadgeWall v-if="data?.wall" :wall="data.wall" /></template>

SvelteKit

src/routes/+layout.server.tsTypeScript
// src/routes/+layout.server.ts (merge into your load if you have one)import { fetchWall } from "@submitator/badges"; export async function load() {  return { wall: await fetchWall({ project: "YOUR_PROJECT_ID", framework: "sveltekit" }) };}
src/routes/+layout.svelteSvelte
<!-- src/routes/+layout.svelte --><script lang="ts">  import { BadgeWall } from "@submitator/badges/svelte";   let { data, children } = $props();</script> {@render children()} <footer>  {#if data.wall}    <BadgeWall wall={data.wall} />  {/if}</footer>

Astro

src/components/Footer.astroAstro
---// src/components/Footer.astroimport { SubmitatorBadges } from "@submitator/badges/astro";--- <footer>  <SubmitatorBadges project="YOUR_PROJECT_ID" /></footer>

Note: Astro prerenders pages by default, so the badges change only when you rebuild. For live badges add export const prerender = false to src/pages/index.astro (needs a server adapter). Never wrap the badges in a server island (server:defer): it loads by script, and directories never see it.

Any other Node server

JavaScript
import { fetchWall, wallToHtml } from "@submitator/badges"; const wall = await fetchWall({ project: "YOUR_PROJECT_ID" });const badgesHtml = wallToHtml(wall); // "" when there is no wall; every value is escaped// Print badgesHtml inside your footer, in the HTML your server sends.

Still row instead of the scrolling strip. Add layout="row" to the component (or pass { layout: "row" } to wallToHtml).

Check your install

Terminal
curl -sL 'https://your-site.com' | grep -cE 'data-sbm-ref="?YOUR_PROJECT_ID'

1 or more means your badges are in the HTML your server sends, which is what directories read. 0 means they are not, however right the page looks in a browser.

One exception: the check counts the marker only in double quotes or none. If an HTML minifier rewrote it as data-sbm-ref='...', the check finds 0 although your badges are there.

Caches, CDNs and Content-Security-Policy

Directories read the HTML your homepage serves, so whatever keeps a copy of that HTML decides how soon new badges show.

Static builds. Astro's default output, Next.js with output: "export", SvelteKit pages with prerender and nuxi generate fetch the badges when they build, and only then. Render your homepage on request, or rebuild after you add directories.

CDN and page caches. If a CDN or a page cache stores your homepage (Cloudflare's Cache Everything, a long edge TTL, a caching plugin), keep that copy for 60 seconds or less, or purge it after you add directories. curl -sI 'https://your-site.com' shows the age and cache-control headers of the copy it serves.

Content-Security-Policy. img-src must allow https:, because badge images load from each directory's site, and http: too if a directory serves its image over http. The wall uses style attributes and one style element: allow both with 'unsafe-inline' in style-src, or keep style-src strict, add style-src-attr 'unsafe-inline' and give the package your nonce with its nonce option. Nothing changes for script-src: the package adds no script.

Questions

Do I need an API key?

No. The package reads your badges with your public project ID, the one already in your badge link. There is nothing secret to store on your server.

What happens to my site if Submitator is down?

Nothing breaks. Your server keeps showing its last good copy of your badges, for up to 7 days after we signed it. A server that has no copy yet, or only an expired one, waits 2 seconds at most by default, then renders the page without them.

Will it slow my pages down?

Pages render from the copy your server keeps in memory, and the refresh runs in the background, every 60 seconds by default. Only a server with no copy yet, or an expired one, waits for our answer: 2 seconds at most by default. The package adds no script tag.

My site is not on Next.js, Nuxt, SvelteKit or Astro. Can I still use it?

On any other server running Node.js 20.9 or later, yes: wallToHtml gives you escaped HTML to print in your footer. Other backends, such as PHP, Rails or Python, fetch your badges from the public endpoint as HTML or JSON and print them; Configure Badges in the app has ready snippets.

How do I check that it works?

Deploy, then run curl -sL 'https://your-site.com' | grep -cE 'data-sbm-ref="?YOUR_PROJECT_ID' with your own address and project ID. 1 or more means your badges are in the HTML your server sends, which is what directories read. 0 means they are not, however right the page looks in a browser. One exception: the check counts the marker only in double quotes or none. If an HTML minifier rewrote it as data-sbm-ref='...', the check finds 0 although your badges are there.

Something else, or a security review for your team? Email vlad@submitator.com.

Last updated