UNPKG

website-visitor-counter

Version:

Real visitor counting system with Railway backend - works like komarev.com with accurate cross-device counting

314 lines (313 loc) 11 kB
// Website Visitor Counter v3.1.0 // Multi-platform cloud adapter system with improved security and scalability export class RailwayAdapter { constructor(baseUrl) { this.name = 'railway'; this.baseUrl = baseUrl || process.env.RAILWAY_API_BASE || 'https://websitevisiotrscounter-production.up.railway.app'; } getCounterUrl(project, options) { return `${this.baseUrl}/counter?project=${encodeURIComponent(project)}&${this.buildQueryString(options)}`; } getCountUrl(project) { return `${this.baseUrl}/count/${encodeURIComponent(project)}`; } getResetUrl(project) { return `${this.baseUrl}/reset/${encodeURIComponent(project)}`; } getHealthUrl() { return `${this.baseUrl}/health`; } getStatsUrl() { return `${this.baseUrl}/stats`; } buildQueryString(options) { if (!options) return ''; const params = new URLSearchParams(); if (options.label) params.append('label', options.label); if (options.color) params.append('color', options.color); if (options.style) params.append('style', options.style); if (options.base) params.append('base', options.base.toString()); if (options.abbreviated) params.append('abbreviated', options.abbreviated.toString()); return params.toString(); } } export class VercelAdapter { constructor(baseUrl) { this.name = 'vercel'; this.baseUrl = baseUrl || process.env.VERCEL_API_BASE || 'https://your-vercel-app.vercel.app'; } getCounterUrl(project, options) { return `${this.baseUrl}/counter?project=${encodeURIComponent(project)}&${this.buildQueryString(options)}`; } getCountUrl(project) { return `${this.baseUrl}/count/${encodeURIComponent(project)}`; } getResetUrl(project) { return `${this.baseUrl}/reset/${encodeURIComponent(project)}`; } getHealthUrl() { return `${this.baseUrl}/health`; } getStatsUrl() { return `${this.baseUrl}/stats`; } buildQueryString(options) { if (!options) return ''; const params = new URLSearchParams(); if (options.label) params.append('label', options.label); if (options.color) params.append('color', options.color); if (options.style) params.append('style', options.style); if (options.base) params.append('base', options.base.toString()); if (options.abbreviated) params.append('abbreviated', options.abbreviated.toString()); return params.toString(); } } export class NetlifyAdapter { constructor(baseUrl) { this.name = 'netlify'; this.baseUrl = baseUrl || process.env.NETLIFY_API_BASE || 'https://your-netlify-app.netlify.app'; } getCounterUrl(project, options) { return `${this.baseUrl}/counter?project=${encodeURIComponent(project)}&${this.buildQueryString(options)}`; } getCountUrl(project) { return `${this.baseUrl}/count/${encodeURIComponent(project)}`; } getResetUrl(project) { return `${this.baseUrl}/reset/${encodeURIComponent(project)}`; } getHealthUrl() { return `${this.baseUrl}/health`; } getStatsUrl() { return `${this.baseUrl}/stats`; } buildQueryString(options) { if (!options) return ''; const params = new URLSearchParams(); if (options.label) params.append('label', options.label); if (options.color) params.append('color', options.color); if (options.style) params.append('style', options.style); if (options.base) params.append('base', options.base.toString()); if (options.abbreviated) params.append('abbreviated', options.abbreviated.toString()); return params.toString(); } } export class CloudflareAdapter { constructor(baseUrl) { this.name = 'cloudflare'; this.baseUrl = baseUrl || process.env.CLOUDFLARE_API_BASE || 'https://your-cloudflare-app.pages.dev'; } getCounterUrl(project, options) { return `${this.baseUrl}/counter?project=${encodeURIComponent(project)}&${this.buildQueryString(options)}`; } getCountUrl(project) { return `${this.baseUrl}/count/${encodeURIComponent(project)}`; } getResetUrl(project) { return `${this.baseUrl}/reset/${encodeURIComponent(project)}`; } getHealthUrl() { return `${this.baseUrl}/health`; } getStatsUrl() { return `${this.baseUrl}/stats`; } buildQueryString(options) { if (!options) return ''; const params = new URLSearchParams(); if (options.label) params.append('label', options.label); if (options.color) params.append('color', options.color); if (options.style) params.append('style', options.style); if (options.base) params.append('base', options.base.toString()); if (options.abbreviated) params.append('abbreviated', options.abbreviated.toString()); return params.toString(); } } // Default adapter (Railway for backward compatibility) const defaultAdapter = new RailwayAdapter(); // Platform factory export function createPlatformAdapter(options) { if (options?.customBaseUrl) { return new RailwayAdapter(options.customBaseUrl); // Use Railway adapter as base for custom URLs } switch (options?.platform) { case 'vercel': return new VercelAdapter(); case 'netlify': return new NetlifyAdapter(); case 'cloudflare': return new CloudflareAdapter(); case 'railway': default: return defaultAdapter; } } // Core functions with improved security and error handling export async function getVisitorCounterBadge(project, options) { try { const adapter = createPlatformAdapter(options); // Add rate limiting check if (!isRateLimitAllowed(project)) { throw new Error('Rate limit exceeded'); } const response = await fetch(adapter.getCounterUrl(project, options), { method: 'GET', headers: { 'User-Agent': 'Website-Visitor-Counter/3.1.0', 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.badgeUrl || data.url || `https://img.shields.io/badge/visitors-${data.count || 0}-blue`; } catch (error) { console.warn('Failed to fetch from backend, using fallback:', error); // Fallback to shields.io with base count const count = options?.base || 0; return `https://img.shields.io/badge/visitors-${count}-blue`; } } export async function getVisitorCount(project, options) { try { const adapter = createPlatformAdapter(options); if (!isRateLimitAllowed(project)) { throw new Error('Rate limit exceeded'); } const response = await fetch(adapter.getCountUrl(project), { method: 'GET', headers: { 'User-Agent': 'Website-Visitor-Counter/3.1.0', 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.count || 0; } catch (error) { console.warn('Failed to fetch visitor count:', error); return options?.base || 0; } } export async function resetVisitorCount(project, options) { try { const adapter = createPlatformAdapter(options); if (!isRateLimitAllowed(project)) { throw new Error('Rate limit exceeded'); } const response = await fetch(adapter.getResetUrl(project), { method: 'POST', headers: { 'User-Agent': 'Website-Visitor-Counter/3.1.0', 'Content-Type': 'application/json', }, }); return response.ok; } catch (error) { console.warn('Failed to reset visitor count:', error); return false; } } // Utility functions export function getSimpleVisitorBadge(project, options) { const adapter = createPlatformAdapter(options); return adapter.getCounterUrl(project, options); } export function getVisitorCounterHTML(project, options) { const badgeUrl = getSimpleVisitorBadge(project, options); return `<img src="${badgeUrl}" alt="visitors" />`; } export function getVisitorCounterMarkdown(project, options) { const badgeUrl = getSimpleVisitorBadge(project, options); return `![visitors](${badgeUrl})`; } export function getVisitorCounterReact(project, options) { const badgeUrl = getSimpleVisitorBadge(project, options); return `<img src="${badgeUrl}" alt="visitors" />`; } // Backend monitoring functions export async function getBackendHealth(options) { try { const adapter = createPlatformAdapter(options); const response = await fetch(adapter.getHealthUrl(), { method: 'GET', headers: { 'User-Agent': 'Website-Visitor-Counter/3.1.0', 'Accept': 'application/json', }, }); return response.ok; } catch (error) { return false; } } export async function getBackendStats(options) { try { const adapter = createPlatformAdapter(options); const response = await fetch(adapter.getStatsUrl(), { method: 'GET', headers: { 'User-Agent': 'Website-Visitor-Counter/3.1.0', 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.warn('Failed to fetch backend stats:', error); return { error: 'Failed to fetch stats' }; } } // Rate limiting implementation const rateLimitMap = new Map(); const RATE_LIMIT_WINDOW = 60000; // 1 minute const RATE_LIMIT_MAX = 10; // 10 requests per minute function isRateLimitAllowed(project) { const now = Date.now(); const key = `rate_limit_${project}`; const current = rateLimitMap.get(key); if (!current || now > current.resetTime) { rateLimitMap.set(key, { count: 1, resetTime: now + RATE_LIMIT_WINDOW }); return true; } if (current.count >= RATE_LIMIT_MAX) { return false; } current.count++; return true; }