Simple url shortener backend
This is only intended for personal use, possibly giving urls to a few friends. Nothing that requires scaling.
This is made by Gemini using the following prompts.
The frontend then sends JSON to the backend of the form
{
"short": "never",
"url": "https://gonna.give.you.up/"
}
To delete, set the url to !delete.
Then one can write a simple frontend with Javascript.
CORS headers are sent in the cors.php file if needed (e.g. if the add short interface is on a different domain than the get short frontend, nice for security as the main user-facing frontend domain has no capacity to add or delete).
<?php
header("Access-Control-Allow-Origin: *"); // or specify your domain, e.g. "https://example.com"
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Content-Type: application/json");
A backend file for a given table then needs to be merely
<?php
define('PREFIX','bobbins');
require('short_be.php');
A frontend script does
<?php
define('BACKEND',"never_gonna.php");
define('LIST_URL',"https://give.you.up/.list");
define('SHORT_URL_PREFIX','https://never.gonna.let.you.down.com/');
require_once("add_short_common.php");
1 Prompt for Gemini
2 Prompt for Gemini
Example reporting errors (from Gemini)
fetch('api/index.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ short: 'nas', url: 'http://nas-server/storage' })
})
.then(response => {
if (!response.ok) {
// This catches 500 errors and logs the PHP output to console
response.text().then(text => console.error("PHP Error:", text));
}
return response.json();
})
.then(data => {
if (data.message) console.log("Success:", data.message);
if (data.error) console.warn("App Error:", data.error);
});
table create (from Gemini)
table create php (from Gemini)
<?php
// Enable error reporting for setup
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once("dbauth.php");
try {
// 1. Connect to MySQL
$mysqli = new mysqli(HOST, USER, PASS, DB, PORT);
if ($mysqli->connect_error) {
throw new Exception("Connection failed: " . $mysqli->connect_error);
}
// 2. Prepare the Table Name
$tableName = PREFIX . "_short_to_url";
// 3. Define the SQL
// We use VARCHAR(191) for 'short' to ensure compatibility with utf8mb4 indexing
$sql = "CREATE TABLE IF NOT EXISTS `$tableName` (
`short` VARCHAR(191) NOT NULL,
`url` TEXT NOT NULL,
PRIMARY KEY (`short`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
// 4. Execute
if ($mysqli->query($sql) === TRUE) {
echo "<h3>Success!</h3>";
echo "Table <strong>$tableName</strong> is ready.";
} else {
throw new Exception("Error creating table: " . $mysqli->error);
}
$mysqli->close();
} catch (Exception $e) {
echo "<h3>Setup Failed</h3>";
echo "Error: " . $e->getMessage();
}
PHP Backend
<?php
/**
* Error Reporting Toggle
*/
$DEBUG_MODE = true;
if ($DEBUG_MODE) {
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
} else {
ini_set('display_errors', 0);
error_reporting(0);
}
// Security and Credentials
require_once("cors.php");
require_once("bouncer.php");
require_once("dbauth.php");
header('Content-Type: application/json');
/**
* URL Manager Class
*/
class UrlManager {
private $db;
private $table;
public function __construct() {
// Using the constants defined in dbauth.php
$this->db = new mysqli(HOST, USER, PASS, DB, PORT);
if ($this->db->connect_error) {
throw new Exception("Connection failed: " . $this->db->connect_error);
}
// table name: PREFIX_short_to_url
$this->table = PREFIX . "_short_to_url";
}
public function handleRequest($jsonInput) {
$data = json_decode($jsonInput, true);
// Validation: Basic structure
if (!$data || !isset($data['short']) || !isset($data['url'])) {
return ["error" => "Invalid JSON: 'short' and 'url' fields required."];
}
$short = trim($data['short']);
$url = trim($data['url']);
if (empty($short)) {
return ["error" => "The 'short' value cannot be empty."];
}
// Action: Delete
if ($url === "!delete") {
return $this->deleteShort($short);
}
// Action: Add/Update
// Validates protocol; accepts LAN domains (no period required)
if (preg_match('~^(?:f|ht)tps?://~i', $url)) {
return $this->upsertShort($short, $url);
}
return ["error" => "Invalid URL format or command."];
}
private function upsertShort($short, $url) {
$sql = "INSERT INTO {$this->table} (short, url) VALUES (?, ?)
ON DUPLICATE KEY UPDATE url = VALUES(url)";
$stmt = $this->db->prepare($sql);
$stmt->bind_param("ss", $short, $url);
if ($stmt->execute()) {
return [
"message" => "added $short => $url",
"short" => $short,
"url" => $url
];
}
return ["error" => "DB Execution failed: " . $this->db->error];
}
private function deleteShort($short) {
$stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE short = ?");
$stmt->bind_param("s", $short);
if ($stmt->execute()) {
return ["message" => "deleted $short"];
}
return ["error" => "DB Execution failed: " . $this->db->error];
}
}
/**
* Execution
*/
try {
$manager = new UrlManager();
$rawInput = file_get_contents('php://input');
$response = $manager->handleRequest($rawInput);
echo json_encode($response);
} catch (Exception $e) {
// If PHP crashes, this is caught here.
// Client logs response text to console.
http_response_code(500);
echo json_encode(["error" => $e->getMessage()]);
}
dbauth.php defines the constants for the database HOST, USER, PASS, DB, PORT.
bouncer.php is arbitrary PHP code that determines if the request is authorised, and errors out if not.
add_short_common.php - Web frontend to set shorts
<?php
require_once("bouncer.php");
$qs = $_SERVER["QUERY_STRING"];
if( $qs != "" ) {
$qs = urldecode($qs);
$qs = preg_replace("@['\"]@","",$qs);
$val = "value='$qs'";
$af1 = "";
$af2 = "autofocus";
} else {
$val = "";
$af1 = "autofocus";
$af2 = "";
}
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>URL Shortener</title>
<style>
body { font-family: sans-serif; max-width: 400px; margin: 2em auto; }
label { display: block; margin-top: 1em; }
input { width: 100%; padding: 0.5em; }
button { margin-top: 1em; padding: 0.5em 1em; }
#message { margin-top: 1em; font-weight: bold; }
</style>
</head>
<body>
<h1>Add a Short URL</h1>
<label for="short">Short code:</label>
<input type="text" id="short" placeholder="e.g. abc123, leave blank for auto generate" <?php echo $af1." ".$val; ?>>
<label for="url">Full URL:</label>
<input type="text" id="url" placeholder="e.g. https://example.com/page" <?php echo $af2; ?>>
<button id="submit">Save</button>
<div id="message"></div>
<script>
function getmods(e) {
let t = ""
if( e.shiftKey ) t = "S-"+t
if( e.metaKey ) t = "M-"+t
if( e.ctrlKey ) t = "C-"+t
if( e.altKey ) t = "A-"+t
return t
}
function acms(e) {
let key = e.key
const code = e.code.toLowerCase()
if( code === "backquote" ) { key = "`" }
if( code.startsWith("digit") ) { key = code.substr(5) }
if( code === "space" ) { key = "space" }
if( key === "-" ) { key = "minus" }
if( key === "_" ) { key = "minus" }
if( key === "+" ) { key = "equals" }
let t = key.toLowerCase()
t = getmods(e)+t
console.log("acms",t)
return t
}
window.addEventListener("keydown", e => {
const k = acms(e)
switch(k) {
case "C-s":
e.preventDefault()
return dosubmit()
case "A-l":
e.preventDefault()
return dolist()
}
})
document.getElementById("submit").addEventListener("click", dosubmit)
function dolist() {
window.location.href = "<?php echo LIST_URL; ?>"
}
async function dosubmit() {
const short = document.getElementById("short").value.trim()
const url = document.getElementById("url").value.trim()
const message = document.getElementById("message")
message.textContent = ""
if (!url) {
message.textContent = "Please fill in the url."
return
}
if( url === "!delete" ) {
await do_delete(short)
} else {
await do_insert(short,url)
}
}
async function do_insert(short,url) {
const message = document.getElementById("message")
try {
const reqbody = JSON.stringify({ short, url })
console.log(reqbody)
const response = await fetch("<?php echo BACKEND; ?>", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: reqbody
})
const text = await response.text()
console.log(text)
const data = JSON.parse(text)
console.log({data})
if( data.short ) {
if( data.short === "#fail" ) {
message.textContent = "#fail"
} else {
const short = data.short
const short_url = `<?php echo SHORT_URL_PREFIX; ?>${short}`
message.textContent = ""
let span = document.createElement("span")
span.textContent = short
message.append(span)
message.append(document.createTextNode(" "))
let a = document.createElement("a")
a.textContent = short_url
a.href = short_url
message.append(a)
}
} else {
message.textContent = data.message || "Done."
}
} catch (err) {
message.textContent = "Error: " + err
}
}
async function do_delete(short) {
const message = document.getElementById("message")
try {
const reqbody = JSON.stringify({ short, url:"!delete" })
console.log(reqbody)
const response = await fetch("insert_short.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: reqbody
})
const text = await response.text()
console.log(text)
const data = JSON.parse(text)
console.log({data})
if( data.message ) {
message.textContent = data.message || "Done."
} else {
message.textContent = "no message"
}
} catch (err) {
message.textContent = "Error: " + err
}
}
</script>
</body>
</html>
Redirector
<?php
require_once("db.php");
$conn = new mysqli(HOST,USER,PASS,DB);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$stmt = $conn->prepare("SELECT id, short, url FROM ".PREFIX."_short_to_url WHERE short = ?");
$stmt->bind_param("s", $req);
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
$url = $row["url"];
$url = str_replace("%s",$qstring,$url);
} else {
$url = null;
}
$stmt->close();
$conn->close();
<?php
require_once("db.php");
require_once("query_core.php");
if( !is_null($url) ) {
if (filter_var($url, FILTER_VALIDATE_URL)) {
header("Location: " . $url, true, 302); // 302 = temporary redirect
exit();
} else {
http_response_code(500);
$message = "Stored URL is invalid. '$url'";
$message_class = "error";
}
} else {
$message = "No URL found for short code '$req'";
$message_class = "notfound";
}