██╗   ██╗███████╗██████╗ ███████╗██╗   ██╗ ██████╗ ██╗██████╗
██║   ██║██╔════╝██╔══██╗██╔════╝██║   ██║██╔═══██╗██║██╔══██╗
██║   ██║█████╗  ██████╔╝█████╗  ██║   ██║██║   ██║██║██████╔╝
╚██╗ ██╔╝██╔══╝  ██╔══██╗██╔══╝  ╚██╗ ██╔╝██║   ██║██║██╔══██╗
 ╚████╔╝ ███████╗██║  ██║███████╗ ╚████╔╝ ╚██████╔╝██║██║  ██║
  ╚═══╝  ╚══════╝╚═╝  ╚═╝╚══════╝  ╚═══╝   ╚═════╝ ╚═╝╚═╝  ╚═╝

QR Codes & Link Tracking

@verevoir/qr generates vector SVG QR codes with multiple visual styles. @verevoir/link-tracking shortens URLs, resolves short codes, and tracks clicks. Both are zero-dependency and standalone — use them together or independently.

Installation

npm install @verevoir/qr @verevoir/link-tracking

Both packages have zero runtime dependencies. Neither depends on any other @verevoir package.

Overview

The two packages serve complementary roles:

A typical flow: shorten a URL, encode the short URL as a QR code, print it. When someone scans the code, the short URL resolves to the target and the click is recorded.

Step 1 — Generate a QR Code

The simplest use case — encode text and render as SVG:

import { encode, toSvg } from '@verevoir/qr';

const results = encode('https://example.com');
const svg = toSvg(results[0], { style: 'square' });

// Write to a file
import { writeFileSync } from 'fs';
writeFileSync('qr.svg', svg);

encode() returns an array of QrResult objects — one per mask variant above the quality threshold. The first has the lowest penalty score (most technically optimal), but any candidate is valid. Choose the one that looks best with your chosen style.

Step 2 — Choose a Visual Style

Ten SVG rendering styles are available:

import { encode, toSvg } from '@verevoir/qr';
import type { SvgStyle, CornerStyle } from '@verevoir/qr';

const qr = encode('https://example.com')[0];

// Each style renders the same data differently
toSvg(qr, { style: 'square' });       // filled rectangles (default)
toSvg(qr, { style: 'dots' });         // dark + light circles
toSvg(qr, { style: 'horizontal' });   // horizontal line segments
toSvg(qr, { style: 'vertical' });     // vertical line segments
toSvg(qr, { style: 'diagonal' });     // diagonal line segments
toSvg(qr, { style: 'grid' });         // connected regions as outline paths
toSvg(qr, { style: 'lines' });        // diagonal-first tubemap-style paths
toSvg(qr, { style: 'metro' });        // horizontal over vertical over diagonal layers
toSvg(qr, { style: 'scribble' });     // diagonal zigzag with bezier-smoothed turns
toSvg(qr, { style: 'scribble-alt' }); // horizontal zigzag with angular turns

Corner styles affect the finder and alignment patterns:

toSvg(qr, { style: 'dots', cornerStyle: 'square' });  // sharp corners
toSvg(qr, { style: 'dots', cornerStyle: 'rounded' }); // rounded stroke
toSvg(qr, { style: 'dots', cornerStyle: 'round' });   // circular

Line width applies to the horizontal, vertical, and diagonal styles:

toSvg(qr, { style: 'horizontal', lineWidth: 'normal' }); // default
toSvg(qr, { style: 'horizontal', lineWidth: 'thin' });   // thinner strokes

Step 3 — Fabrication Layers

The layers option emits dark, light, and background modules as separate SVG groups:

const svg = toSvg(qr, { style: 'dots', layers: true });
// Contains:
//   <g id="background">...</g>
//   <g id="dark">...</g>
//   <g id="light">...</g>

Each layer can be exported independently for:

Step 4 — PNG Export (Browser)

In the browser, convert SVG to PNG via canvas:

import { encode, toSvg, svgToPng, downloadPng } from '@verevoir/qr';

const svg = toSvg(encode('https://example.com')[0]);

// Get a Blob
const blob = await svgToPng(svg, { size: 1024 });

// Or trigger a download
await downloadPng(svg, { size: 1024, filename: 'qr-code.png' });

PNG export uses native browser APIs (canvas, Image) — no additional dependencies. It is not available in Node.js.

Step 5 — Shorten and Track URLs

Create a tracker with any store that implements create, list, update:

import { createTracker } from '@verevoir/link-tracking';

const tracker = createTracker({
  store: myStore,          // StorageAdapter or any compatible store
  baseUrl: 'https://example.short',
});

Shorten a URL and get a trackable link:

const link = await tracker.shorten('https://example.com/long-page');
// link.shortCode — e.g. "a1b2c3"
// Full URL: https://example.short/a1b2c3

The TrackerStore interface is structurally typed — it is compatible with @verevoir/storage StorageAdapter without importing it:

import { PostgresAdapter } from '@verevoir/storage';

const storage = new PostgresAdapter({ connectionString: '...' });
await storage.connect();

const tracker = createTracker({
  store: storage,
  baseUrl: 'https://example.short',
});

Step 6 — QR + Short Links Together

The typical integration — shorten a URL, then encode the short URL as a QR code:

import { encode, toSvg } from '@verevoir/qr';
import { createTracker } from '@verevoir/link-tracking';

const tracker = createTracker({ store: myStore, baseUrl: 'https://vrev.io' });

// Shorten the target URL
const link = await tracker.shorten('https://example.com/event-registration');

// Encode the short URL as a QR code
const shortUrl = `https://vrev.io/${link.shortCode}`;
const svg = toSvg(encode(shortUrl)[0], {
  style: 'dots',
  cornerStyle: 'round',
});

// The QR code is simpler because the URL is shorter
// When scanned, the click is tracked before redirecting

Step 7 — Resolution Endpoint

The tracker provides resolve() but does not include an HTTP layer. Wire it into your routing:

Next.js API Route

// app/r/[code]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { tracker } from '@/lib/tracker';

export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ code: string }> },
) {
  const { code } = await params;
  const targetUrl = await tracker.resolve(code);

  if (!targetUrl) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }

  return new NextResponse(null, {
    status: 302,
    headers: {
      Location: targetUrl,
      'Cache-Control': 'public, max-age=86400',
    },
  });
}

Express

app.get('/r/:code', async (req, res) => {
  const targetUrl = await tracker.resolve(req.params.code);
  if (!targetUrl) return res.status(404).json({ error: 'Not found' });
  res.set('Cache-Control', 'public, max-age=86400');
  res.redirect(302, targetUrl);
});

Use 302 (temporary redirect) so browsers always request the CDN. The CDN caches the redirect; every request is still logged for analytics.

Step 8 — Record Clicks and View Stats

Record click metadata when a short link is resolved:

await tracker.recordClick('a1b2c3', {
  referrer: request.headers.get('referer'),
  userAgent: request.headers.get('user-agent'),
  ip: request.headers.get('x-forwarded-for'),
});

For high-volume services, process CDN access logs in batch:

await tracker.recordClicks([
  { code: 'a1b2c3', metadata: { referrer: '...', ip: '...' } },
  { code: 'x9y8z7', metadata: { referrer: '...', ip: '...' } },
]);

View analytics:

const stats = await tracker.getStats('a1b2c3');
// {
//   totalClicks: 847,
//   clicksByDay: { '2026-03-10': 312, '2026-03-11': 535 },
//   uniqueReferrers: ['https://google.com', 'https://twitter.com'],
// }

Step 9 — Custom Aliases and Link Expiry

// Vanity short code
const link = await tracker.shorten('https://example.com/pricing', {
  alias: 'pricing',
});
// Resolves at https://vrev.io/pricing

// Link with expiry
const promo = await tracker.shorten('https://example.com/flash-sale', {
  expiresAt: new Date('2026-06-30'),
});
// resolve() returns null after expiry

Putting It Together

A full example — storage adapter, link tracking, QR generation, and a resolution endpoint:

import { PostgresAdapter } from '@verevoir/storage';
import { createTracker } from '@verevoir/link-tracking';
import { encode, toSvg } from '@verevoir/qr';

// Storage
const storage = new PostgresAdapter({ connectionString: process.env.DATABASE_URL! });
await storage.connect();

// Tracker
const tracker = createTracker({
  store: storage,
  baseUrl: 'https://vrev.io',
});

// Create a short link with a QR code
async function createQrLink(targetUrl: string) {
  const link = await tracker.shorten(targetUrl);
  const shortUrl = `https://vrev.io/${link.shortCode}`;
  const svg = toSvg(encode(shortUrl)[0], {
    style: 'dots',
    cornerStyle: 'rounded',
  });
  return { link, svg };
}

// Resolve and track
async function resolveLink(code: string, request: Request) {
  const targetUrl = await tracker.resolve(code);
  if (!targetUrl) return null;

  await tracker.recordClick(code, {
    referrer: request.headers.get('referer'),
    userAgent: request.headers.get('user-agent'),
  });

  return targetUrl;
}

Further Reading