├── .env.sample ├── .gitignore ├── README.md ├── functions ├── details.js ├── go.js └── short-url.js ├── netlify.toml ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── css │ └── main.css ├── img │ ├── backgrounds │ │ ├── bg-header.png │ │ └── partners.png │ └── sprites │ │ └── bg-brazillian-creators.png ├── index.html └── js │ └── main.js ├── tailwind.config.js └── yarn.lock /.env.sample: -------------------------------------------------------------------------------- 1 | REDIS_HOST=your_redis_host 2 | REDIS_PORT=your_redis_port 3 | REDIS_PASSWORD=your_redis_password 4 | BASE_URL=https://serverless-url-shortener-upstash.netlify.app 5 | RECAPTCHA_PRIVATE_KEY=your_google_key -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | dist/ 3 | node_modules/ 4 | # Local Netlify folder 5 | .netlify -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Short URL with Serverless + Redis + Netlify 2 | ### Create your own "short url" service in minutes, not days. 3 | ##### Try it here: https://serverless-url-shortener-upstash.netlify.app 4 | --- 5 | This project aims to create a scalable and open-source solution alternative to Bit.ly, Tinyurl, Goo.gl, etc. If you want: 6 | 7 | - Create unlimited shorted urls in your own cloud 8 | - Use a custom domain of yours 9 | - Scalable and cheap solution 10 | - ✨Magic ✨ 11 | 12 | This is **exactly** for you. 13 | 14 | ## How to install 15 | The installation is simple once you are registered in **Netlify**. Please note that you can easily change this serverless function to work with other alternatives if you are used to serverless functions (basically just change the entry point and the request body). 16 | 17 | ### Requirements 18 | - Create an account in Netlify (it's free) 19 | - Create an account in Upstash to use a free Redis database (or use your own cloud Redis solution) 20 | - This project was made using Node v14.15.5 21 | - The npm version used to install the packages is v6.14.11 22 | 23 | ### Installation 24 | - Clone this project 25 | - Run **npm run install** in the projects folder 26 | - Configure your environment variables in **Netlify** (see below how) 27 | - Push your project to **Netlify** or run **netlify dev --live** (note: you must install netlify-cli before, see how [here.](https://docs.netlify.com/cli/get-started/) 28 | 29 | ### Configuring Netlify Environment Variables 30 | The good thing about Netlify is that we can use our environment variables in production and local. You must configure the following variables to work with this project without problems when running netlify dev --live: 31 | 32 | Environment Variable | Description | Example 33 | --- | --- | --- 34 | REDIS_HOST | Your redis **host** (upstash or other alternative) | 127.0.0.1 35 | REDIS_PORT | Your redis **port** (upstash or other alternative) | 66587 36 | REDIS_PASSWORD | Your redis **password** (upstash or other alternative) | 127.0.0.1 37 | BASE_URL | Your netlify base URL (production or development) | https://serverless-url-shortener-upstash.netlify.app/ 38 | RECAPTCHA_PRIVATE_KEY | Your google private recaptcha key | 39 | 40 | All those variables are mandatory and the system won't work without them. They are accessed via proccess.env. 41 | 42 | ### Good to go? 43 | 1. Create account in service providers 44 | 2. Setup environment variables 45 | 3. Install the project 46 | 4. Install netlify-cli 47 | 5. Install dependencies with npm install 48 | 6. Run the project with **netlify dev --live** 49 | 7. Try it free. 50 | 51 | If you want to talk, you get find me here: msfbr.00@gmail.com -------------------------------------------------------------------------------- /functions/details.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serverless Function Requirements 3 | */ 4 | const redis = require('redis'); 5 | const asyncRedis = require("async-redis"); 6 | const redisClient = asyncRedis.createClient({ 7 | host : process.env.REDIS_HOST, 8 | port : process.env.REDIS_PORT, 9 | password: process.env.REDIS_PASSWORD 10 | }); 11 | 12 | exports.handler = async function(event, context) { 13 | if(event.httpMethod != 'GET') { 14 | return { 15 | statusCode: 200, 16 | body: JSON.stringify({ 17 | "success": false, 18 | "message": "Invalid method." 19 | }) 20 | }; 21 | } 22 | 23 | const splitUrlPathAndGetLast = event.path.substring(1).split('/').pop(); 24 | const getRedirectUrlVisits = await redisClient.get(`url_visits_${splitUrlPathAndGetLast}`); 25 | 26 | if(getRedirectUrlVisits == null) { 27 | return { 28 | statusCode: 200, 29 | body: JSON.stringify({ 30 | "success": false, 31 | "message": "Invalid code." 32 | }) 33 | }; 34 | } 35 | 36 | return { 37 | statusCode: 200, 38 | body: JSON.stringify({ 39 | "success": true, 40 | "message": "Visits retrieved.", 41 | "data" : { 42 | "visitsCount": parseInt((!getRedirectUrlVisits) ? 0:getRedirectUrlVisits) 43 | } 44 | }) 45 | }; 46 | } -------------------------------------------------------------------------------- /functions/go.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serverless Function Requirements 3 | */ 4 | const redis = require('redis'); 5 | const asyncRedis = require("async-redis"); 6 | const redisClient = asyncRedis.createClient({ 7 | host : process.env.REDIS_HOST, 8 | port : process.env.REDIS_PORT, 9 | password: process.env.REDIS_PASSWORD 10 | }); 11 | 12 | exports.handler = async function(event, context) { 13 | if(event.httpMethod != 'GET') { 14 | return { 15 | statusCode: 200, 16 | body: JSON.stringify({ 17 | "success": false, 18 | "message": "Invalid method." 19 | }) 20 | }; 21 | } 22 | 23 | const splitUrlPathAndGetLast = event.path.substring(1).split('/').pop(); 24 | const getRedirectUrl = await redisClient.get(`url_${splitUrlPathAndGetLast}`); 25 | const incrementRedirectUrlVisits = await redisClient.incr(`url_visits_${splitUrlPathAndGetLast}`); 26 | 27 | return { 28 | statusCode: 301, 29 | headers: { 30 | Location: getRedirectUrl 31 | }, 32 | body: '' 33 | }; 34 | } -------------------------------------------------------------------------------- /functions/short-url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serverless Function Requirements 3 | */ 4 | const fetch = require("node-fetch"); 5 | const redis = require('redis'); 6 | const asyncRedis = require("async-redis"); 7 | const redisClient = asyncRedis.createClient({ 8 | host : process.env.REDIS_HOST, 9 | port : process.env.REDIS_PORT, 10 | password: process.env.REDIS_PASSWORD 11 | }); 12 | 13 | /** 14 | * Shorter App Configurations 15 | */ 16 | const shortUrlSize = 8; 17 | const recaptchaPrivateKey = process.env.RECAPTCHA_PRIVATE_KEY; 18 | 19 | /** 20 | * Creates a random string. 21 | * 22 | * @source https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript 23 | * @param {Number} length 24 | * @returns 25 | */ 26 | function makeid(length) { 27 | var result = ''; 28 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 29 | var charactersLength = characters.length; 30 | 31 | for ( var i = 0; i < length; i++ ) { 32 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 33 | } 34 | 35 | return result; 36 | } 37 | 38 | /** 39 | * Generates unique random string for a given URL. 40 | * 41 | * @param {Number} String length 42 | * @param {String} Destination url (where the user will be redirected) 43 | * @returns {Object} 44 | */ 45 | async function generateUniqueShorterUrl(length, destinationUrl) { 46 | let uniqueString = makeid(length); 47 | let shorterUrlExists = await redisClient.get(uniqueString); 48 | 49 | if(shorterUrlExists) { 50 | return generateUniqueShorterUrl(length); 51 | } 52 | 53 | let saveUrl = await redisClient.set(`url_${uniqueString}`, destinationUrl); 54 | let incrementSavedUrlVisits = await redisClient.incr(`url_visits_${uniqueString}`); 55 | let totalSavedUrls = await redisClient.incr('total_saved_urls'); 56 | 57 | return { 58 | "success": (saveUrl == "OK") ? true:false, 59 | "shorterUrl": `${process.env.BASE_URL}/r/${uniqueString}`, 60 | "destinationUrl": destinationUrl, 61 | } 62 | } 63 | 64 | /** 65 | * Verify if the given recaptcha token is valid. 66 | * 67 | * @param {String} Token generated from the front-end. 68 | * @returns Boolean 69 | */ 70 | async function validateRecaptchaToken(token) { 71 | const validateRecaptchaRequest = await fetch('https://www.google.com/recaptcha/api/siteverify', { 72 | method: 'POST', 73 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 74 | body: `secret=${recaptchaPrivateKey}&response=${token}` 75 | }); 76 | 77 | const recaptchaResponse = await validateRecaptchaRequest.json(); 78 | return recaptchaResponse; 79 | } 80 | 81 | exports.handler = async function(event, context) { 82 | if(event.httpMethod != 'POST') { 83 | return { 84 | statusCode: 200, 85 | body: JSON.stringify({ 86 | "success": false, 87 | "message": "Invalid method." 88 | }) 89 | } 90 | } 91 | 92 | const request = JSON.parse(event.body); 93 | const validateRequestWithRecaptcha = await validateRecaptchaToken(request.token); 94 | 95 | if(!validateRequestWithRecaptcha.success) { 96 | return { 97 | statusCode: 200, 98 | body: JSON.stringify({ 99 | "success": false, 100 | "message": "Oops. Try again. Recaptcha validation failed.", 101 | }) 102 | } 103 | } 104 | 105 | const generateShorterUrl = await generateUniqueShorterUrl(shortUrlSize, request.url); 106 | 107 | return { 108 | statusCode: 200, 109 | body: JSON.stringify(generateShorterUrl) 110 | }; 111 | } -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "functions" 3 | 4 | [dev] 5 | functions = "functions" 6 | 7 | [[redirects]] 8 | from = "/app/short-url" 9 | to = "/.netlify/functions/short-url" 10 | status = 200 11 | 12 | [[redirects]] 13 | from = "/r/*" 14 | to = "/.netlify/functions/go/:splat" 15 | status = 200 16 | 17 | [[redirects]] 18 | from = "/v/*" 19 | to = "/.netlify/functions/details/:splat" 20 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "scripts": { 4 | "dev": "parcel src/index.html", 5 | "build": "parcel build src/index.html" 6 | }, 7 | "devDependencies": { 8 | "@fullhuman/postcss-purgecss": "^4.0.2", 9 | "autoprefixer": "^9", 10 | "parcel-bundler": "^1.12.4", 11 | "postcss": "^7", 12 | "redis": "^3.0.2", 13 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.4" 14 | }, 15 | "dependencies": { 16 | "async-redis": "^1.1.7", 17 | "node-fetch": "^2.6.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const purgecss = require('@fullhuman/postcss-purgecss')({ 2 | // Specify the paths to all of the template files in your project 3 | content: [ 4 | './src/**/*.html', 5 | './src/**/*.vue', 6 | './src/**/*.jsx', 7 | // etc. 8 | ], 9 | 10 | // Include any special characters you're using in this regular expression 11 | defaultExtractor: (content) => content.match(/[\w-/:]+(? 2 | 3 |
4 | 5 | 6 | 7 | 8 |Paste the URL code above and see how many visits you got in your link.
52 | 53 |