Created
May 17, 2022 11:39
-
-
Save jasonbyrne/9df2058d2470935ded4467c370285b1d to your computer and use it in GitHub Desktop.
Generate Placeholder SVG with Cloudflare Workers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Credit:: https://github.com/cloudfour/simple-svg-placeholder | |
type SvgOptions = { | |
width: number | |
height: number | |
text: string | |
fontFamily: string | |
fontWeight: string | |
bgColor: string | |
textColor: string | |
fontSize?: number | |
} | |
const defaultOptions: SvgOptions = { | |
width: 300, | |
height: 300, | |
text: ':-)', | |
fontFamily: 'sans-serif', | |
fontWeight: 'bold', | |
bgColor: 'red', | |
textColor: 'white', | |
} | |
const generateSVG = (qsOpts: SvgOptions): string => { | |
const opts = { | |
...defaultOptions, | |
...qsOpts, | |
} | |
const fontSize = | |
opts.fontSize || Math.floor(Math.min(opts.width, opts.height) * 0.3) | |
const dy = fontSize * 0.35 | |
const charset = 'UTF-8' | |
const dataUri = false | |
const str = `<svg xmlns="http://www.w3.org/2000/svg" width="${opts.width}" height="${opts.height}" viewBox="0 0 ${opts.width} ${opts.height}"> | |
<rect fill="${opts.bgColor}" width="${opts.width}" height="${opts.height}"/> | |
<text fill="${opts.textColor}" font-family="${opts.fontFamily}" font-size="${fontSize}" dy="${dy}" font-weight="${opts.fontWeight}" x="50%" y="50%" text-anchor="middle">${opts.text}</text> | |
</svg>` | |
// Thanks to: filamentgroup/directory-encoder | |
const cleaned = str | |
.replace(/[\t\n\r]/gim, '') // Strip newlines and tabs | |
.replace(/\s\s+/g, ' ') // Condense multiple spaces | |
.replace(/'/gim, '\\i') // Normalize quotes | |
if (dataUri) { | |
const encoded = encodeURIComponent(cleaned) | |
.replace(/\(/g, '%28') // Encode brackets | |
.replace(/\)/g, '%29') | |
return `data:image/svg+xml;charset=${charset},${encoded}` | |
} | |
return cleaned | |
} | |
export default generateSVG |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import generateSVG from './generate-svg' | |
import { IncomingRequest } from './incoming-request' | |
import { | |
sanitizeColor, | |
sanitizeNumber, | |
sanitizeString, | |
} from './sanitizers' | |
const cacheTtl = 60 * 60 * 24 * 90 // 90 days | |
// QS Options: width, height, bgColor, textColor, text | |
export async function generatePlaceholder( | |
req: IncomingRequest, | |
): Promise<Response> { | |
const url = new URL(req.request.url) | |
url.searchParams.sort() // improve cache-hits by sorting search params | |
const cache = caches.default // Cloudflare edge caching | |
let response = await cache.match(req.request, { ignoreMethod: true }) // try to find match for this request in the edge cache | |
if (response) { | |
// use cache found on Cloudflare edge. Set X-Worker-Cache header for helpful debug | |
const headers = new Headers(response.headers) | |
headers.set('X-Worker-Cache', 'true') | |
return new Response(response.body, { | |
status: response.status, | |
statusText: response.statusText, | |
headers, | |
}) | |
} | |
const imageOptions = { | |
width: sanitizeNumber(url.searchParams.get('width'), [10, 1200], 300), | |
height: sanitizeNumber(url.searchParams.get('height'), [10, 1200], 300), | |
fontFamily: 'sans-serif', | |
fontWeight: 'bold', | |
bgColor: sanitizeColor(url.searchParams.get('bgColor'), 'red'), | |
textColor: sanitizeColor(url.searchParams.get('textColor'), 'white'), | |
text: sanitizeString(url.searchParams.get('text'), ':)'), | |
fontSize: | |
sanitizeNumber(url.searchParams.get('fontSize'), [3, 400], 0) || | |
undefined, | |
} | |
response = new Response(generateSVG(imageOptions), { | |
headers: { | |
'content-type': 'image/svg+xml; charset=utf-8', | |
}, | |
}) | |
// set cache header on 200 response | |
if (response.status === 200) { | |
response.headers.set('Cache-Control', 'public, max-age=' + cacheTtl) | |
} else { | |
// only cache other things for 5 minutes (errors, 404s, etc.) | |
response.headers.set('Cache-Control', 'public, max-age=300') | |
} | |
req.event.waitUntil(cache.put(req.request, response.clone())) // store in cache | |
return response | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const COLOR_NAMES = [ | |
'aliceblue', | |
'antiquewhite', | |
'aqua', | |
'aquamarine', | |
'azure', | |
'beige', | |
'bisque', | |
'black', | |
'blanchedalmond', | |
'blue', | |
'blueviolet', | |
'brown', | |
'burlywood', | |
'cadetblue', | |
'chartreuse', | |
'chocolate', | |
'coral', | |
'cornflowerblue', | |
'cornsilk', | |
'crimson', | |
'cyan', | |
'darkblue', | |
'darkcyan', | |
'darkgoldenrod', | |
'darkgray', | |
'darkgrey', | |
'darkgreen', | |
'darkkhaki', | |
'darkmagenta', | |
'darkolivegreen', | |
'darkorange', | |
'darkorchid', | |
'darkred', | |
'darksalmon', | |
'darkseagreen', | |
'darkslateblue', | |
'darkslategray', | |
'darkslategrey', | |
'darkturquoise', | |
'darkviolet', | |
'deeppink', | |
'deepskyblue', | |
'dimgray', | |
'dimgrey', | |
'dodgerblue', | |
'firebrick', | |
'floralwhite', | |
'forestgreen', | |
'fuchsia', | |
'gainsboro', | |
'ghostwhite', | |
'gold', | |
'goldenrod', | |
'gray', | |
'grey', | |
'green', | |
'greenyellow', | |
'honeydew', | |
'hotpink', | |
'indianred', | |
'indigo', | |
'ivory', | |
'khaki', | |
'lavender', | |
'lavenderblush', | |
'lawngreen', | |
'lemonchiffon', | |
'lightblue', | |
'lightcoral', | |
'lightcyan', | |
'lightgoldenrodyellow', | |
'lightgray', | |
'lightgrey', | |
'lightgreen', | |
'lightpink', | |
'lightsalmon', | |
'lightseagreen', | |
'lightskyblue', | |
'lightslategray', | |
'lightslategrey', | |
'lightsteelblue', | |
'lightyellow', | |
'lime', | |
'limegreen', | |
'linen', | |
'magenta', | |
'maroon', | |
'mediumaquamarine', | |
'mediumblue', | |
'mediumorchid', | |
'mediumpurple', | |
'mediumseagreen', | |
'mediumslateblue', | |
'mediumspringgreen', | |
'mediumturquoise', | |
'mediumvioletred', | |
'midnightblue', | |
'mintcream', | |
'mistyrose', | |
'moccasin', | |
'navajowhite', | |
'navy', | |
'oldlace', | |
'olive', | |
'olivedrab', | |
'orange', | |
'orangered', | |
'orchid', | |
'palegoldenrod', | |
'palegreen', | |
'paleturquoise', | |
'palevioletred', | |
'papayawhip', | |
'peachpuff', | |
'peru', | |
'pink', | |
'plum', | |
'powderblue', | |
'purple', | |
'rebeccapurple', | |
'red', | |
'rosybrown', | |
'royalblue', | |
'saddlebrown', | |
'salmon', | |
'sandybrown', | |
'seagreen', | |
'seashell', | |
'sienna', | |
'silver', | |
'skyblue', | |
'slateblue', | |
'slategray', | |
'slategrey', | |
'snow', | |
'springgreen', | |
'steelblue', | |
'tan', | |
'teal', | |
'thistle', | |
'tomato', | |
'turquoise', | |
'violet', | |
'wheat', | |
'white', | |
'whitesmoke', | |
'yellow', | |
'yellowgreen', | |
] | |
export const sanitizeNumber = ( | |
input: string | null, | |
range: [number, number], | |
defaultValue: number, | |
): number => { | |
if (input === null) return defaultValue | |
const num = parseInt(input) | |
if (!isNaN(num) && num >= range[0] && num <= range[1]) return num | |
return defaultValue | |
} | |
export const sanitizeString = ( | |
input: string | null, | |
defaultValue: string, | |
): string => { | |
if (input === null) return defaultValue | |
return input.replace(/[^a-z]+/gi, '') | |
} | |
export const sanitizeColor = ( | |
input: string | null, | |
defaultValue: string, | |
): string => { | |
if (input === null) return defaultValue | |
if (COLOR_NAMES.includes(input.toLowerCase())) return input.toLowerCase() | |
if (input.match(/^[A-F0-9]{6}$/i)) return `#${input}` | |
return defaultValue | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment