Skill: Link Shortener

## Overview

7 stars

Best use case

Skill: Link Shortener is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

## Overview

Teams using Skill: Link Shortener should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/link-shortener/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/web-applications/link-shortener/skills/link-shortener/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/link-shortener/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How Skill: Link Shortener Compares

Feature / AgentSkill: Link ShortenerStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

## Overview

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Skill: Link Shortener

## Overview

Core domain skill for the Link Shortener service. Covers Base62 code generation, short-link lifecycle, click recording, analytics aggregation, and all associated API routes.

## Base62 Code Generation

```ts
// api/src/lib/base62.ts
import crypto from 'node:crypto';

const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const BASE = ALPHABET.length; // 62

export function generateCode(length = 7): string {
  const bytes = crypto.randomBytes(length * 2); // over-provision to avoid modulo bias
  let result = '';
  let i = 0;
  while (result.length < length) {
    const byte = bytes[i++];
    if (byte < BASE * Math.floor(256 / BASE)) {
      result += ALPHABET[byte % BASE];
    }
  }
  return result;
}
```

Collision probability for 7-char Base62 codes with 10,000 links: ~1 in 2 billion. Handle `UNIQUE` constraint violations by retrying up to 5 times.

## URL Validation

```ts
// api/src/lib/validate.ts
const ALLOWED_SCHEMES = ['http:', 'https:'];

export function isValidUrl(raw: string): boolean {
  try {
    const url = new URL(raw);
    return ALLOWED_SCHEMES.includes(url.protocol);
  } catch {
    return false;
  }
}

export function isValidSlug(slug: string): boolean {
  return /^[a-zA-Z0-9_-]{3,64}$/.test(slug);
}
```

Rejected schemes: `javascript:`, `data:`, `ftp:`, `file:`, `vbscript:`.

## IP Hashing

```ts
// api/src/lib/hash.ts
import crypto from 'node:crypto';

export function hashIp(ip: string): string {
  return crypto.createHash('sha256').update(ip).digest('hex').slice(0, 16);
}
```

## Database Queries

### Create link (with retry on collision)

```ts
function createLink(
  db: Database,
  userId: number,
  longUrl: string,
  title: string | null,
  customSlug: string | null,
  expiresAt: string | null,
  codeLength: number,
): Link {
  const MAX_RETRIES = 5;
  const code = customSlug ?? generateCode(codeLength);

  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
    const tryCode = attempt === 0 ? code : generateCode(codeLength);
    try {
      const stmt = db.prepare(`
        INSERT INTO links (user_id, code, long_url, title, expires_at)
        VALUES (?, ?, ?, ?, ?)
        RETURNING *
      `);
      return stmt.get(userId, tryCode, longUrl, title, expiresAt) as Link;
    } catch (err: unknown) {
      const e = err as NodeJS.ErrnoException;
      if (e.message?.includes('UNIQUE constraint failed') && !customSlug) continue;
      throw err;
    }
  }
  throw new Error('Failed to generate a unique code after 5 attempts');
}
```

### List links with click counts

```sql
SELECT
  l.*,
  COUNT(c.id) AS click_count
FROM links l
LEFT JOIN clicks c ON c.link_id = l.id
WHERE l.user_id = ?
  AND (? IS NULL OR l.code LIKE '%' || ? || '%' OR l.title LIKE '%' || ? || '%')
GROUP BY l.id
ORDER BY l.created_at DESC
LIMIT ? OFFSET ?
```

### Redirect and click recording

```ts
function handleRedirect(db: Database, code: string, req: Request, res: Response): void {
  const link = db.prepare('SELECT * FROM links WHERE code = ?').get(code) as Link | undefined;

  if (!link) {
    res.status(404).send('Not found');
    return;
  }

  if (!link.is_active) {
    res.status(410).send('Gone');
    return;
  }

  if (link.expires_at && new Date(link.expires_at) < new Date()) {
    res.status(410).send('Gone');
    return;
  }

  const ip = (req.headers['x-forwarded-for'] as string ?? req.ip ?? '').split(',')[0].trim();
  db.prepare(`
    INSERT INTO clicks (link_id, ip_hash, user_agent, referrer)
    VALUES (?, ?, ?, ?)
  `).run(link.id, hashIp(ip), req.get('user-agent') ?? null, req.get('referer') ?? null);

  res.redirect(302, link.long_url);
}
```

## Analytics Aggregation

```sql
-- Click time series (last N days)
SELECT
  date(clicked_at) AS date,
  COUNT(*) AS clicks
FROM clicks
WHERE link_id = ?
  AND clicked_at >= datetime('now', '-' || ? || ' days')
GROUP BY date(clicked_at)
ORDER BY date ASC

-- Top referrers
SELECT
  COALESCE(referrer, 'Direct / None') AS referrer,
  COUNT(*) AS count
FROM clicks
WHERE link_id = ?
GROUP BY referrer
ORDER BY count DESC
LIMIT 5

-- Unique IPs (hashed)
SELECT COUNT(DISTINCT ip_hash) AS unique_ips
FROM clicks
WHERE link_id = ?
```

### User-agent parsing

```ts
function parseUserAgent(ua: string | null): string {
  if (!ua) return 'Unknown';
  if (/Chrome\//.test(ua) && !/Chromium|Edg\//.test(ua)) {
    return /Android/.test(ua) ? 'Chrome / Android' : 'Chrome / macOS';
  }
  if (/Firefox\//.test(ua)) return 'Firefox / Win';
  if (/Safari\//.test(ua) && !/Chrome/.test(ua)) {
    return /iPhone|iPad/.test(ua) ? 'Safari / iOS' : 'Safari / macOS';
  }
  if (/Edg\//.test(ua)) return 'Edge / Win';
  return 'Other';
}
```

## API Route Registration

```ts
// api/src/app.ts
app.use('/api/auth',    authRouter);
app.use('/api/links',   requireAuth, linksRouter);
app.use('/api/links',   requireAuth, analyticsRouter);
app.use('/api/links',   requireAuth, qrRouter);
app.use('/',           redirectRouter);   // must be last - catches /:code
```

Rate limiters:
```ts
const redirectLimiter = rateLimit({ windowMs: 60_000, max: 60, standardHeaders: true });
const createLimiter   = rateLimit({ windowMs: 60_000, max: 20, standardHeaders: true });
redirectRouter.use(redirectLimiter);
linksRouter.post('/', createLimiter);
```

## TypeScript Types

```ts
// api/src/types.ts
export interface Link {
  id: number;
  user_id: number;
  code: string;
  long_url: string;
  title: string | null;
  expires_at: string | null;
  is_active: number;   // 1 | 0
  created_at: string;
  updated_at: string;
}

export interface Click {
  id: number;
  link_id: number;
  clicked_at: string;
  ip_hash: string | null;
  user_agent: string | null;
  referrer: string | null;
  country: string | null;
}

export interface AnalyticsResponse {
  linkId: number;
  totalClicks: number;
  uniqueIps: number;
  timeSeries: Array<{ date: string; clicks: number }>;
  topReferrers: Array<{ referrer: string; count: number }>;
  topUserAgents: Array<{ label: string; count: number }>;
}
```

## pnpm Commands

```bash
pnpm install              # install all workspace deps
pnpm --filter api dev     # run API server with ts-node-dev
pnpm --filter web dev     # run Vite dev server
pnpm --filter api test    # run Vitest for API
pnpm --filter api build   # compile TypeScript
pnpm --filter web build   # Vite production build
pnpm typecheck            # tsc --noEmit in all packages
pnpm lint                 # ESLint in all packages
```

Related Skills

expiring-links

7
from heldernoid/agentic-build-templates

Create and manage time-limited, count-limited, and password-protected share links for uploaded files.

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.

Skill: personal-finance

7
from heldernoid/agentic-build-templates

## Overview

Skill: csv-import

7
from heldernoid/agentic-build-templates

## Overview

Skill: Syntax Highlighting

7
from heldernoid/agentic-build-templates

## Purpose