/*
Simple URL shortener with preview page
- Node.js + Express
- SQLite for storage
Features:
- POST /shorten -> { url, title?, image_url?, auto_redirect_seconds? }
returns a short URL using BASE_DOMAIN config
- GET /:code -> shows a preview page with image + "Open link" button
If auto_redirect_seconds is set, page will redirect after that many seconds
- GET /admin -> simple listing of links (for testing)
Run:
1) npm init -y
2) npm install express sqlite3 body-parser valid-url
3) node simple-url-shortener-with-preview.js
Environment variables:
- PORT (default 3000)
- BASE_DOMAIN (default http://localhost:3000) <-- set this to your "সিম অফ চ্যাট" domain
Example curl to shorten:
curl -X POST -H "Content-Type: application/json" -d '{"url":"https://example.com","title":"Example","image_url":"https://picsum.photos/1200/600","auto_redirect_seconds":5}' http://localhost:3000/shorten
*/
const express = require('express');
const bodyParser = require('body-parser');
const sqlite3 = require('sqlite3').verbose();
const { isWebUri } = require('valid-url');
const path = require('path');
const PORT = process.env.PORT || 3000;
const BASE_DOMAIN = (process.env.BASE_DOMAIN || `http://localhost:${PORT}`).replace(/\/$/, "");
// --- Setup DB ---
const db = new sqlite3.Database(':memory:');
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE,
target_url TEXT,
title TEXT,
image_url TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
auto_redirect_seconds INTEGER
)`);
});
// --- Helpers ---
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
function encodeBase62(num) {
if (num === 0) return '0';
let s = '';
while (num > 0) {
s = alphabet[num % 62] + s;
num = Math.floor(num / 62);
}
return s;
}
function generateCodeFromId(id) {
return encodeBase62(id);
}
// sanitize URL: ensure has protocol
function normalizeUrl(u) {
if (!u) return null;
u = u.trim();
if (!/^https?:\/\//i.test(u)) u = 'http://' + u;
return u;
}
// --- App ---
const app = express();
app.use(bodyParser.json());
app.use(express.urlencoded({ extended: true }));
app.use('/static', express.static(path.join(__dirname, 'static')));
// Shorten endpoint
app.post('/shorten', (req, res) => {
try {
let { url, title, image_url, auto_redirect_seconds } = req.body;
if (!url) return res.status(400).json({ error: 'url is required' });
url = normalizeUrl(url);
if (!isWebUri(url)) return res.status(400).json({ error: 'invalid url' });
if (image_url) {
image_url = normalizeUrl(image_url);
if (!isWebUri(image_url)) image_url = null;
}
auto_redirect_seconds = parseInt(auto_redirect_seconds) || null;
// Insert row first to get numeric id
const stmt = db.prepare('INSERT INTO links (target_url, title, image_url, auto_redirect_seconds) VALUES (?, ?, ?, ?)');
stmt.run(url, title || null, image_url || null, auto_redirect_seconds, function(err) {
if (err) return res.status(500).json({ error: 'db insert error', details: err.message });
const id = this.lastID;
const code = generateCodeFromId(id);
db.run('UPDATE links SET code = ? WHERE id = ?', [code, id], function(err2) {
if (err2) return res.status(500).json({ error: 'db update error', details: err2.message });
const short = `${BASE_DOMAIN}/${code}`;
return res.json({ short, code, target: url, title: title || null, image_url: image_url || null, auto_redirect_seconds });
});
});
stmt.finalize();
} catch (e) {
return res.status(500).json({ error: e.message });
}
});
// Redirect / preview handler
app.get('/:code', (req, res) => {
const code = req.params.code;
db.get('SELECT * FROM links WHERE code = ?', [code], (err, row) => {
if (err) return res.status(500).send('DB error');
if (!row) return res.status(404).send('${escapeHtml(title)}
`);
});
});
// Simple admin list for testing
app.get('/admin', (req, res) => {
db.all('SELECT * FROM links ORDER BY id DESC LIMIT 200', [], (err, rows) => {
if (err) return res.status(500).send('DB error');
let html = `Links
';
res.send(html);
});
});
// health
app.get('/health', (req, res) => res.json({ ok: true }));
app.listen(PORT, () => console.log(`URL shortener running at ${BASE_DOMAIN} (port ${PORT})`));
// --- small util ---
function escapeHtml(s){
if (!s) return '';
return String(s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
---
# BODY SNIPPET (for quick copy)
Below is the HTML `` section used in the preview page. I added it here so you can quickly copy just the body markup without the rest of the script.
```html
```
(এই অংশটি আমি ক্যানভাসে "BODY SNIPPET" নামে যোগ করে দিলাম। আপনি চাইলে আমি এখানেই কপি করে দিতে পারি — বলুন।)
Link not found
'); // If there's no image_url, use a scenic placeholder const image = row.image_url || `https://picsum.photos/1200/600?random=${row.id}`; const title = row.title || 'Preview'; const target = row.target_url; const autoSec = row.auto_redirect_seconds; // render simple HTML preview page res.send(`${escapeHtml(title)}
এই লিংকটি টেস্ট করার জন্য নিচের বোতনে ক্লিক করুন।
${autoSec ? `এই পেজটি ${autoSec} সেকেন্ড পর স্বয়ংক্রিয়ভাবে রিডাইরেক্ট হবে...
` : ''}Links
id | code | target | image | created |
---|---|---|---|---|
${r.id} | ${r.code} | ${escapeHtml(r.target_url)} | ${r.image_url ? `img` : '-'} | ${r.created_at} |
${escapeHtml(title)}
এই লিংকটি টেস্ট করার জন্য নিচের বোতনে ক্লিক করুন।
${autoSec ? `এই পেজটি ${autoSec} সেকেন্ড পর স্বয়ংক্রিয়ভাবে রিডাইরেক্ট হবে...
` : ''}
Comments
Post a Comment