feat: optimize performance and SEO, integrate Lighthouse CI
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m53s
Build & Deploy / 🏗️ Build (push) Successful in 7m44s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Smoke Test (push) Successful in 59s
Build & Deploy / ⚡ Lighthouse (push) Successful in 3m14s
Build & Deploy / 🔔 Notify (push) Successful in 1s
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 1m53s
Build & Deploy / 🏗️ Build (push) Successful in 7m44s
Build & Deploy / 🚀 Deploy (push) Successful in 33s
Build & Deploy / 🧪 Smoke Test (push) Successful in 59s
Build & Deploy / ⚡ Lighthouse (push) Successful in 3m14s
Build & Deploy / 🔔 Notify (push) Successful in 1s
- Integrated imgproxy for centralized image optimization - Implemented Lighthouse CI in Gitea pipeline with native Chromium - Reached 100/100 SEO score by fixing canonicals, hreflang, and link text - Optimized LCP by forcing Hero component visibility until hydration - Decoupled analytics into an async shell to reduce TTI
This commit is contained in:
27
lib/imgproxy-loader.ts
Normal file
27
lib/imgproxy-loader.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { getImgproxyUrl } from './imgproxy';
|
||||
|
||||
/**
|
||||
* Next.js Image Loader for imgproxy
|
||||
*
|
||||
* @param {Object} props - properties from Next.js Image component
|
||||
* @param {string} props.src - The source image URL
|
||||
* @param {number} props.width - The desired image width
|
||||
* @param {number} props.quality - The desired image quality (ignored for now as imgproxy handles it)
|
||||
*/
|
||||
export default function imgproxyLoader({
|
||||
src,
|
||||
width,
|
||||
_quality,
|
||||
}: {
|
||||
src: string;
|
||||
width: number;
|
||||
_quality?: number;
|
||||
}) {
|
||||
// We use the width provided by Next.js for responsive images
|
||||
// Height is set to 0 to maintain aspect ratio
|
||||
return getImgproxyUrl(src, {
|
||||
width,
|
||||
resizing_type: 'fit',
|
||||
gravity: 'fv', // Use face-aware focusing (face detection)
|
||||
});
|
||||
}
|
||||
82
lib/imgproxy.ts
Normal file
82
lib/imgproxy.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Generates an imgproxy URL for a given source image and options.
|
||||
*
|
||||
* Documentation: https://docs.imgproxy.net/usage/processing
|
||||
*/
|
||||
|
||||
interface ImgproxyOptions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizing_type?: 'fit' | 'fill' | 'fill-down' | 'force' | 'auto';
|
||||
gravity?: string;
|
||||
enlarge?: boolean;
|
||||
extension?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a string to Base64 (URL-safe)
|
||||
*/
|
||||
function encodeBase64(str: string): string {
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.from(str)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
} else {
|
||||
// Fallback for browser environment if Buffer is not available
|
||||
return window.btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
export function getImgproxyUrl(src: string, options: ImgproxyOptions = {}): string {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_IMGPROXY_URL || 'https://img.infra.mintel.me';
|
||||
|
||||
// If no imgproxy URL is configured, return the source as is
|
||||
if (!baseUrl) return src;
|
||||
|
||||
// Handle local paths or relative URLs
|
||||
let absoluteSrc = src;
|
||||
if (src.startsWith('/')) {
|
||||
const baseUrlForSrc =
|
||||
process.env.NEXT_PUBLIC_BASE_URL ||
|
||||
(typeof window !== 'undefined' ? window.location.origin : 'https://klz-cables.com');
|
||||
if (baseUrlForSrc) {
|
||||
absoluteSrc = `${baseUrlForSrc.replace(/\/$/, '')}${src}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Development mapping: Map local domains to internal Docker hostnames
|
||||
// so imgproxy can fetch images without SSL issues or external routing
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (absoluteSrc.includes('klz.localhost')) {
|
||||
absoluteSrc = absoluteSrc.replace(/^https?:\/\/klz\.localhost/, 'http://klz-app:3000');
|
||||
} else if (absoluteSrc.includes('cms.klz.localhost')) {
|
||||
absoluteSrc = absoluteSrc.replace(/^https?:\/\/cms\.klz\.localhost/, 'http://klz-cms:8055');
|
||||
}
|
||||
// Also handle direct container names if needed
|
||||
}
|
||||
|
||||
const {
|
||||
width = 0,
|
||||
height = 0,
|
||||
resizing_type = 'fit',
|
||||
gravity = 'sm', // Default to smart gravity
|
||||
enlarge = false,
|
||||
extension = '',
|
||||
} = options;
|
||||
|
||||
// Processing options
|
||||
// Format: /rs:<type>:<width>:<height>:<enlarge>/g:<gravity>
|
||||
const processingOptions = [
|
||||
`rs:${resizing_type}:${width}:${height}:${enlarge ? 1 : 0}`,
|
||||
`g:${gravity}`,
|
||||
].join('/');
|
||||
|
||||
// Using /unsafe/ for now as we don't handle signatures yet
|
||||
// Format: <base_url>/unsafe/<options>/<base64_url>
|
||||
const suffix = extension ? `@${extension}` : '';
|
||||
const encodedSrc = encodeBase64(absoluteSrc + suffix);
|
||||
|
||||
return `${baseUrl}/unsafe/${processingOptions}/${encodedSrc}`;
|
||||
}
|
||||
Reference in New Issue
Block a user