mirror of https://gitlab.com/curben/blog
				
				
				
			feat: cloudflare images
This commit is contained in:
		
							parent
							
								
									889875e673
								
							
						
					
					
						commit
						5565b5d5d8
					
				| 
						 | 
					@ -6,3 +6,5 @@ node_modules/
 | 
				
			||||||
package-lock.json
 | 
					package-lock.json
 | 
				
			||||||
public/
 | 
					public/
 | 
				
			||||||
.deploy*/
 | 
					.deploy*/
 | 
				
			||||||
 | 
					bun.lockb
 | 
				
			||||||
 | 
					.wrangler/
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,7 @@ build:
 | 
				
			||||||
  script:
 | 
					  script:
 | 
				
			||||||
    # Generate site
 | 
					    # Generate site
 | 
				
			||||||
    - npm run build
 | 
					    - npm run build
 | 
				
			||||||
 | 
					    - npm run deploy-cf-images
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  rules:
 | 
					  rules:
 | 
				
			||||||
    # Only trigger through push & "Run pipeline" events not in "site" branch; Skip in renovate job
 | 
					    # Only trigger through push & "Run pipeline" events not in "site" branch; Skip in renovate job
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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'
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,8 @@
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "build": "hexo generate",
 | 
					    "build": "hexo generate",
 | 
				
			||||||
    "snyk": "snyk auth $SNYK_TOKEN && snyk-protect && snyk test && snyk monitor",
 | 
					    "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": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "hexo": "^7.0.0",
 | 
					    "hexo": "^7.0.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,18 +20,16 @@ hexo.extend.filter.register('marked:renderer', (renderer) => {
 | 
				
			||||||
    // embed external image
 | 
					    // embed external image
 | 
				
			||||||
    if (href.startsWith('http')) return `<img src="${href}" alt="${alt}" title="${title}">`
 | 
					    if (href.startsWith('http')) return `<img src="${href}" alt="${alt}" title="${title}">`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const fLink = (path, width) => {
 | 
					    const fLink = (path, width = '') => {
 | 
				
			||||||
      const query = new URLSearchParams('f=auto')
 | 
					      const url = new URL(join('images', width, path), 'http://example.com/')
 | 
				
			||||||
      if (typeof width === 'number') query.set('width', width)
 | 
					 | 
				
			||||||
      const url = new URL('http://example.com/' + join('img', path) + '?' + query)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return url.pathname + url.search
 | 
					      return url.pathname
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return `<a href="${join('/img', href)}">` +
 | 
					    return `<a href="${join('/img', href)}">` +
 | 
				
			||||||
      `<img srcset="${fLink(href, 320)} 320w,` +
 | 
					      `<img srcset="${fLink(href, '320')} 320w,` +
 | 
				
			||||||
      `${fLink(href, 468)} 468w,` +
 | 
					      `${fLink(href, '468')} 468w,` +
 | 
				
			||||||
      `${fLink(href, 768)} 768w,` +
 | 
					      `${fLink(href, '768')} 768w,` +
 | 
				
			||||||
      `${fLink(href)} 800w"` +
 | 
					      `${fLink(href)} 800w"` +
 | 
				
			||||||
      ' sizes="(max-width: 320px) 320px,' +
 | 
					      ' sizes="(max-width: 320px) 320px,' +
 | 
				
			||||||
      '(max-width: 468px) 468px,' +
 | 
					      '(max-width: 468px) 468px,' +
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					name = "cf-images"
 | 
				
			||||||
 | 
					main = "cf-images/index.js"
 | 
				
			||||||
 | 
					compatibility_date = "2024-09-23"
 | 
				
			||||||
 | 
					compatibility_flags = [ "nodejs_compat" ]
 | 
				
			||||||
 | 
					send_metrics = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					route = { pattern = "https://mdleom.com/images/*", zone_name = "mdleom.com" }
 | 
				
			||||||
		Loading…
	
		Reference in New Issue