vite-plugin-whereami
v0.7.0
Published
Tints your favicon and prefixes the page title per environment (dev/staging/prod), plus an optional build-info banner in <head> and the browser console.
Maintainers
Readme
vite-plugin-whereami
Tints your favicon and prefixes the page title per environment, so you never
mistake a staging tab for prod again — plus an optional build-info banner
(name/version/environment) in <head> and the browser console, and an
on-screen corner badge for build info that's always visible, custom
metadata included.
Inspired by this tip about using a different favicon per environment to keep tabs visually distinct.

Install
bun add -D vite-plugin-whereamiOr, let an AI coding agent do it
Paste this into Claude Code, OpenCode, Cursor, Copilot, or whatever agent you're using:
Install and configure vite-plugin-whereami (https://www.npmjs.com/package/vite-plugin-whereami)
in this project:
1. Add it as a dev dependency with this project's package manager.
2. Wire it up for this project's framework:
- Plain Vite: add `whereami()` to the `plugins` array in vite.config.ts/js.
- SvelteKit: add `whereami()` to the `plugins` array in vite.config.ts (after
`sveltekit()`), and add `whereamiHandle()` to `src/hooks.server.ts`, fed with
what the plugin resolved at build time so it works on edge runtimes:
`import build from "vite-plugin-whereami/build";`
`export const handle = whereamiHandle({ ...build });`
(use `sequence()` from `@sveltejs/kit/hooks` if there are other handlers).
See https://github.com/mastermakrela/whereami#sveltekit.
3. Check how/where this project deploys (vercel.json, .vercel/, wrangler.toml or
wrangler.jsonc, CI/CD workflow files). Vercel and Cloudflare Pages are
auto-detected out of the box, so usually nothing else is needed. If it deploys
somewhere else, or the default prod/staging/dev mapping doesn't match this
project's branches/environments, add a custom `detect` option per
https://github.com/mastermakrela/whereami#environment-detection.
4. Leave favicon tinting, the build-info banner, and the on-screen badge at their
defaults — don't enable extra options unless asked. Full reference:
https://github.com/mastermakrela/whereamiQuick start
// vite.config.ts
import { defineConfig } from "vite";
import whereami from "vite-plugin-whereami";
export default defineConfig({
plugins: [whereami()],
});That's it. By default:
- prod (
vite build, i.e.mode: "production") — no changes at all. - staging (
vite build --mode staging) — 🟠 orange favicon tint +🟠title prefix. - dev (
vite dev, or any other/unknown mode) — 🟢 green favicon tint +🟢title prefix.
Unknown modes default to dev's tint on purpose — the whole point is to make it
obvious when you're not looking at production, so an unrecognized mode should
look suspicious, not blend in.
If your project already has a favicon (<link rel="icon"> in index.html,
public/favicon.{svg,png}, SvelteKit's static/favicon.{svg,png}, or the
src/lib/assets/favicon.{svg,png} path the sv create scaffold uses), it
gets tinted (hue/saturation swapped for the environment color, lightness
preserved — a shape-preserving recolor, not a flat overlay). If there's none,
a simple icon is generated using the first letter of your package.json
name.
Environment detection
By default, whereami checks, in order:
- the
WHEREAMI_ENVenvironment variable, if set - a few popular hosting platforms' own environment variables:
- Vercel —
VERCEL_ENV:production→prod,preview→staging,development→dev - Cloudflare Pages —
CF_PAGES/CF_PAGES_BRANCH: themain/masterbranch →prod, any other branch →staging - Node —
NODE_ENV=production→prod
- Vercel —
- Vite's
mode:"production"→prod,"staging"→staging, anything else →dev
You can fully replace this with your own function — return any string key,
matched against environments:
whereami({
detect: ({ mode, env }) => {
// https://developers.cloudflare.com/pages/configuration/build-configuration/#environment-variables
if (env.CF_PAGES === "1") {
return env.CF_PAGES_BRANCH === "main" ? "prod" : "staging";
}
return mode === "production" ? "prod" : "dev";
},
});Custom environments
whereami({
environments: {
prod: {}, // no color/titlePrefix = no changes
staging: { color: "#f59e0b", titlePrefix: "🟠 [staging] " },
dev: { color: "#22c55e", titlePrefix: "🟢 [dev] " },
qa: { color: "#a855f7", titlePrefix: "🟣 [qa] " },
},
detect: ({ mode }) => (mode === "qa" ? "qa" : undefined) ?? "prod",
});Any environment whose config has no color gets no favicon change, and no
titlePrefix gets no title change — this is how prod stays a no-op by
default without special-casing it in the plugin itself.
Favicon options
whereami({
favicon: {
enabled: true, // set false to disable favicon tinting/generation entirely
path: "public/logo.svg", // explicit source; auto-detected otherwise
},
});Only .svg and .png sources are supported. .ico (or anything else) falls
back to generating the default letter icon, with a console warning.
Build-info banner
Injects <meta> tags and a styled console.log with your package's name,
version, and detected environment — in every environment, including prod,
unless you disable it:
whereami({
banner: {
enabled: true, // master switch
meta: true, // <meta name="app-name" content="..."> etc.
console: true, // the console.log banner
metaPrefix: "app", // -> app-name, app-version, app-environment
},
});
// or shorthand to disable everything:
whereami({ banner: false });On-screen badge
A small colored triangle, fixed to the bottom-left corner, in every non-prod
environment (same "no color, no badge" rule as the favicon — prod stays a
no-op by default). Click it to open a panel with the same info as the console
banner, in a <pre> block:

Dismiss it by clicking the × button, clicking anywhere outside the panel, or pressing Escape.
whereami({ badge: { enabled: true } });
// or shorthand to disable it:
whereami({ badge: false });Served SVG badge (SvelteKit)
whereamiHandle() can also serve a small SVG status badge at a fixed URL, so
a GitLab/GitHub project badge can point at your running deployment and show
the live version — no third-party badge service, no CI step keeping a badge
image in sync:
whereamiHandle({
...build,
badgeEndpoint: true, // or, with the defaults spelled out:
// badgeEndpoint: {
// path: "/_whereami/badge.svg",
// label: "staging", // default: the environment key
// show: ["version"], // also: "name", "environment"
// color: "#0b7285", // default: the environment's color, else this teal
// },
});Point a GitLab project badge at it under Settings → General → Badges:
Badge image URL: https://your-app.example.com/_whereami/badge.svg
Link: https://your-app.example.comThis endpoint is public and unauthenticated — anyone who knows (or
guesses) the path can request it, with no way to gate access here. It
deliberately renders only the fields listed in show; it never exposes
metadata, environment variables, or anything else. Responses are served
with cache-control: no-cache and an ETag (so repeat requests get a cheap
304) — the whole point is that the badge always reflects what's currently
deployed, not a cached snapshot.
It only exists on whereamiHandle(), not the Vite plugin: a plain Vite app
is just static files in production, with no server left at request time to
answer this request.
Custom metadata
Anything you pass to metadata shows up in both the console banner (as a
second, inspectable console.log) and the badge's panel, merged with the
built-in name/version/environment (which always win on a key clash):
whereami({
metadata: {
region: process.env.FLY_REGION,
commit: process.env.GITHUB_SHA?.slice(0, 7),
},
});Values must be JSON-serializable — they're embedded into the injected script at build/dev-server-start time, not read at request time.
SvelteKit
SvelteKit renders pages through its own SSR pipeline and never calls Vite's
transformIndexHtml hook, so the injection happens in the
vite-plugin-whereami/sveltekit entry point instead, through SvelteKit's own
transformPageChunk in hooks.server.ts. The whereami() Vite plugin still
belongs in vite.config.ts: it resolves what is only knowable while building
and hands it over through vite-plugin-whereami/build:
// vite.config.ts
plugins: [sveltekit(), whereami()];
// src/hooks.server.ts
import { sequence } from "@sveltejs/kit/hooks";
import build from "vite-plugin-whereami/build";
import { whereamiHandle } from "vite-plugin-whereami/sveltekit";
export const handle = sequence(whereamiHandle({ ...build }) /* ...your other handlers */);build holds:
environment— whatdetectreturned at build time, where the CI's env vars are (CF_PAGES_BRANCH,VERCEL_ENV, your own deploy-target variable) andvite buildmeans production. A deployed Worker has none of them at request time, and a compiled binary often has noNODE_ENV. AWHEREAMI_ENVset at request time still overrides it, so one artifact deployed to several environments can name each one. Your own deploy-target variable goes in the plugin'sdetect:whereami({ detect: ({ env }) => ({ production: "prod", development: "staging" })[env.DEPLOY_ENV] ?? "dev", });pkg—package.json's name and version, with the commit (CI_COMMIT_SHORT_SHA, elsegit rev-parse --short HEAD) as semver build metadata:1.4.0+abc1234.
The values are inlined into your hooks.server.ts — always part of the server
bundle — so they need no filesystem access at request time, including on
edge/isolate runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy). (The
plugin can't hand them to whereamiHandle() directly: Vite leaves packages in
node_modules out of the server bundle it processes.) The import only
resolves with the plugin installed. Without it, call whereamiHandle() without
build: it detects the environment at request time and reads package.json
off disk — fine on Node; on an edge runtime it logs a one-time warning and
labels the app [email protected]. You can also pass environment and pkg yourself.
Next to SvelteKit the plugin does nothing else, so SvelteKit doesn't warn about
it.
The handle itself is runtime-agnostic either way: it never statically imports a
Node builtin (or Buffer, or process), so a Cloudflare Workers deploy needs
no nodejs_compat compatibility flag to use it. Node's fs is loaded
lazily, only on runtimes that have it, purely to serve that package.json
fallback and to read a favicon off disk.
whereamiHandle() takes the same options as the Vite plugin, with two other
differences:
- There's no Vite
mode/commandat request time (this runs in the already- built, deployed server), so the default detector trustsNODE_ENV=productionoutright instead of only duringserve— theWHEREAMI_ENVenv var and the Vercel/Cloudflare Pages checks work exactly the same way. - The favicon is inlined as a
data:URI in the<link>tag rather than served from a separate emitted file, since there's no Vite build pipeline to emit assets into at request time. A customfavicon.pathis still unreachable on edge runtimes without a filesystem; the generated letter icon is used instead, correctly once the name is resolved.
An icon declared in <svelte:head> (the default in sv create scaffolds) and
a titlePrefix both keep working across hydration: the existing icon <link>
is left in place — renamed to data-whereami-rel rather than removed — so
Svelte's head-hydration cursor never lands on a missing node, and the title
prefix is re-applied client-side by a small script, since Svelte's
compiled <title> sets document.title again once the page hydrates.
Content-Security-Policy
whereamiHandle() injects no inline JavaScript. The title keeper, console
banner and badge are served together as one external file at
/_whereami/whereami.js (with an ETag and cache-control: no-cache), loaded
by a single <script src="/_whereami/whereami.js" defer> in the <head> — so
they work under script-src 'self' or SvelteKit's nonce-based csp config, with
no nonce and no 'unsafe-inline'. If none of the three is enabled, no script
tag is injected and the path isn't served.
Like the badge endpoint, that path is public and answered before your other
handlers run, so whatever you pass as metadata is readable without logging
in. Two limitations: apps with paths.base can't load it yet (SvelteKit
404s paths outside base before any hook runs), and a fully static or
prerendered site has no server left to answer it.
Full options reference
interface WhereAmIOptions {
detect?: (ctx: {
mode: string;
command: "build" | "serve";
env: Record<string, string>;
}) => string;
environment?: string; // replaces the default detection; WHEREAMI_ENV still wins, a custom detect replaces it
environments?: Record<string, { color?: string; titlePrefix?: string }>;
favicon?: { enabled?: boolean; path?: string };
banner?: boolean | { enabled?: boolean; meta?: boolean; console?: boolean; metaPrefix?: string };
badge?: boolean | { enabled?: boolean };
metadata?: Record<string, unknown>; // must be JSON-serializable
packageJsonPath?: string; // default: "package.json"
pkg?: { name: string; version: string }; // bypasses packageJsonPath/fs when set
badgeEndpoint?:
| true // all defaults
| {
path?: string; // default: "/_whereami/badge.svg"
label?: string; // default: the environment key
show?: Array<"name" | "version" | "environment">; // default: ["version"]
color?: string; // default: the environment's color, else "#0b7285"
}; // SvelteKit only, see "Served SVG badge" above
}Development
bun install
bun run test # vitest (unit + integration against fixture vite projects)
bun run build # tsup -> dist/
bun run typecheck
bun run lint # oxlint
bun run format:check # oxfmt --checkplayground-app/ is a throwaway Vite project (gitignored) useful for manual
checks:
bun run playground # starts a dev server against playground-app
bun run playground:build # runs a production build of playground-appBoth rebuild the plugin first, so changes under src/ show up on the next run.
To try a different environment, pass --mode, e.g.
vite build playground-app --mode staging (or set WHEREAMI_ENV=staging).
Releasing
This repo uses Changesets for
versioning/changelogs and npm's OIDC trusted publishing
for the actual npm publish — no NPM_TOKEN secret needed, as long as this
package's Trusted Publisher on npmjs.com is configured to point at this
GitHub repo and the Release workflow.
- Run
bun run changesetto describe your change (patch/minor/major + summary). - Commit the generated
.changeset/*.mdfile and push tomain. - The
Releaseworkflow bumps the version and updatesCHANGELOG.md(committing that straight back tomain, no PR/review step), then publishes to npm withnpm publish(the one command that actually supports trusted publishing — bun/pnpm don't yet) — all in the same run.
License
MIT
