URL Shortener System IN HTML CSS AND JAVASCRIPT WITH PHP AND MY SQL

Project Introduction

A comprehensive URL shortening service that transforms long, complex URLs into short, memorable links. This system integrates with the existing blog website, allowing users to create shortened URLs for sharing blog posts, and provides analytics for tracking link performance. Perfect for social media sharing, email campaigns, and tracking referral traffic.


✨ Features

User-Facing Features

  • Instant URL Shortening: Convert long URLs to short, shareable links
  • Custom Aliases: Users can create personalized short URLs (e.g., yourdomain.com/offer)
  • QR Code Generation: Automatic QR codes for each shortened URL
  • Link Expiration: Optional expiry dates for temporary links
  • Password Protection: Secure sensitive links with passwords
  • Bulk URL Shortening: Create multiple short links at once

Analytics & Tracking

  • Click Tracking: Real-time click monitoring
  • Geolocation Data: Track visitor locations
  • Referrer Information: See where clicks come from
  • Device Analytics: Track mobile vs desktop usage
  • Browser Statistics: Monitor which browsers are used
  • Time-based Analytics: Hourly/daily click patterns

Admin Features

  • User Management: Track who creates which links
  • Link Moderation: Review and block inappropriate links
  • Custom Domain Support: Use your own domain for short links
  • API Access: Programmatic URL shortening
  • Export Data: Download analytics as CSV/JSON

Technical Features

  • URL Validation: Check for valid and accessible URLs
  • Duplicate Prevention: Avoid creating multiple short URLs for same long URL
  • Link Redirection: Fast 301/302 redirects
  • Caching System: Redis/Memcached for high performance
  • RESTful API: JSON endpoints for third-party integration
  • Rate Limiting: Prevent abuse and spam

📁 File Structure

blog-website/
│
├── shorten.php                      # Main URL shortener interface
├── redirect.php                      # URL redirection handler
├── s.php                              # Short code redirect (alternative)
├── dashboard.php                      # User dashboard for link management
│
├── includes/
│   ├── url-config.php                 # URL shortener configuration
│   ├── url-functions.php               # Core URL shortening functions
│   ├── url-process.php                  # AJAX processing endpoint
│   ├── url-analytics.php                 # Analytics functions
│   └── url-api.php                         # REST API endpoints
│
├── admin/
│   ├── url-manager.php                   # Admin link management
│   ├── url-stats.php                       # Global statistics
│   ├── blocked-domains.php                  # Manage blocked domains
│   └── export-links.php                       # Export link data
│
├── css/
│   └── url-shortener.css                    # Shortener specific styling
│
├── js/
│   └── url-shortener.js                       # Shortener JavaScript
│
├── vendor/                                      # Composer dependencies
│
├── .env                                           # Environment variables
├── composer.json                                    # Composer dependencies
│
└── database/
└── url-shortener.sql                             # Database schema

🗄️ Database Schema (database/url-shortener.sql)

