Skip to content

Instantly share code, notes, and snippets.

@raiderrobert
Created February 23, 2023 18:47
Show Gist options
  • Save raiderrobert/7ad45541b066710c56f8d5f04d04258c to your computer and use it in GitHub Desktop.
Save raiderrobert/7ad45541b066710c56f8d5f04d04258c to your computer and use it in GitHub Desktop.
Cloudflare Worker for Traffic Splitting
import {
getAssetFromKV,
mapRequestToAsset,
} from "@cloudflare/kv-asset-handler";
/**
* The DEBUG flag will do two things that help during development:
* 1. we will skip caching on the edge, which makes it easier to
* debug.
* 2. we will return an error message on exception in your Response rather
* than the default 404.html page.
*/
const DEBUG = true;
addEventListener("fetch", (event) => {
const { request } = event;
const url = new URL(request.url);
const { hostname } = url;
const requestUserAgent = (request.headers.get('User-Agent') || '').toLowerCase();
const pathName = url.pathname.toLowerCase();
const ext = pathName.substring(pathName.lastIndexOf('.') || pathName.length);
try {
if (
BOT_AGENTS.some(e => requestUserAgent.includes(e))
&& !isOneOfThem(IGNORE_EXTENSIONS, ext)
) {
event.respondWith(prerenderRequest(request))
} else {
event.respondWith(handleEvent(event));
}
} catch (e) {
event.respondWith(
new Response("Internal Error", { status: 500 })
);
}
});
async function handleEvent(event) {
const { request } = event;
const url = new URL(request.url);
let options = {};
/**
* You can add custom logic to how we fetch your assets
* by configuring the function `mapRequestToAsset`
*/
options.mapRequestToAsset = (req) => {
// First let's apply the default handler, which we imported from
// '@cloudflare/kv-asset-handler' at the top of the file. We do
// this because the default handler already has logic to detect
// paths that should map to HTML files, for which it appends
// `/index.html` to the path.
req = mapRequestToAsset(req);
// Now we can detect if the default handler decided to map to
// index.html in some specific directory.
if (req.url.endsWith("/index.html")) {
// Indeed. Let's change it to instead map to the root `/index.html`.
// This avoids the need to do a redundant lookup that we know will
// fail.
return new Request(
`${new URL(req.url).origin}/index.html`,
req
);
} else {
// The default handler decided this is not an HTML page. It's probably
// an image, CSS, or JS file. Leave it as-is.
return req;
}
};
try {
if (DEBUG) {
// customize caching
options.cacheControl = {
bypassCache: true,
};
}
return await getAssetFromKV(event, options);
} catch (e) {
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
try {
let notFoundResponse = await getAssetFromKV(event, {
mapRequestToAsset: (req) =>
new Request(`${new URL(req.url).origin}/404.html`, req),
});
return new Response(notFoundResponse.body, {
...notFoundResponse,
status: 404,
});
} catch (e) {}
}
return new Response(e.message || e.toString(), { status: 500 });
}
}
/**
* Basic settings
*/
/**
* Advanced settings
*/
// These are the user agents that the worker will look for to
// initiate prerendering of the site.
const BOT_AGENTS = [
'googlebot',
'yahoo! slurp',
'bingbot',
'yandex',
'baiduspider',
'facebookexternalhit',
'twitterbot',
'rogerbot',
'linkedinbot',
'embedly',
'quora link preview',
'showyoubot',
'outbrain',
'pinterest/0.',
'developers.google.com/+/web/snippet',
'slackbot',
'vkshare',
'w3c_validator',
'redditbot',
'applebot',
'whatsapp',
'flipboard',
'tumblr',
'bitlybot',
'skypeuripreview',
'nuzzel',
'discordbot',
'google page speed',
'qwantify',
'pinterestbot',
'bitrix link preview',
'xing-contenttabreceiver',
'chrome-lighthouse'
];
// These are the extensions that the worker will skip prerendering
// even if any other conditions pass.
const IGNORE_EXTENSIONS = [
'.js',
'.css',
'.xml',
'.less',
'.png',
'.jpg',
'.jpeg',
'.gif',
'.pdf',
'.doc',
'.txt',
'.ico',
'.rss',
'.zip',
'.mp3',
'.rar',
'.exe',
'.wmv',
'.doc',
'.avi',
'.ppt',
'.mpg',
'.mpeg',
'.tif',
'.wav',
'.mov',
'.psd',
'.ai',
'.xls',
'.mp4',
'.m4a',
'.swf',
'.dat',
'.dmg',
'.iso',
'.flv',
'.m4v',
'.torrent',
'.woff',
'.ttf',
'.svg',
'.webmanifest'
];
/**
* Helper function to check if an array contains an element or not.
*
* @param {string[]} array - The array to check.
* @param {string} element - The element to check if the array contains.
* @returns {boolean}
*/
function isOneOfThem(array, element) {
return array.some(e => e === element);
}
// The Prerender.io API key
// Load this from .env file don't hard code it; just an example
const API_KEY = 'FOO';
/**
* Function to request the prerendered version of a request.
*
* @param {Request} request - The request received by CloudFlare
* @returns {Promise<Response>}
*/
async function prerenderRequest(request) {
const { url } = request;
const prerenderUrl = `https://service.prerender.io/${url}`;
const headers = new Headers({
'X-Prerender-Token': API_KEY
});
const prerenderRequest = new Request(prerenderUrl, {
headers
});
const response = await fetch(prerenderRequest);
const results = await response.text();
const init = {
headers: {
"content-type": "text/html;charset=UTF-8",
},
}
return new Response(results, init)
}
{
"private": true,
"name": "worker",
"version": "1.0.0",
"description": "A template for kick starting a Cloudflare Workers project",
"main": "index.js",
"author": "Robert Roskam <raiderrobert@gmail.com>",
"license": "MIT",
"dependencies": {
"@cloudflare/kv-asset-handler": "^0.0.5"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment