Link Organization with Tags
Build scalable link organization systems using tags, metadata, and smart filtering. Learn tagging strategies that work for teams managing thousands of short links across multiple campaigns, departments, and projects.
What You'll Learn
This tutorial covers complete link organization from planning tag taxonomies to implementing automated tagging workflows and building advanced search tools.
Why Link Organization Matters
As your link library grows, organization becomes critical:
- Scale - Manage 10,000+ links across campaigns, products, teams
- Discovery - Find specific links in seconds, not minutes
- Reporting - Aggregate analytics by campaign, department, or product
- Cleanup - Identify and remove outdated or unused links
- Collaboration - Teams know where to find shared resources
- Compliance - Track links by project for audits
Tags vs Metadata
SnipLink provides two organization tools:
- Tags - Array of strings (max 10 tags per link, 50 chars each)
- Metadata - Key-value pairs (unlimited, any JSON-compatible data)
// Tags: For categorization and filtering
{
tags: ['marketing', 'q4-2025', 'email-campaign', 'conversion-tracked']
}
// Metadata: For custom attributes and data
{
metadata: {
campaignId: 'camp_12345',
owner: 'marketing@company.com',
budget: 50000,
targetAudience: 'enterprise',
priority: 'high',
conversionGoal: 'signup'
}
}Designing Tag Taxonomies
Multi-Dimensional Tagging
Use tags to categorize links across multiple dimensions:
// Dimension 1: Department/Team
const departmentTags = [
'marketing',
'sales',
'support',
'product',
'engineering',
];
// Dimension 2: Campaign/Project
const campaignTags = [
'q1-2025',
'black-friday',
'product-launch',
'webinar-series',
];
// Dimension 3: Content Type
const contentTags = [
'blog-post',
'video',
'landing-page',
'pdf-guide',
'pricing',
];
// Dimension 4: Status
const statusTags = [
'active',
'archived',
'testing',
'expired',
];
// Dimension 5: Performance
const performanceTags = [
'high-traffic',
'conversion-tracked',
'a-b-test',
'vip-only',
];
// Example: Link with multi-dimensional tags
{
url: 'https://company.com/webinar/q1',
tags: [
'marketing', // Department
'q1-2025', // Campaign
'webinar-series', // Project
'landing-page', // Content type
'active', // Status
'conversion-tracked' // Performance
]
}Naming Conventions
Establish consistent tag naming rules:
// ✅ GOOD: Consistent, readable, scalable
const goodTags = [
'marketing', // lowercase
'q4-2025', // lowercase with dashes
'email-campaign', // descriptive
'landing-page', // clear category
'high-priority', // hyphenated phrases
];
// ❌ BAD: Inconsistent, unclear, hard to scale
const badTags = [
'Marketing', // Mixed case
'Q4_2025', // Underscore vs dash
'email', // Too vague
'lp', // Unclear abbreviation
'highPriority', // camelCase (use hyphens)
];
// Tag validation helper
function validateTag(tag: string): boolean {
return (
tag.length >= 1 &&
tag.length <= 50 &&
/^[a-z0-9-]+$/.test(tag) && // lowercase, numbers, hyphens only
!tag.startsWith('-') &&
!tag.endsWith('-')
);
}Tag Taxonomy Example: Marketing Agency
const TAG_TAXONOMY = {
// Client tags
clients: [
'client-acme',
'client-globex',
'client-initech',
],
// Service tags
services: [
'seo',
'ppc',
'social-media',
'email-marketing',
'content-marketing',
],
// Campaign phase
phases: [
'planning',
'active',
'completed',
'paused',
],
// Link purpose
purposes: [
'campaign-landing',
'report-dashboard',
'client-preview',
'internal-tracking',
],
// Performance tier
performance: [
'tier-high', // >1000 clicks
'tier-medium', // 100-1000 clicks
'tier-low', // <100 clicks
],
};
// Usage example
async function createClientCampaignLink(
url: string,
client: string,
service: string
) {
return await fetch(`${API_BASE}/workspaces/${WORKSPACE_ID}/links`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
tags: [
`client-${client}`,
service,
'active',
'campaign-landing',
],
metadata: {
createdBy: 'campaign-tool',
clientId: client,
},
}),
});
}Bulk Tagging Operations
Add Tags to Existing Links
const API_KEY = process.env.SNIPLINK_API_KEY;
const WORKSPACE_ID = 'your-workspace-id';
const API_BASE = 'https://api.sniplink.co/v1';
async function bulkAddTags(
linkIds: string[],
tagsToAdd: string[]
) {
const results = [];
for (const linkId of linkIds) {
try {
// Get current link
const link = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}`,
{ headers: { 'X-API-Key': API_KEY } }
).then((r) => r.json());
// Merge tags (avoid duplicates)
const currentTags = link.data.link.tags || [];
const newTags = [...new Set([...currentTags, ...tagsToAdd])];
// Update link
await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags: newTags }),
}
);
results.push({ linkId, success: true });
} catch (error) {
results.push({ linkId, success: false, error });
}
}
return results;
}
// Tag all Q4 marketing campaign links
const campaignLinks = ['lnk_123', 'lnk_456', 'lnk_789'];
await bulkAddTags(campaignLinks, ['q4-2025', 'high-priority']);Find and Tag Links by Pattern
async function findAndTagLinks(
searchQuery: string,
tagsToAdd: string[]
) {
let page = 1;
let hasMore = true;
let totalTagged = 0;
while (hasMore) {
// Fetch links matching search
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links?` +
new URLSearchParams({
page: String(page),
limit: '100',
search: searchQuery,
}),
{ headers: { 'X-API-Key': API_KEY } }
).then((r) => r.json());
const links = response.data.links;
hasMore = response.data.pagination.hasMore;
// Add tags to all matching links
const linkIds = links.map((link: any) => link.id);
await bulkAddTags(linkIds, tagsToAdd);
totalTagged += linkIds.length;
page++;
}
return { totalTagged };
}
// Tag all links containing 'webinar' in title/URL
const result = await findAndTagLinks('webinar', [
'webinar-series',
'lead-generation',
]);
console.log(`Tagged ${result.totalTagged} links`);Remove Tags from Links
async function bulkRemoveTags(
linkIds: string[],
tagsToRemove: string[]
) {
for (const linkId of linkIds) {
// Get current link
const link = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}`,
{ headers: { 'X-API-Key': API_KEY } }
).then((r) => r.json());
// Remove specified tags
const currentTags = link.data.link.tags || [];
const newTags = currentTags.filter(
(tag: string) => !tagsToRemove.includes(tag)
);
// Update link
await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags: newTags }),
}
);
}
}
// Remove 'active' tag from expired campaigns
await bulkRemoveTags(expiredLinks, ['active']);Replace Tags Across All Links
async function replaceTag(
oldTag: string,
newTag: string
) {
let page = 1;
let hasMore = true;
let totalUpdated = 0;
while (hasMore) {
// Fetch links with old tag
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links?` +
new URLSearchParams({
page: String(page),
limit: '100',
search: oldTag,
}),
{ headers: { 'X-API-Key': API_KEY } }
).then((r) => r.json());
const links = response.data.links;
hasMore = response.data.pagination.hasMore;
for (const link of links) {
if (!link.tags.includes(oldTag)) continue;
// Replace tag
const newTags = link.tags.map((tag: string) =>
tag === oldTag ? newTag : tag
);
await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${link.id}`,
{
method: 'PATCH',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags: newTags }),
}
);
totalUpdated++;
}
page++;
}
return { totalUpdated };
}
// Rename tag: 'q3-2024' → 'archived-q3-2024'
await replaceTag('q3-2024', 'archived-q3-2024');Search and Filter Systems
Advanced Link Search Tool
interface SearchFilters {
tags?: string[];
domainId?: string;
createdAfter?: Date;
createdBefore?: Date;
minClicks?: number;
maxClicks?: number;
sortBy?: 'createdAt' | 'clicks' | 'title';
sortOrder?: 'asc' | 'desc';
}
async function advancedSearch(filters: SearchFilters) {
const allLinks = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const params = new URLSearchParams({
page: String(page),
limit: '100',
...(filters.domainId && { domainId: filters.domainId }),
...(filters.sortBy && { sortBy: filters.sortBy }),
...(filters.sortOrder && { sortOrder: filters.sortOrder }),
});
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links?${params}`,
{ headers: { 'X-API-Key': API_KEY } }
).then((r) => r.json());
let links = response.data.links;
// Client-side filtering for advanced criteria
if (filters.tags && filters.tags.length > 0) {
links = links.filter((link: any) =>
filters.tags!.every((tag) => link.tags.includes(tag))
);
}
if (filters.createdAfter) {
links = links.filter(
(link: any) =>
new Date(link.createdAt) >= filters.createdAfter!
);
}
if (filters.createdBefore) {
links = links.filter(
(link: any) =>
new Date(link.createdAt) <= filters.createdBefore!
);
}
if (filters.minClicks !== undefined) {
links = links.filter(
(link: any) => link.clickCount >= filters.minClicks!
);
}
if (filters.maxClicks !== undefined) {
links = links.filter(
(link: any) => link.clickCount <= filters.maxClicks!
);
}
allLinks.push(...links);
hasMore = response.data.pagination.hasMore;
page++;
}
return allLinks;
}
// Example: Find high-performing Q4 marketing links
const results = await advancedSearch({
tags: ['marketing', 'q4-2025'],
minClicks: 1000,
sortBy: 'clicks',
sortOrder: 'desc',
});
console.log(`Found ${results.length} high-performing links`);Tag Analytics Dashboard
async function getTagAnalytics() {
const tagStats = new Map<string, {
count: number;
totalClicks: number;
avgClicks: number;
links: any[];
}>();
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links?page=${page}&limit=100`,
{ headers: { 'X-API-Key': API_KEY } }
).then((r) => r.json());
const links = response.data.links;
for (const link of links) {
for (const tag of link.tags) {
if (!tagStats.has(tag)) {
tagStats.set(tag, {
count: 0,
totalClicks: 0,
avgClicks: 0,
links: [],
});
}
const stats = tagStats.get(tag)!;
stats.count++;
stats.totalClicks += link.clickCount;
stats.links.push(link);
}
}
hasMore = response.data.pagination.hasMore;
page++;
}
// Calculate averages
for (const [tag, stats] of tagStats.entries()) {
stats.avgClicks = Math.round(stats.totalClicks / stats.count);
}
// Sort by total clicks
return Array.from(tagStats.entries())
.sort(([, a], [, b]) => b.totalClicks - a.totalClicks)
.map(([tag, stats]) => ({ tag, ...stats }));
}
// Generate tag performance report
const analytics = await getTagAnalytics();
console.log('Top Tags by Performance:');
analytics.slice(0, 10).forEach((stat) => {
console.log(` ${stat.tag}:`);
console.log(` Links: ${stat.count}`);
console.log(` Total clicks: ${stat.totalClicks.toLocaleString()}`);
console.log(` Avg clicks/link: ${stat.avgClicks}`);
});Automated Tag Management
Auto-Tag Based on URL Patterns
interface TagRule {
pattern: RegExp;
tags: string[];
}
const AUTO_TAG_RULES: TagRule[] = [
{
pattern: /blog\.company\.com/,
tags: ['blog-post', 'content-marketing'],
},
{
pattern: /pricing|buy|purchase/,
tags: ['conversion', 'sales'],
},
{
pattern: /youtube\.com|vimeo\.com/,
tags: ['video', 'media'],
},
{
pattern: /\/webinar\//,
tags: ['webinar', 'lead-generation'],
},
];
async function autoTagLink(url: string, existingTags: string[] = []) {
const autoTags = [];
for (const rule of AUTO_TAG_RULES) {
if (rule.pattern.test(url)) {
autoTags.push(...rule.tags);
}
}
// Combine and deduplicate
return [...new Set([...existingTags, ...autoTags])];
}
// Use in link creation
async function createSmartLink(url: string, manualTags: string[] = []) {
const tags = await autoTagLink(url, manualTags);
return await fetch(`${API_BASE}/workspaces/${WORKSPACE_ID}/links`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, tags }),
});
}Webhook-Based Auto-Tagging
// Automatically tag new links based on metadata
async function handleLinkCreated(event: any) {
const { link } = event.data;
const autoTags = [];
// Tag based on metadata
if (link.metadata?.department) {
autoTags.push(`dept-${link.metadata.department}`);
}
if (link.metadata?.priority === 'high') {
autoTags.push('high-priority');
}
if (link.metadata?.tracking === 'conversion') {
autoTags.push('conversion-tracked');
}
// Add auto-tags if any were generated
if (autoTags.length > 0) {
const currentTags = link.tags || [];
const newTags = [...new Set([...currentTags, ...autoTags])];
await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${link.id}`,
{
method: 'PATCH',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags: newTags }),
}
);
}
}Scheduled Tag Cleanup
async function cleanupOldCampaignTags() {
const cutoffDate = new Date();
cutoffDate.setMonth(cutoffDate.getMonth() - 6); // 6 months ago
const links = await advancedSearch({
createdBefore: cutoffDate,
tags: ['active'],
});
console.log(`Found ${links.length} old active links`);
// Remove 'active' tag, add 'archived'
for (const link of links) {
const newTags = link.tags
.filter((tag: string) => tag !== 'active')
.concat(['archived']);
await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${link.id}`,
{
method: 'PATCH',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags: newTags }),
}
);
}
}
// Run monthly via cron
// 0 0 1 * * /path/to/cleanup-tags.shBest Practices
1. Start Simple, Grow Gradually
// Phase 1: Basic department tags
const basicTags = ['marketing', 'sales', 'support'];
// Phase 2: Add campaign/project tags
const expandedTags = ['marketing', 'q4-campaign', 'email'];
// Phase 3: Add status and performance tags
const advancedTags = [
'marketing',
'q4-campaign',
'email',
'active',
'high-traffic',
];2. Document Your Tag Taxonomy
// Create centralized tag definitions
const TAG_GUIDE = {
departments: {
description: 'Team or department owner',
examples: ['marketing', 'sales', 'product'],
required: true,
},
campaigns: {
description: 'Quarterly or named campaigns',
format: 'campaign-name or qX-YYYY',
examples: ['black-friday', 'q1-2025'],
required: false,
},
status: {
description: 'Current lifecycle status',
options: ['active', 'archived', 'testing', 'expired'],
required: true,
},
};3. Validate Tags on Creation
async function createValidatedLink(
url: string,
tags: string[],
metadata?: any
) {
// Validate tag count
if (tags.length > 10) {
throw new Error('Maximum 10 tags per link');
}
// Validate tag format
for (const tag of tags) {
if (!validateTag(tag)) {
throw new Error(`Invalid tag format: ${tag}`);
}
}
// Ensure required tags
const hasDepartment = tags.some((t) =>
['marketing', 'sales', 'support', 'product'].includes(t)
);
const hasStatus = tags.some((t) =>
['active', 'archived', 'testing', 'expired'].includes(t)
);
if (!hasDepartment) {
throw new Error('Department tag required');
}
if (!hasStatus) {
throw new Error('Status tag required');
}
return await fetch(`${API_BASE}/workspaces/${WORKSPACE_ID}/links`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, tags, metadata }),
});
}Next Steps
Bulk Operations →
Create and tag hundreds of links at once with bulk operations
Analytics by Tag →
Build dashboards that aggregate analytics by tags
Auto-Tagging →
Automate tagging with webhooks and business rules
API Reference →
Complete API documentation for tags and metadata
Need Help Organizing Links?
Want to discuss tagging strategies for your use case? Loading contact information or share your organization system with other users.