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 ``
- 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 `` +
- `