From 5565b5d5d8b4121324b52664e2cbb096886bcc7e Mon Sep 17 00:00:00 2001 From: Ming Di Leom <2809763-curben@users.noreply.gitlab.com> Date: Fri, 4 Oct 2024 10:46:57 +0000 Subject: [PATCH] feat: cloudflare images --- .gitignore | 2 + .gitlab-ci.yml | 1 + cf-images/index.js | 121 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- scripts/image.js | 14 +++--- wrangler.toml | 7 +++ 6 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 cf-images/index.js create mode 100644 wrangler.toml diff --git a/.gitignore b/.gitignore index 2d8d57f..84d1a50 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ node_modules/ package-lock.json public/ .deploy*/ +bun.lockb +.wrangler/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5faf9fb..6784895 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,6 +18,7 @@ build: script: # Generate site - npm run build + - npm run deploy-cf-images rules: # Only trigger through push & "Run pipeline" events not in "site" branch; Skip in renovate job diff --git a/cf-images/index.js b/cf-images/index.js new file mode 100644 index 0000000..026fc51 --- /dev/null +++ b/cf-images/index.js @@ -0,0 +1,121 @@ +import { join, relative } from 'node:path' + +export default { + /** + * Fetch and log a request + * @param {Request} request + */ + async fetch (request) { + const { pathname } = new URL(request.url) + + if (pathname === '/images/favicon.ico') { + return new Response('', { + status: 404, + headers: { + 'Cache-Control': 'no-cache' + } + }) + } + + // https://developers.cloudflare.com/images/url-format#supported-formats-and-limitations + if (!/\.(jpe?g|png|gif|webp)$/i.test(pathname)) { + return new Response('Invalid file extension', { + status: 400, + headers: { + 'Cache-Control': 'no-cache' + } + }) + } + + // Cloudflare-specific options are in the cf object. + const options = { cf: { image: {} } } + + const numParts = pathname.split('/').length + let imgPath = '' + // original size + if (numParts === 4) { + imgPath = relative('/images', pathname) + } else if (numParts === 5) { + // Copy width size from path to request options + const width = relative('/images', pathname).split('/')[0] + const validSizes = new Set(['320', '468', '768']) + + if (validSizes.has(width)) { + imgPath = relative(join('/images', width), pathname) + options.cf.image.width = width + // serve original size if width is larger + options.cf.image.fit = 'scale-down' + } else { + return new Response('Invalid width', { + status: 400, + headers: { + 'Cache-Control': 'no-cache' + } + }) + } + } else { + return new Response('Invalid path', { + status: 404, + headers: { + 'Cache-Control': 'no-cache' + } + }) + } + + // Your Worker is responsible for automatic format negotiation. Check the Accept header. + const accept = request.headers.get('Accept') + if (/image\/avif/.test(accept)) { + options.cf.image.format = 'avif' + } else if (/image\/webp/.test(accept)) { + options.cf.image.format = 'webp' + } + + // Build a request that passes through request headers + // Images are stored on https://gitlab.com/curben/blog/-/tree/site + const imageURL = new URL(imgPath, 'https://curben.gitlab.io/blog/') + const imageRequest = new Request(imageURL, { + headers: request.headers + }) + + let response = await fetch(imageRequest, options) + // Reconstruct the Response object to make its headers mutable. + response = new Response(response.body, response) + + if (response.ok || response.redirected) { + // Set cache for 1 week + response.headers.set('Cache-Control', 'max-age=604800, public') + // Set Vary header + response.headers.set('Vary', 'Accept') + return response + } else if (response.status === 404) { + // Custom 404 page + const { status, statusText } = response + + const htmlHeader = new Headers({ + ...request.headers, + Accept: 'text/html' + }) + const page404 = new Request('https://curben.pages.dev/404', { + headers: htmlHeader + }) + response = await fetch(page404) + const html = await response.text() + return new Response(html, { + status, + statusText, + headers: { + ...response.headers, + 'Cache-Control': 'no-cache', + 'Content-Type': 'text/html; charset=utf-8' + } + }) + } else { + return new Response(`Could not fetch the image, the origin returned HTTP error ${response.status}`, { + status: response.status, + headers: { + 'Cache-Control': 'no-cache' + } + }) + } + } +} diff --git a/package.json b/package.json index af896fb..6ca416a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "scripts": { "build": "hexo generate", "snyk": "snyk auth $SNYK_TOKEN && snyk-protect && snyk test && snyk monitor", - "renovate": "renovate --platform 'gitlab' --autodiscover false --onboarding false --update-lock-files false --labels 'renovate' --require-config='ignored' \"$CI_PROJECT_PATH\"" + "renovate": "renovate --platform 'gitlab' --autodiscover false --onboarding false --update-lock-files false --labels 'renovate' --require-config='ignored' \"$CI_PROJECT_PATH\"", + "deploy-cf-images": "npx wrangler deploy" }, "dependencies": { "hexo": "^7.0.0", diff --git a/scripts/image.js b/scripts/image.js index eb82a89..8e220a8 100644 --- a/scripts/image.js +++ b/scripts/image.js @@ -20,18 +20,16 @@ hexo.extend.filter.register('marked:renderer', (renderer) => { // embed external image if (href.startsWith('http')) return `${alt}` - const fLink = (path, width) => { - const query = new URLSearchParams('f=auto') - if (typeof width === 'number') query.set('width', width) - const url = new URL('http://example.com/' + join('img', path) + '?' + query) + const fLink = (path, width = '') => { + const url = new URL(join('images', width, path), 'http://example.com/') - return url.pathname + url.search + return url.pathname } return `` + - `