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:
- QR — encode text into QR matrices, render as vector SVG in ten visual styles, export as PNG in the browser
- Link Tracking — shorten URLs to trackable short codes, record click metadata, compute analytics
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:
- 3D printing — dark layer as raised surface, light layer as recessed
- Laser cutting — separate paths for cut and engrave
- CNC engraving — positive and negative toolpaths
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
- Integration Guide — connecting content models, storage, editor, and more
- Commerce — products, baskets, orders, payments
- Bookings — calendars, availability, holds, confirmed bookings