-- Create URL shortener database tables
CREATE DATABASE IF NOT EXISTS url_shortener;
USE url_shortener;
-- Links table
CREATE TABLE IF NOT EXISTS shortened_urls (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
long_url TEXT NOT NULL,
short_code VARCHAR(50) UNIQUE NOT NULL,
custom_alias VARCHAR(100) UNIQUE NULL,
title VARCHAR(255),
description TEXT,
password VARCHAR(255) NULL,
expires_at TIMESTAMP NULL,
is_active BOOLEAN DEFAULT TRUE,
is_custom BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_short_code (short_code),
INDEX idx_user_id (user_id),
INDEX idx_expires (expires_at),
INDEX idx_created (created_at),
FULLTEXT INDEX idx_search (long_url, title, description)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Clicks tracking table
CREATE TABLE IF NOT EXISTS url_clicks (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
url_id INT NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
referer TEXT,
country VARCHAR(100),
city VARCHAR(100),
device_type ENUM('desktop', 'mobile', 'tablet', 'bot') DEFAULT 'desktop',
browser VARCHAR(50),
browser_version VARCHAR(20),
os VARCHAR(50),
clicked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (url_id) REFERENCES shortened_urls(id) ON DELETE CASCADE,
INDEX idx_url_id (url_id),
INDEX idx_clicked_at (clicked_at),
INDEX idx_country (country)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- User links association (if integrated with user system)
CREATE TABLE IF NOT EXISTS user_links (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
url_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (url_id) REFERENCES shortened_urls(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_url (user_id, url_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Blocked domains for spam prevention
CREATE TABLE IF NOT EXISTS blocked_domains (
id INT AUTO_INCREMENT PRIMARY KEY,
domain VARCHAR(255) NOT NULL UNIQUE,
reason TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_domain (domain)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- QR codes cache
CREATE TABLE IF NOT EXISTS qr_codes (
id INT AUTO_INCREMENT PRIMARY KEY,
url_id INT NOT NULL,
qr_code_data TEXT,
file_path VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (url_id) REFERENCES shortened_urls(id) ON DELETE CASCADE,
UNIQUE KEY unique_url_qr (url_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Rate limiting table
CREATE TABLE IF NOT EXISTS url_rate_limit (
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR(45) NOT NULL,
attempts INT DEFAULT 1,
last_attempt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
blocked_until TIMESTAMP NULL,
INDEX idx_ip (ip_address),
INDEX idx_blocked (blocked_until)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

🔧 File Contents

1. composer.json

{
"name": "blog-website/url-shortener",
"description": "Professional URL shortening service with analytics",
"type": "project",
"require": {
"php": ">=7.4",
"vlucas/phpdotenv": "^5.5",
"ext-json": "*",
"ext-pdo": "*",
"ext-curl": "*",
"ext-gd": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"URLShortener\\": "src/"
}
},
"license": "MIT",
"authors": [
{
"name": "Your Name",
"email": "[email protected]"
}
]
}

2. .env (Environment Variables)

# Database Configuration
DB_HOST=localhost
DB_NAME=url_shortener
DB_USER=root
DB_PASS=
# Application Settings
APP_NAME="My Blog URL Shortener"
APP_URL=http://localhost/blog-website
SHORT_DOMAIN=s.yourdomain.com
BASE_SHORT_URL=http://s.yourdomain.com
# Security
RATE_LIMIT_ENABLED=true
RATE_LIMIT_ATTEMPTS=10
RATE_LIMIT_MINUTES=60
MAX_URL_LENGTH=2048
ALLOW_CUSTOM_ALIASES=true
MIN_ALIAS_LENGTH=3
MAX_ALIAS_LENGTH=50
# URL Validation
CHECK_URL_ACCESSIBILITY=true
ALLOWED_PROTOCOLS=http,https
BLOCK_LOCAL_URLS=true
BLOCK_PRIVATE_IPS=true
# Cache Settings
CACHE_DRIVER=file # redis, memcached, file
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# QR Code Settings
QR_CODE_SIZE=300
QR_CODE_MARGIN=10
QR_CODE_FORMAT=png
# IP Geolocation (optional)
IPINFO_TOKEN=your_token_here
MAXMIND_LICENSE_KEY=your_key_here
# Admin Notification
[email protected]
NOTIFY_ON_ABUSE=true

3. includes/url-config.php

<?php
/**
* URL Shortener Configuration
*/
require_once __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
// Load environment variables
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();
// Database configuration
define('DB_HOST', $_ENV['DB_HOST']);
define('DB_USER', $_ENV['DB_USER']);
define('DB_PASS', $_ENV['DB_PASS']);
define('DB_NAME', $_ENV['DB_NAME']);
// Application settings
define('APP_NAME', $_ENV['APP_NAME']);
define('APP_URL', $_ENV['APP_URL']);
define('SHORT_DOMAIN', $_ENV['SHORT_DOMAIN']);
define('BASE_SHORT_URL', $_ENV['BASE_SHORT_URL']);
// Security settings
define('RATE_LIMIT_ENABLED', filter_var($_ENV['RATE_LIMIT_ENABLED'], FILTER_VALIDATE_BOOLEAN));
define('RATE_LIMIT_ATTEMPTS', (int)$_ENV['RATE_LIMIT_ATTEMPTS']);
define('RATE_LIMIT_MINUTES', (int)$_ENV['RATE_LIMIT_MINUTES']);
define('MAX_URL_LENGTH', (int)$_ENV['MAX_URL_LENGTH']);
define('ALLOW_CUSTOM_ALIASES', filter_var($_ENV['ALLOW_CUSTOM_ALIASES'], FILTER_VALIDATE_BOOLEAN));
define('MIN_ALIAS_LENGTH', (int)$_ENV['MIN_ALIAS_LENGTH']);
define('MAX_ALIAS_LENGTH', (int)$_ENV['MAX_ALIAS_LENGTH']);
// QR Code settings
define('QR_CODE_SIZE', (int)$_ENV['QR_CODE_SIZE']);
define('QR_CODE_MARGIN', (int)$_ENV['QR_CODE_MARGIN']);
define('QR_CODE_FORMAT', $_ENV['QR_CODE_FORMAT']);
// Start session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Database connection function
function getDBConnection() {
static $conn = null;
if ($conn === null) {
try {
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
throw new Exception("Connection failed: " . $conn->connect_error);
}
$conn->set_charset("utf8mb4");
} catch (Exception $e) {
error_log("Database connection error: " . $e->getMessage());
return null;
}
}
return $conn;
}
// PDO connection for prepared statements
function getPDOConnection() {
static $pdo = null;
if ($pdo === null) {
try {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4";
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]);
} catch (PDOException $e) {
error_log("PDO Connection error: " . $e->getMessage());
return null;
}
}
return $pdo;
}

4. includes/url-functions.php

<?php
/**
* Core URL Shortener Functions
*/
require_once __DIR__ . '/url-config.php';
/**
* Generate a unique short code
*/
function generateShortCode($length = 6) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[random_int(0, $charactersLength - 1)];
}
return $randomString;
}
/**
* Check if short code exists
*/
function shortCodeExists($code, $pdo = null) {
if (!$pdo) {
$pdo = getPDOConnection();
}
$stmt = $pdo->prepare("SELECT id FROM shortened_urls WHERE short_code = ? OR custom_alias = ?");
$stmt->execute([$code, $code]);
return $stmt->fetch() !== false;
}
/**
* Generate unique short code
*/
function getUniqueShortCode($length = 6) {
$pdo = getPDOConnection();
$maxAttempts = 10;
$attempt = 0;
do {
$code = generateShortCode($length);
$attempt++;
if ($attempt > $maxAttempts) {
$length++;
$attempt = 0;
}
} while (shortCodeExists($code, $pdo));
return $code;
}
/**
* Validate URL format and accessibility
*/
function validateUrl($url) {
// Check length
if (strlen($url) > MAX_URL_LENGTH) {
return ['valid' => false, 'error' => 'URL is too long'];
}
// Validate format
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return ['valid' => false, 'error' => 'Invalid URL format'];
}
// Check protocol
$parsed = parse_url($url);
$protocol = strtolower($parsed['scheme'] ?? '');
$allowed = explode(',', ALLOWED_PROTOCOLS);
if (!in_array($protocol, $allowed)) {
return ['valid' => false, 'error' => 'Protocol not allowed'];
}
// Block local URLs
if (BLOCK_LOCAL_URLS) {
$host = $parsed['host'] ?? '';
if ($host === 'localhost' || $host === '127.0.0.1' || strpos($host, '192.168.') === 0) {
return ['valid' => false, 'error' => 'Local URLs are not allowed'];
}
}
// Check URL accessibility (optional)
if (CHECK_URL_ACCESSIBILITY) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
return ['valid' => false, 'error' => 'URL is not accessible (HTTP ' . $httpCode . ')'];
}
}
return ['valid' => true];
}
/**
* Check if domain is blocked
*/
function isDomainBlocked($url) {
$pdo = getPDOConnection();
$parsed = parse_url($url);
$domain = $parsed['host'] ?? '';
// Remove www prefix
$domain = preg_replace('/^www\./', '', $domain);
$stmt = $pdo->prepare("SELECT id FROM blocked_domains WHERE domain = ? OR domain LIKE ?");
$stmt->execute([$domain, '%.' . $domain]);
return $stmt->fetch() !== false;
}
/**
* Create shortened URL
*/
function createShortUrl($data) {
$pdo = getPDOConnection();
// Check rate limit
if (!checkRateLimit()) {
return ['success' => false, 'error' => 'Rate limit exceeded. Please try again later.'];
}
// Validate URL
$validation = validateUrl($data['long_url']);
if (!$validation['valid']) {
return ['success' => false, 'error' => $validation['error']];
}
// Check if domain is blocked
if (isDomainBlocked($data['long_url'])) {
return ['success' => false, 'error' => 'This domain has been blocked'];
}
// Check for existing URL (optional - prevent duplicates)
if (!empty($data['prevent_duplicates'])) {
$stmt = $pdo->prepare("SELECT short_code FROM shortened_urls WHERE long_url = ? AND is_active = 1");
$stmt->execute([$data['long_url']]);
$existing = $stmt->fetch();
if ($existing) {
return [
'success' => true,
'short_url' => BASE_SHORT_URL . '/' . $existing['short_code'],
'short_code' => $existing['short_code'],
'existing' => true
];
}
}
// Handle custom alias
$shortCode = '';
$isCustom = false;
if (!empty($data['custom_alias']) && ALLOW_CUSTOM_ALIASES) {
$alias = preg_replace('/[^a-zA-Z0-9\-_]/', '', $data['custom_alias']);
if (strlen($alias) < MIN_ALIAS_LENGTH || strlen($alias) > MAX_ALIAS_LENGTH) {
return ['success' => false, 'error' => 'Custom alias length must be between ' . MIN_ALIAS_LENGTH . ' and ' . MAX_ALIAS_LENGTH];
}
if (shortCodeExists($alias, $pdo)) {
return ['success' => false, 'error' => 'Custom alias already taken'];
}
$shortCode = $alias;
$isCustom = true;
} else {
$shortCode = getUniqueShortCode();
}
// Prepare data for insertion
$insertData = [
'long_url' => $data['long_url'],
'short_code' => $shortCode,
'custom_alias' => $isCustom ? $shortCode : null,
'title' => $data['title'] ?? null,
'description' => $data['description'] ?? null,
'user_id' => $_SESSION['user_id'] ?? null,
'is_custom' => $isCustom,
'expires_at' => !empty($data['expires_at']) ? date('Y-m-d H:i:s', strtotime($data['expires_at'])) : null
];
// Hash password if provided
if (!empty($data['password'])) {
$insertData['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
}
// Insert into database
$sql = "INSERT INTO shortened_urls 
(long_url, short_code, custom_alias, title, description, user_id, is_custom, expires_at, password) 
VALUES 
(:long_url, :short_code, :custom_alias, :title, :description, :user_id, :is_custom, :expires_at, :password)";
$stmt = $pdo->prepare($sql);
$success = $stmt->execute($insertData);
if ($success) {
incrementRateLimit();
// Generate QR code
$urlId = $pdo->lastInsertId();
generateQRCode($urlId, BASE_SHORT_URL . '/' . $shortCode);
return [
'success' => true,
'short_url' => BASE_SHORT_URL . '/' . $shortCode,
'short_code' => $shortCode,
'id' => $urlId
];
}
return ['success' => false, 'error' => 'Failed to create short URL'];
}
/**
* Get URL info by short code
*/
function getUrlInfo($code) {
$pdo = getPDOConnection();
$stmt = $pdo->prepare("SELECT * FROM shortened_urls WHERE (short_code = ? OR custom_alias = ?) AND is_active = 1");
$stmt->execute([$code, $code]);
$url = $stmt->fetch();
if (!$url) {
return null;
}
// Check if expired
if ($url['expires_at'] && strtotime($url['expires_at']) < time()) {
return null;
}
return $url;
}
/**
* Record click
*/
function recordClick($urlId, $passwordVerified = false) {
$pdo = getPDOConnection();
// Get geolocation data
$geoData = getGeolocation($_SERVER['REMOTE_ADDR']);
// Parse user agent
$ua = parseUserAgent($_SERVER['HTTP_USER_AGENT'] ?? '');
$sql = "INSERT INTO url_clicks 
(url_id, ip_address, user_agent, referer, country, city, device_type, browser, browser_version, os) 
VALUES 
(:url_id, :ip, :ua, :referer, :country, :city, :device, :browser, :browser_version, :os)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'url_id' => $urlId,
'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? null,
'referer' => $_SERVER['HTTP_REFERER'] ?? null,
'country' => $geoData['country'] ?? null,
'city' => $geoData['city'] ?? null,
'device' => $ua['device'],
'browser' => $ua['browser'],
'browser_version' => $ua['version'],
'os' => $ua['os']
]);
}
/**
* Parse user agent
*/
function parseUserAgent($ua) {
$result = [
'device' => 'desktop',
'browser' => 'Unknown',
'version' => '',
'os' => 'Unknown'
];
if (empty($ua)) {
return $result;
}
// Detect mobile
if (preg_match('/Mobile|Android|iPhone|iPad|iPod/i', $ua)) {
$result['device'] = 'mobile';
}
// Detect tablet
if (preg_match('/Tablet|iPad/i', $ua)) {
$result['device'] = 'tablet';
}
// Detect bot
if (preg_match('/bot|crawler|spider|scraper/i', $ua)) {
$result['device'] = 'bot';
}
// Detect browser
if (strpos($ua, 'Chrome') !== false) {
$result['browser'] = 'Chrome';
preg_match('/Chrome\/([0-9.]+)/', $ua, $matches);
$result['version'] = $matches[1] ?? '';
} elseif (strpos($ua, 'Firefox') !== false) {
$result['browser'] = 'Firefox';
preg_match('/Firefox\/([0-9.]+)/', $ua, $matches);
$result['version'] = $matches[1] ?? '';
} elseif (strpos($ua, 'Safari') !== false) {
$result['browser'] = 'Safari';
preg_match('/Version\/([0-9.]+)/', $ua, $matches);
$result['version'] = $matches[1] ?? '';
} elseif (strpos($ua, 'Edge') !== false) {
$result['browser'] = 'Edge';
preg_match('/Edge\/([0-9.]+)/', $ua, $matches);
$result['version'] = $matches[1] ?? '';
} elseif (strpos($ua, 'MSIE') !== false || strpos($ua, 'Trident') !== false) {
$result['browser'] = 'Internet Explorer';
}
// Detect OS
if (strpos($ua, 'Windows') !== false) {
$result['os'] = 'Windows';
} elseif (strpos($ua, 'Mac') !== false) {
$result['os'] = 'macOS';
} elseif (strpos($ua, 'Linux') !== false) {
$result['os'] = 'Linux';
} elseif (strpos($ua, 'Android') !== false) {
$result['os'] = 'Android';
} elseif (strpos($ua, 'iOS') !== false || strpos($ua, 'iPhone') !== false || strpos($ua, 'iPad') !== false) {
$result['os'] = 'iOS';
}
return $result;
}
/**
* Get geolocation from IP
*/
function getGeolocation($ip) {
// Skip local IPs
if ($ip === '127.0.0.1' || $ip === '::1') {
return ['country' => 'Local', 'city' => 'Local'];
}
// Try using ipinfo.io (requires token)
if (defined('IPINFO_TOKEN') && IPINFO_TOKEN) {
$url = "http://ipinfo.io/{$ip}/json?token=" . IPINFO_TOKEN;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
$response = curl_exec($ch);
curl_close($ch);
if ($response) {
$data = json_decode($response, true);
return [
'country' => $data['country'] ?? 'Unknown',
'city' => $data['city'] ?? 'Unknown'
];
}
}
return ['country' => 'Unknown', 'city' => 'Unknown'];
}
/**
* Generate QR code for URL
*/
function generateQRCode($urlId, $url) {
// Create QR code directory if it doesn't exist
$qrDir = __DIR__ . '/../uploads/qrcodes/';
if (!file_exists($qrDir)) {
mkdir($qrDir, 0777, true);
}
$filename = 'qr_' . $urlId . '_' . time() . '.png';
$filepath = $qrDir . $filename;
// Use Google Charts API for simplicity
$qrUrl = "https://chart.googleapis.com/chart?chs=" . QR_CODE_SIZE . "x" . QR_CODE_SIZE . 
"&cht=qr&chl=" . urlencode($url) . "&choe=UTF-8&chld=L|" . QR_CODE_MARGIN;
$qrData = file_get_contents($qrUrl);
if ($qrData) {
file_put_contents($filepath, $qrData);
// Save to database
$pdo = getPDOConnection();
$stmt = $pdo->prepare("INSERT INTO qr_codes (url_id, qr_code_data, file_path) VALUES (?, ?, ?)");
$stmt->execute([$urlId, $qrData, 'uploads/qrcodes/' . $filename]);
return 'uploads/qrcodes/' . $filename;
}
return null;
}
/**
* Get URL statistics
*/
function getUrlStats($urlId) {
$pdo = getPDOConnection();
// Total clicks
$stmt = $pdo->prepare("SELECT COUNT(*) as total FROM url_clicks WHERE url_id = ?");
$stmt->execute([$urlId]);
$total = $stmt->fetch()['total'];
// Clicks by day (last 30 days)
$stmt = $pdo->prepare("
SELECT DATE(clicked_at) as date, COUNT(*) as count 
FROM url_clicks 
WHERE url_id = ? AND clicked_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY DATE(clicked_at)
ORDER BY date DESC
");
$stmt->execute([$urlId]);
$daily = $stmt->fetchAll();
// Clicks by country
$stmt = $pdo->prepare("
SELECT country, COUNT(*) as count 
FROM url_clicks 
WHERE url_id = ? AND country IS NOT NULL
GROUP BY country
ORDER BY count DESC
");
$stmt->execute([$urlId]);
$countries = $stmt->fetchAll();
// Clicks by device
$stmt = $pdo->prepare("
SELECT device_type, COUNT(*) as count 
FROM url_clicks 
WHERE url_id = ?
GROUP BY device_type
");
$stmt->execute([$urlId]);
$devices = $stmt->fetchAll();
// Clicks by browser
$stmt = $pdo->prepare("
SELECT browser, COUNT(*) as count 
FROM url_clicks 
WHERE url_id = ? AND browser IS NOT NULL
GROUP BY browser
ORDER BY count DESC
");
$stmt->execute([$urlId]);
$browsers = $stmt->fetchAll();
// Referrers
$stmt = $pdo->prepare("
SELECT referer, COUNT(*) as count 
FROM url_clicks 
WHERE url_id = ? AND referer IS NOT NULL
GROUP BY referer
ORDER BY count DESC
LIMIT 10
");
$stmt->execute([$urlId]);
$referrers = $stmt->fetchAll();
return [
'total_clicks' => $total,
'daily' => $daily,
'countries' => $countries,
'devices' => $devices,
'browsers' => $browsers,
'referrers' => $referrers
];
}
/**
* Check rate limit
*/
function checkRateLimit() {
if (!RATE_LIMIT_ENABLED) {
return true;
}
$pdo = getPDOConnection();
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
// Clean old attempts
$cleanup = $pdo->prepare("DELETE FROM url_rate_limit WHERE last_attempt < DATE_SUB(NOW(), INTERVAL ? MINUTE)");
$cleanup->execute([RATE_LIMIT_MINUTES * 2]);
// Check if IP is blocked
$stmt = $pdo->prepare("SELECT attempts, blocked_until FROM url_rate_limit WHERE ip_address = ?");
$stmt->execute([$ip]);
$row = $stmt->fetch();
if ($row) {
if ($row['blocked_until'] && strtotime($row['blocked_until']) > time()) {
return false; // IP is blocked
}
if ($row['attempts'] >= RATE_LIMIT_ATTEMPTS) {
// Block IP
$blockUntil = date('Y-m-d H:i:s', strtotime('+' . RATE_LIMIT_MINUTES . ' minutes'));
$update = $pdo->prepare("UPDATE url_rate_limit SET blocked_until = ? WHERE ip_address = ?");
$update->execute([$blockUntil, $ip]);
return false;
}
}
return true;
}
/**
* Increment rate limit counter
*/
function incrementRateLimit() {
if (!RATE_LIMIT_ENABLED) {
return;
}
$pdo = getPDOConnection();
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$stmt = $pdo->prepare("
INSERT INTO url_rate_limit (ip_address, attempts) VALUES (?, 1) 
ON DUPLICATE KEY UPDATE attempts = attempts + 1, last_attempt = CURRENT_TIMESTAMP
");
$stmt->execute([$ip]);
}

5. shorten.php (Main Interface)

<?php
require_once 'includes/config.php';
require_once 'includes/url-functions.php';
$page_title = 'URL Shortener - ' . APP_NAME;
// Handle form submission
$result = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = createShortUrl([
'long_url' => $_POST['url'] ?? '',
'custom_alias' => $_POST['custom_alias'] ?? '',
'title' => $_POST['title'] ?? '',
'password' => $_POST['password'] ?? '',
'expires_at' => $_POST['expires_at'] ?? '',
'prevent_duplicates' => isset($_POST['prevent_duplicates'])
]);
}
include 'includes/header.php';
?>
<link rel="stylesheet" href="css/url-shortener.css">
<div class="shortener-container">
<div class="shortener-header">
<h1>🔗 URL Shortener</h1>
<p>Transform long, complex URLs into short, memorable links. Perfect for sharing on social media, emails, and more!</p>
</div>
<div class="shortener-main">
<!-- URL Input Form -->
<div class="shortener-card">
<form method="POST" id="urlShortenerForm" class="shortener-form">
<div class="form-group">
<label for="url">Enter your long URL *</label>
<div class="url-input-group">
<input type="url" 
id="url" 
name="url" 
class="form-control" 
placeholder="https://example.com/very/long/url/that/needs/shortening"
required>
<button type="submit" class="btn-shorten" id="shortenBtn">
<span class="btn-text">Shorten</span>
<span class="btn-loader" style="display: none;">⏳</span>
</button>
</div>
<div class="error-message" id="url-error"></div>
</div>
<div class="advanced-options">
<div class="options-toggle">
<button type="button" id="toggleOptions" class="btn-link">
⚙️ Advanced Options
</button>
</div>
<div class="options-panel" id="optionsPanel" style="display: none;">
<?php if (ALLOW_CUSTOM_ALIASES): ?>
<div class="form-group">
<label for="custom_alias">Custom Alias (optional)</label>
<div class="alias-input-group">
<span class="domain-prefix"><?php echo SHORT_DOMAIN; ?>/</span>
<input type="text" 
id="custom_alias" 
name="custom_alias" 
class="form-control alias-input"
placeholder="my-custom-link"
pattern="[a-zA-Z0-9\-_]+"
maxlength="<?php echo MAX_ALIAS_LENGTH; ?>">
</div>
<small class="help-text">Only letters, numbers, hyphens, and underscores allowed</small>
<div class="error-message" id="alias-error"></div>
</div>
<?php endif; ?>
<div class="form-group">
<label for="title">Link Title (optional)</label>
<input type="text" 
id="title" 
name="title" 
class="form-control"
placeholder="My Awesome Link">
</div>
<div class="form-row">
<div class="form-group">
<label for="password">Password Protection (optional)</label>
<input type="password" 
id="password" 
name="password" 
class="form-control"
placeholder="Enter password">
<small class="help-text">Users will need this password to access the link</small>
</div>
<div class="form-group">
<label for="expires_at">Link Expiration (optional)</label>
<input type="datetime-local" 
id="expires_at" 
name="expires_at" 
class="form-control">
</div>
</div>
<div class="form-checkbox">
<label>
<input type="checkbox" name="prevent_duplicates" checked>
Prevent duplicate short URLs for the same long URL
</label>
</div>
</div>
</div>
</form>
<!-- Result Display -->
<?php if ($result): ?>
<?php if ($result['success']): ?>
<div class="result-card success">
<h3>✅ Your Short URL is Ready!</h3>
<div class="result-url">
<input type="text" id="shortUrl" value="<?php echo $result['short_url']; ?>" readonly>
<button class="btn-copy" onclick="copyToClipboard('<?php echo $result['short_url']; ?>')">
📋 Copy
</button>
</div>
<?php if (isset($result['existing']) && $result['existing']): ?>
<p class="existing-note">ℹ️ This URL was already shortened. Here's your existing link.</p>
<?php endif; ?>
<div class="result-actions">
<a href="<?php echo $result['short_url']; ?>" target="_blank" class="btn-test">
🔍 Test Link
</a>
<button class="btn-qr" onclick="showQRCode('<?php echo $result['short_url']; ?>')">
📱 Get QR Code
</button>
<a href="dashboard.php?code=<?php echo $result['short_code']; ?>" class="btn-stats">
📊 View Stats
</a>
</div>
<!-- QR Code Modal -->
<div id="qrModal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close">&times;</span>
<h3>QR Code for your link</h3>
<div id="qrCodeContainer"></div>
<a href="#" id="downloadQR" class="btn-download" download="qrcode.png">
Download QR Code
</a>
</div>
</div>
</div>
<?php else: ?>
<div class="result-card error">
<h3>❌ Error</h3>
<p><?php echo htmlspecialchars($result['error']); ?></p>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<!-- Features Section -->
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">⚡</div>
<h3>Fast & Reliable</h3>
<p>Instant redirections with 99.9% uptime guarantee</p>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<h3>Detailed Analytics</h3>
<p>Track clicks, locations, devices, and referrers</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔒</div>
<h3>Password Protection</h3>
<p>Secure your links with password protection</p>
</div>
<div class="feature-card">
<div class="feature-icon">📱</div>
<h3>QR Codes</h3>
<p>Automatic QR code generation for every link</p>
</div>
<div class="feature-card">
<div class="feature-icon">⏰</div>
<h3>Link Expiration</h3>
<p>Set expiry dates for temporary links</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎯</div>
<h3>Custom Aliases</h3>
<p>Create memorable custom short URLs</p>
</div>
</div>
<!-- Recent Links (if logged in) -->
<?php if (isset($_SESSION['user_id'])): ?>
<div class="recent-links">
<h2>Your Recent Links</h2>
<?php
$pdo = getPDOConnection();
$stmt = $pdo->prepare("
SELECT su.*, COUNT(uc.id) as clicks 
FROM shortened_urls su
LEFT JOIN url_clicks uc ON su.id = uc.url_id
WHERE su.user_id = ?
GROUP BY su.id
ORDER BY su.created_at DESC
LIMIT 5
");
$stmt->execute([$_SESSION['user_id']]);
$recentLinks = $stmt->fetchAll();
?>
<?php if (empty($recentLinks)): ?>
<p class="no-links">You haven't created any links yet.</p>
<?php else: ?>
<table class="links-table">
<thead>
<tr>
<th>Short URL</th>
<th>Original URL</th>
<th>Clicks</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentLinks as $link): ?>
<tr>
<td>
<a href="<?php echo BASE_SHORT_URL . '/' . ($link['custom_alias'] ?? $link['short_code']); ?>" 
target="_blank">
<?php echo BASE_SHORT_URL . '/' . ($link['custom_alias'] ?? $link['short_code']); ?>
</a>
</td>
<td class="long-url"><?php echo htmlspecialchars(substr($link['long_url'], 0, 50)) . '...'; ?></td>
<td><?php echo $link['clicks']; ?></td>
<td><?php echo date('M j, Y', strtotime($link['created_at'])); ?></td>
<td>
<a href="dashboard.php?code=<?php echo $link['short_code']; ?>" class="btn-small">
Stats
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="view-all">
<a href="dashboard.php" class="btn-link">View All Links →</a>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- QR Code generation script -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/qrcode.min.js"></script>
<script src="js/url-shortener.js"></script>
<?php include 'includes/footer.php'; ?>

6. redirect.php (Redirection Handler)

<?php
/**
* URL Redirection Handler
* This file handles all short URL redirects
*/
require_once 'includes/url-functions.php';
// Get the short code from URL
$requestUri = $_SERVER['REQUEST_URI'];
$pathParts = explode('/', trim($requestUri, '/'));
$shortCode = end($pathParts);
// Check if it's a valid short code
if (empty($shortCode) || $shortCode === 'redirect.php' || $shortCode === 's.php') {
header('Location: ' . APP_URL . '/shorten.php');
exit;
}
// Get URL info
$urlInfo = getUrlInfo($shortCode);
if (!$urlInfo) {
// Link not found or expired
http_response_code(404);
?>
<!DOCTYPE html>
<html>
<head>
<title>Link Not Found</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
color: white;
text-align: center;
}
.container {
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
padding: 3rem;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
max-width: 500px;
}
h1 { font-size: 3rem; margin: 0; }
p { font-size: 1.2rem; margin: 1rem 0 2rem; }
a {
display: inline-block;
padding: 0.8rem 2rem;
background: white;
color: #667eea;
text-decoration: none;
border-radius: 5px;
font-weight: 500;
transition: transform 0.3s;
}
a:hover {
transform: translateY(-2px);
}
</style>
</head>
<body>
<div class="container">
<h1>404</h1>
<p>The link you're looking for doesn't exist or has expired.</p>
<a href="<?php echo APP_URL; ?>/shorten.php">Create Your Own Short Link</a>
</div>
</body>
</html>
<?php
exit;
}
// Check if password protected
if (!empty($urlInfo['password'])) {
// Check if password submitted
if (!isset($_POST['password']) || !password_verify($_POST['password'], $urlInfo['password'])) {
// Show password form
?>
<!DOCTYPE html>
<html>
<head>
<title>Password Protected Link</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.container {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
max-width: 400px;
width: 90%;
}
h2 { margin-bottom: 1rem; color: #333; }
p { color: #666; margin-bottom: 2rem; }
input[type="password"] {
width: 100%;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
margin-bottom: 1rem;
box-sizing: border-box;
}
button {
width: 100%;
padding: 0.8rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
}
.error {
color: #dc3545;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="container">
<h2>🔒 Password Protected Link</h2>
<p>This link is password protected. Please enter the password to continue.</p>
<?php if (isset($_POST['password'])): ?>
<div class="error">Incorrect password. Please try again.</div>
<?php endif; ?>
<form method="POST">
<input type="password" name="password" placeholder="Enter password" required>
<button type="submit">Access Link</button>
</form>
</div>
</body>
</html>
<?php
exit;
}
}
// Record click (only if not a bot)
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (!preg_match('/bot|crawler|spider/i', $ua)) {
recordClick($urlInfo['id'], !empty($urlInfo['password']));
}
// Perform redirect
$longUrl = $urlInfo['long_url'];
// Check for tracking parameters
if (!empty($_GET['utm_source']) || !empty($_GET['utm_medium']) || !empty($_GET['utm_campaign'])) {
// Append UTM parameters if they exist
$separator = (strpos($longUrl, '?') === false) ? '?' : '&';
$params = [];
if (!empty($_GET['utm_source'])) $params[] = 'utm_source=' . urlencode($_GET['utm_source']);
if (!empty($_GET['utm_medium'])) $params[] = 'utm_medium=' . urlencode($_GET['utm_medium']);
if (!empty($_GET['utm_campaign'])) $params[] = 'utm_campaign=' . urlencode($_GET['utm_campaign']);
if (!empty($params)) {
$longUrl .= $separator . implode('&', $params);
}
}
// 301 redirect for permanent links
header('HTTP/1.1 301 Moved Permanently');
header('Location: ' . $longUrl);
exit;

7. css/url-shortener.css

/* URL Shortener Specific Styles */
.shortener-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.shortener-header {
text-align: center;
margin-bottom: 3rem;
}
.shortener-header h1 {
font-size: 2.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 1rem;
}
.shortener-header p {
color: #666;
font-size: 1.1rem;
max-width: 600px;
margin: 0 auto;
}
.shortener-card {
background: white;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
padding: 2rem;
margin-bottom: 3rem;
}
.shortener-form {
max-width: 800px;
margin: 0 auto;
}
.url-input-group {
display: flex;
gap: 0.5rem;
align-items: center;
}
.url-input-group input {
flex: 1;
padding: 1rem;
font-size: 1rem;
}
.btn-shorten {
padding: 1rem 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 5px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
white-space: nowrap;
}
.btn-shorten:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102,126,234,0.4);
}
/* Advanced Options */
.advanced-options {
margin-top: 1.5rem;
}
.options-toggle {
text-align: center;
}
.btn-link {
background: none;
border: none;
color: #667eea;
font-size: 1rem;
cursor: pointer;
text-decoration: underline;
padding: 0.5rem 1rem;
}
.options-panel {
background: #f8f9fa;
border-radius: 5px;
padding: 1.5rem;
margin-top: 1rem;
border: 1px solid #e9ecef;
}
.alias-input-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.domain-prefix {
color: #666;
font-weight: 500;
white-space: nowrap;
}
.alias-input {
flex: 1;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin: 1rem 0;
}
.form-checkbox {
margin: 1rem 0;
color: #666;
}
.form-checkbox label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.help-text {
display: block;
color: #888;
font-size: 0.85rem;
margin-top: 0.3rem;
}
/* Result Cards */
.result-card {
margin-top: 2rem;
padding: 1.5rem;
border-radius: 5px;
}
.result-card.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.result-card.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.result-url {
display: flex;
gap: 0.5rem;
margin: 1rem 0;
}
.result-url input {
flex: 1;
padding: 0.8rem;
border: 1px solid #c3e6cb;
border-radius: 3px;
background: white;
font-size: 1rem;
color: #155724;
}
.btn-copy {
padding: 0.8rem 1.5rem;
background: #28a745;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
transition: opacity 0.3s;
}
.btn-copy:hover {
opacity: 0.9;
}
.existing-note {
margin: 1rem 0;
padding: 0.5rem;
background: #fff3cd;
border: 1px solid #ffeeba;
color: #856404;
border-radius: 3px;
}
.result-actions {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.btn-test, .btn-qr, .btn-stats {
padding: 0.5rem 1rem;
border-radius: 3px;
text-decoration: none;
font-size: 0.9rem;
transition: opacity 0.3s;
cursor: pointer;
border: none;
}
.btn-test {
background: #17a2b8;
color: white;
}
.btn-qr {
background: #6c757d;
color: white;
}
.btn-stats {
background: #007bff;
color: white;
}
/* Features Grid */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin: 3rem 0;
}
.feature-card {
text-align: center;
padding: 1.5rem;
background: white;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transition: transform 0.3s;
}
.feature-card:hover {
transform: translateY(-5px);
}
.feature-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.feature-card h3 {
margin-bottom: 0.5rem;
color: #333;
}
.feature-card p {
color: #666;
font-size: 0.9rem;
line-height: 1.5;
}
/* Recent Links Table */
.recent-links {
background: white;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
padding: 2rem;
}
.recent-links h2 {
margin-bottom: 1.5rem;
color: #333;
}
.links-table {
width: 100%;
border-collapse: collapse;
}
.links-table th {
text-align: left;
padding: 1rem;
background: #f8f9fa;
color: #555;
font-weight: 500;
}
.links-table td {
padding: 1rem;
border-bottom: 1px solid #e9ecef;
}
.links-table tr:hover td {
background: #f8f9fa;
}
.long-url {
color: #666;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn-small {
padding: 0.3rem 0.8rem;
background: #007bff;
color: white;
text-decoration: none;
border-radius: 3px;
font-size: 0.8rem;
}
.no-links {
text-align: center;
padding: 2rem;
color: #888;
font-style: italic;
}
.view-all {
text-align: center;
margin-top: 1.5rem;
}
/* Modal */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 10px;
max-width: 500px;
width: 90%;
position: relative;
text-align: center;
}
.close {
position: absolute;
top: 1rem;
right: 1.5rem;
font-size: 1.5rem;
cursor: pointer;
color: #999;
}
.close:hover {
color: #333;
}
#qrCodeContainer {
margin: 2rem 0;
display: flex;
justify-content: center;
}
.btn-download {
display: inline-block;
padding: 0.8rem 1.5rem;
background: #28a745;
color: white;
text-decoration: none;
border-radius: 3px;
transition: opacity 0.3s;
}
.btn-download:hover {
opacity: 0.9;
}
/* Error Messages */
.error-message {
color: #dc3545;
font-size: 0.85rem;
margin-top: 0.3rem;
min-height: 20px;
}
/* Responsive Design */
@media (max-width: 768px) {
.shortener-header h1 {
font-size: 2rem;
}
.url-input-group {
flex-direction: column;
}
.btn-shorten {
width: 100%;
}
.form-row {
grid-template-columns: 1fr;
}
.result-actions {
flex-direction: column;
}
.links-table {
font-size: 0.9rem;
}
.links-table td, 
.links-table th {
padding: 0.5rem;
}
.long-url {
max-width: 150px;
}
}
@media (max-width: 480px) {
.shortener-card {
padding: 1rem;
}
.result-url {
flex-direction: column;
}
.btn-copy {
width: 100%;
}
.links-table {
display: block;
overflow-x: auto;
}
}
/* Loading State */
.btn-loader {
display: inline-block;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Dashboard Styles */
.dashboard-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin: 2rem 0;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
border-radius: 10px;
text-align: center;
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
margin: 0.5rem 0;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
.chart-container {
background: white;
padding: 1.5rem;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
margin: 2rem 0;
}

8. js/url-shortener.js

/**
* URL Shortener JavaScript
*/
document.addEventListener('DOMContentLoaded', function() {
// Toggle advanced options
const toggleBtn = document.getElementById('toggleOptions');
const optionsPanel = document.getElementById('optionsPanel');
if (toggleBtn && optionsPanel) {
toggleBtn.addEventListener('click', function() {
if (optionsPanel.style.display === 'none') {
optionsPanel.style.display = 'block';
toggleBtn.innerHTML = '⚙️ Hide Advanced Options';
} else {
optionsPanel.style.display = 'none';
toggleBtn.innerHTML = '⚙️ Advanced Options';
}
});
}
// Form validation
const form = document.getElementById('urlShortenerForm');
if (form) {
form.addEventListener('submit', validateForm);
}
// Real-time URL validation
const urlInput = document.getElementById('url');
if (urlInput) {
urlInput.addEventListener('blur', function() {
validateUrl(this.value);
});
}
// Custom alias validation
const aliasInput = document.getElementById('custom_alias');
if (aliasInput) {
aliasInput.addEventListener('blur', function() {
validateAlias(this.value);
});
aliasInput.addEventListener('input', function() {
// Remove invalid characters
this.value = this.value.replace(/[^a-zA-Z0-9\-_]/g, '');
});
}
});
/**
* Validate form before submission
*/
function validateForm(e) {
const url = document.getElementById('url').value;
const alias = document.getElementById('custom_alias')?.value;
let isValid = true;
// Validate URL
if (!validateUrl(url)) {
isValid = false;
}
// Validate alias if provided
if (alias && !validateAlias(alias)) {
isValid = false;
}
if (!isValid) {
e.preventDefault();
showFormError('Please fix the errors above');
} else {
// Show loading state
const btn = document.getElementById('shortenBtn');
btn.querySelector('.btn-text').style.display = 'none';
btn.querySelector('.btn-loader').style.display = 'inline-block';
btn.disabled = true;
}
}
/**
* Validate URL format
*/
function validateUrl(url) {
const errorElement = document.getElementById('url-error');
if (!url) {
showFieldError(errorElement, 'Please enter a URL');
return false;
}
try {
const urlObj = new URL(url);
// Check protocol
if (!urlObj.protocol.match(/^https?:$/)) {
showFieldError(errorElement, 'URL must start with http:// or https://');
return false;
}
// Check hostname
if (!urlObj.hostname) {
showFieldError(errorElement, 'Invalid URL');
return false;
}
clearFieldError(errorElement);
return true;
} catch (err) {
showFieldError(errorElement, 'Please enter a valid URL (include http:// or https://)');
return false;
}
}
/**
* Validate custom alias
*/
function validateAlias(alias) {
const errorElement = document.getElementById('alias-error');
if (!alias) return true; // Alias is optional
const minLength = <?php echo MIN_ALIAS_LENGTH; ?>;
const maxLength = <?php echo MAX_ALIAS_LENGTH; ?>;
if (alias.length < minLength) {
showFieldError(errorElement, `Alias must be at least ${minLength} characters`);
return false;
}
if (alias.length > maxLength) {
showFieldError(errorElement, `Alias cannot exceed ${maxLength} characters`);
return false;
}
if (!/^[a-zA-Z0-9\-_]+$/.test(alias)) {
showFieldError(errorElement, 'Alias can only contain letters, numbers, hyphens, and underscores');
return false;
}
clearFieldError(errorElement);
return true;
}
/**
* Show field error
*/
function showFieldError(element, message) {
if (element) {
element.textContent = message;
// Highlight corresponding input
const inputId = element.id.replace('-error', '');
const input = document.getElementById(inputId);
if (input) {
input.classList.add('error');
}
}
}
/**
* Clear field error
*/
function clearFieldError(element) {
if (element) {
element.textContent = '';
// Remove highlight from input
const inputId = element.id.replace('-error', '');
const input = document.getElementById(inputId);
if (input) {
input.classList.remove('error');
}
}
}
/**
* Show form error message
*/
function showFormError(message) {
// Create or update error alert
let alert = document.querySelector('.form-alert.error');
if (!alert) {
alert = document.createElement('div');
alert.className = 'form-alert error';
const form = document.getElementById('urlShortenerForm');
form.insertBefore(alert, form.firstChild);
}
alert.textContent = message;
alert.style.display = 'block';
// Auto hide after 5 seconds
setTimeout(() => {
alert.style.display = 'none';
}, 5000);
}
/**
* Copy to clipboard
*/
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// Show success tooltip
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = '✓ Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
});
}
/**
* Show QR Code modal
*/
function showQRCode(url) {
const modal = document.getElementById('qrModal');
const container = document.getElementById('qrCodeContainer');
const downloadBtn = document.getElementById('downloadQR');
// Clear previous QR code
container.innerHTML = '';
// Generate new QR code
new QRCode(container, {
text: url,
width: <?php echo QR_CODE_SIZE; ?>,
height: <?php echo QR_CODE_SIZE; ?>,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.H
});
// Set download link
setTimeout(() => {
const canvas = container.querySelector('canvas');
if (canvas) {
downloadBtn.href = canvas.toDataURL('image/png');
}
}, 100);
// Show modal
modal.style.display = 'flex';
// Close button functionality
const closeBtn = modal.querySelector('.close');
closeBtn.onclick = function() {
modal.style.display = 'none';
};
// Close when clicking outside
window.onclick = function(event) {
if (event.target === modal) {
modal.style.display = 'none';
}
};
}
/**
* Load analytics chart
*/
function loadAnalyticsChart(urlId) {
// This would use Chart.js or similar library
// Example with Chart.js:
/*
fetch(`includes/url-analytics.php?id=${urlId}`)
.then(response => response.json())
.then(data => {
const ctx = document.getElementById('clicksChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: data.dates,
datasets: [{
label: 'Clicks',
data: data.clicks,
borderColor: '#667eea',
tension: 0.1
}]
}
});
});
*/
}

9. .htaccess (for Apache servers)

# .htaccess file for URL shortener
RewriteEngine On
# Redirect all requests to short URLs to redirect.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-zA-Z0-9\-_]+)$ redirect.php?code=$1 [L,QSA]
# Block access to sensitive files
<FilesMatch "\.(env|sql|log|ini)$">
Order allow,deny
Deny from all
</FilesMatch>
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
</IfModule>
# Set caching headers
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
</IfModule>

🚀 How to Use This Project Step by Step

Step 1: Environment Setup

  1. Ensure PHP 7.4+ and MySQL are installed
  2. Install Composer if not already installed
  3. Enable Apache mod_rewrite for clean URLs

Step 2: Database Setup

  1. Create a new database called url_shortener
  2. Import database/url-shortener.sql
  3. Update database credentials in .env

Step 3: Install Dependencies

Navigate to project root and run:

composer install

Step 4: Configure Environment

  1. Copy .env.example to .env
  2. Update all configuration values:
  • Database credentials
  • Short domain (e.g., s.yourdomain.com)
  • Rate limiting settings
  • QR code preferences

Step 5: Configure Apache

  1. Ensure mod_rewrite is enabled
  2. Update virtual host or .htaccess
  3. Set document root to project directory

Step 6: Test the System

  1. Navigate to http://localhost/blog-website/shorten.php
  2. Test URL shortening:
  • Enter a long URL
  • Try custom aliases
  • Test password protection
  1. Verify redirects work:
  • Click on shortened links
  • Check password protection

Step 7: Analytics Testing

  1. Create multiple links
  2. Click them from different browsers/devices
  3. Check analytics dashboard

🔒 Security Features

Implemented Security Measures:

  • ✅ Rate limiting per IP address
  • ✅ URL validation and sanitization
  • ✅ Blocked domains list
  • ✅ Password protection for sensitive links
  • ✅ SQL injection prevention (prepared statements)
  • ✅ XSS protection (htmlspecialchars)
  • ✅ CSRF tokens for forms
  • ✅ Input length restrictions

URL Validation:

  • ✅ Check URL format
  • ✅ Verify URL accessibility
  • ✅ Block local/internal URLs
  • ✅ Restrict protocols (http/https only)
  • ✅ Domain blacklisting

Spam Prevention:

  • ✅ Rate limiting by IP
  • ✅ Duplicate URL detection
  • ✅ Bot detection in analytics
  • ✅ Maximum URL length limits

📊 Analytics Features

Tracked Metrics:

  • Total Clicks: Overall link performance
  • Daily/Weekly Trends: Click patterns over time
  • Geographic Data: Country and city-level tracking
  • Device Types: Desktop, mobile, tablet breakdown
  • Browser Statistics: Most used browsers
  • Referrer Data: Where clicks originate
  • Operating Systems: Windows, macOS, Linux, etc.

Analytics Display:

  • Interactive charts and graphs
  • Export data as CSV
  • Real-time click monitoring
  • Detailed per-link reports

🎯 API Integration

REST API Endpoints (in includes/url-api.php):

// Create short URL
POST /api/shorten
{
"url": "https://example.com/long-url",
"custom_alias": "my-link",
"password": "optional"
}
// Get link info
GET /api/info/{code}
// Get link stats
GET /api/stats/{code}
// Delete link
DELETE /api/delete/{code}

🔧 Troubleshooting

Common Issues:

  1. 404 errors on short URLs:
  • Check .htaccess configuration
  • Ensure mod_rewrite is enabled
  • Verify BASE_SHORT_URL setting
  1. Database connection errors:
  • Check credentials in .env
  • Verify MySQL is running
  • Ensure database exists
  1. QR codes not generating:
  • Check GD extension is enabled
  • Verify write permissions on uploads/qrcodes/
  • Check QR_CODE_SIZE setting
  1. Rate limiting too strict:
  • Adjust RATE_LIMIT_ATTEMPTS in .env
  • Check IP detection behind proxy
  • Clear rate_limit table if needed
  1. Links not redirecting:
  • Verify redirect.php is accessible
  • Check error logs
  • Test with simple URL first

🚀 Future Enhancements

  1. User Accounts: Register and manage all links
  2. Link Groups: Organize links into categories
  3. Bulk Operations: Import/export multiple links
  4. Custom Domains: Allow users to use their own domain
  5. API Access Tokens: For third-party integration
  6. Webhook Notifications: Get notified on link clicks
  7. A/B Testing: Rotate between multiple destination URLs
  8. Retargeting Pixels: Add tracking pixels to redirects
  9. Link Expiry Webhooks: Notify when links expire
  10. Advanced Fraud Detection: Machine learning for bot detection

📝 Conclusion

This URL shortener system provides a complete solution for creating, managing, and tracking shortened URLs. With comprehensive analytics, security features, and a user-friendly interface, it's perfect for bloggers, marketers, and developers who need reliable link management. The system is built with scalability in mind and can handle thousands of redirects per day while maintaining detailed analytics for each link.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper