chore: integrate local imgproxy sidecar and unify list components
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 36s
Build & Deploy / 🧪 QA (push) Successful in 4m2s
Build & Deploy / 🏗️ Build (push) Successful in 10m53s
Build & Deploy / 🚀 Deploy (push) Successful in 27s
Build & Deploy / 🩺 Health Check (push) Failing after 11s
Build & Deploy / 🔔 Notify (push) Successful in 2s

- Added imgproxy service to docker-compose.dev.yml with URL mapping
- Implemented robust Base64 encoding for imgproxy source URLs
- Synchronized NEXT_PUBLIC_IMGPROXY_URL and NEXT_PUBLIC_BASE_URL
- Refactored About page to use existing marc-mintel.png asset
- Created shared IconList component and unified list styles project-wide
- Fixed vertical alignment issues in IconList items
- Updated dev script with aggressive port 3000 and lock file cleanup
This commit is contained in:
2026-02-13 22:03:35 +01:00
parent 43b96bb84b
commit 7c774f65bc
11 changed files with 409 additions and 103 deletions

View File

@@ -0,0 +1,26 @@
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",
});
}

View File

@@ -0,0 +1,79 @@
/**
* 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 baseUrl =
process.env.NEXT_PUBLIC_BASE_URL ||
(typeof window !== "undefined" ? window.location.origin : "");
if (baseUrl) {
absoluteSrc = `${baseUrl}${src}`;
}
}
const {
width = 0,
height = 0,
resizing_type = "fit",
gravity = "sm",
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}`;
}

View File

@@ -0,0 +1,62 @@
import { getImgproxyUrl } from "./imgproxy";
/**
* Verification script for imgproxy URL generation
*/
function testImgproxyUrl() {
const testCases = [
{
src: "https://picsum.photos/800/800",
options: { width: 400, height: 300, resizing_type: "fill" as const },
description: "Remote URL with fill resizing",
},
{
src: "/images/avatar.jpg",
options: { width: 100, extension: "webp" },
description: "Local path with extension conversion",
},
{
src: "https://example.com/image.png",
options: { gravity: "no" },
description: "Remote URL with custom gravity",
},
];
console.log("🧪 Testing imgproxy URL generation...\n");
testCases.forEach((tc, i) => {
const url = getImgproxyUrl(tc.src, tc.options);
console.log(`Test Case ${i + 1}: ${tc.description}`);
console.log(`Source: ${tc.src}`);
console.log(`Result: ${url}`);
// Basic validation
if (url.startsWith("https://img.infra.mintel.me/unsafe/")) {
console.log("✅ Base URL and unsafe path correct");
} else {
console.log("❌ Base URL or unsafe path mismatch");
}
if (
tc.options.width &&
url.includes(
`rs:${tc.options.resizing_type || "fit"}:${tc.options.width}`,
)
) {
console.log("✅ Resizing options present");
}
console.log("-------------------\n");
});
}
// Mock environment for testing if not set
if (!process.env.NEXT_PUBLIC_IMGPROXY_URL) {
process.env.NEXT_PUBLIC_IMGPROXY_URL = "https://img.infra.mintel.me";
}
if (!process.env.NEXT_PUBLIC_BASE_URL) {
process.env.NEXT_PUBLIC_BASE_URL = "http://mintel.localhost";
}
testImgproxyUrl();