├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── functions └── redirect.js ├── netlify.toml ├── other ├── README.md └── shorten.js ├── package.json └── public └── static ├── robots.txt └── wp-login.php /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .netlify 3 | .DS_Store 4 | .env 5 | .env.* 6 | 7 | # these cause more harm than good 8 | # when working with contributors 9 | package-lock.json 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | node_modules 3 | dist 4 | coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": false, 9 | "jsxBracketSameLine": false 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # airtable-netlify-short-urls 3 | 4 | :warning: **There's a simpler version using [Netlify redirects](https://www.netlify.com/docs/redirects/) instead of Airtable [here](https://github.com/kentcdodds/netlify-shortener)** 5 | 6 | This is a simple short-url service that works with 7 | [netlify functions](https://www.netlify.com/docs/functions/) and uses 8 | [airtable](https://airtable.com). 9 | 10 | It's recommended to use this with 11 | [CloudFlare caching](https://support.cloudflare.com/hc/en-us/articles/200168326-Are-301-and-302-redirects-cached-by-Cloudflare-) 12 | because airtable has a limit of 5 requests per second. Also, CloudFlare can give 13 | you nice analytics for free. 14 | 15 | ## Usage 16 | 17 | First, setup an airtable account with a base and table. The table should have 18 | a column for the short code and one for the long link. 19 | 20 | Next deploy this github repo to netlify: 21 | 22 | [](https://app.netlify.com/start/deploy?repository=https://github.com/kentcdodds/airtable-netlify-short-urls) 23 | 24 | Then set the following environment variables in netlify: 25 | 26 | ``` 27 | DEFAULT_REDIRECT_URL -> https://example.com 28 | AIRTABLE_KEY -> ***************** 29 | AIRTABLE_BASE -> ***************** 30 | AIRTABLE_TABLE -> URLs 31 | AIRTABLE_SHORT_CODE_FIELD -> Short Code 32 | AIRTABLE_LONG_LINK_FIELD -> Long Link 33 | ENABLE_CACHE -> false 34 | ``` 35 | 36 | > Note: `AIRTABLE_TABLE`, `AIRTABLE_SHORT_CODE_FIELD`, 37 | > `AIRTABLE_LONG_LINK_FIELD`, and `ENABLE_CACHE` are showing the default values 38 | > above. If that's what you call your table and fields then you don't need to 39 | > set those variables. 40 | 41 | > Note also that you can use a `.env` file instead, just don't commit this to 42 | > source control :) (this is useful for local development as `.env` is in the 43 | > `.gitignore`). 44 | 45 | Redirects should be setup automatically for you in the `netlify.toml`, so you 46 | shouldn't have to do anything there. 47 | 48 | Now go ahead and test that your redirects are working as expected. Just go to 49 | the short URL version of your netlify app and it should redirect you like so: 50 | http://jsair.netlify.com/first -> https://javascriptair.com/episodes/2015-12-09/ 51 | 52 | If that works you're on the right track! 53 | 54 | Next, [set up Netlify with a custom domain](https://www.netlify.com/docs/custom-domains/) 55 | then verify that the redirect works with the custom domain. 56 | 57 | Now, go get CloudFlare setup with your custom domain to prevent your function 58 | from being called more than airtable's rate limiting can handle. 59 | 60 | If you're not using CloudFlare, then set `ENABLE_CACHE` to `true` so you can get 61 | some caching from Netlify. That always seemed to not work very well for me 62 | though (which is one reason I use CloudFlare instead) so good luck. 63 | -------------------------------------------------------------------------------- /functions/redirect.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const fs = require('fs') 3 | 4 | const defaultRedirectURL = getEnv('DEFAULT_REDIRECT_URL') 5 | const cacheBusterCode = getEnv('CACHE_BUSTER_CODE', '_bust-cache') 6 | 7 | // I guess functions exist for a while in memory, so this can help 8 | // us avoid having to call airtable for the same link during that time. 9 | let fakeCache = {} 10 | const bustCache = () => (fakeCache = {}) 11 | 12 | exports.handler = async (event, context) => { 13 | // just something for grouping the netlify logs for this run together 14 | const runId = Date.now() 15 | .toString() 16 | .slice(-5) 17 | const log = (...args) => console.log(runId, ...args) 18 | 19 | const {host = ''} = event.headers 20 | log(`Request coming to "${event.path}"`) 21 | const [, code] = event.path.match(/^.*?redirect\/?(.*)$/) || [event.path, ''] 22 | if (!code) { 23 | log(`no code provided`) 24 | return getResponse({statusCode: 301}) 25 | } 26 | if (code === cacheBusterCode) { 27 | log('busting the cache') 28 | bustCache() 29 | return {statusCode: 200, body: 'cache busted'} 30 | } 31 | const codeLength = code.length 32 | if (codeLength > 50) { 33 | log(`short code "${code}" is ${codeLength} characters long. Seems fishy.`) 34 | return getResponse() 35 | } 36 | if (fakeCache[code]) { 37 | log(`short code "${code}" exists in our in-memory cache.`) 38 | return getResponse({longLink: fakeCache[code], statusCode: 301}) 39 | } 40 | try { 41 | const apiKey = getEnv('AIRTABLE_KEY') 42 | const base = getEnv('AIRTABLE_BASE') 43 | const table = getEnv('AIRTABLE_TABLE', 'URLs') 44 | const shortCodeField = getEnv('AIRTABLE_SHORT_CODE_FIELD', 'Short Code') 45 | const longLinkField = getEnv('AIRTABLE_LONG_LINK_FIELD', 'Long Link') 46 | const Airtable = require('airtable') 47 | log(`Attempting to get long link for code "${code}"`) 48 | const result = await new Airtable({apiKey}) 49 | .base(base)(table) 50 | .select({ 51 | maxRecords: 1, 52 | fields: [longLinkField], 53 | filterByFormula: `{${shortCodeField}} = "${code}"`, 54 | }) 55 | .firstPage() 56 | const longLink = result[0].get(longLinkField) 57 | if (longLink) { 58 | fakeCache[code] = longLink 59 | return getResponse({longLink, statusCode: 301}) 60 | } else { 61 | log(`There was no Long Link associated with "${code}".`) 62 | return getResponse() 63 | } 64 | } catch (error) { 65 | if (error.stack) { 66 | log(error.stack) 67 | } else { 68 | log(error) 69 | } 70 | log('!! there was an error and we are ignoring it... !!') 71 | } 72 | 73 | return getResponse() 74 | 75 | function getResponse({longLink = defaultRedirectURL, statusCode = 302} = {}) { 76 | const title = `${host}/${code || ''}` 77 | log(`> redirecting: ${title} -> ${longLink}`) 78 | const body = `