feat: migrate to next/image with custom openinary loader

This commit is contained in:
Mustafa Yildiz
2026-07-12 15:35:10 +03:00
parent fcfc76706c
commit bdab54a9c9
4 changed files with 55 additions and 4 deletions
+8 -1
View File
@@ -2,6 +2,7 @@
import React, { useEffect, useState, useRef, Fragment } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
import { MenuCategory, MenuItem as MenuItemType } from "@/data/menu";
import { CategoryNav } from "@/components/CategoryNav";
import { MenuItem } from "@/components/MenuItem";
@@ -294,7 +295,13 @@ export default function MenuClient({ initialCategories, siteSettings }: { initia
>
{selectedItem.image && (
<div className="w-full h-80 relative">
<img src={selectedItem.image} alt={selectedItem.name} className="w-full h-full object-cover" />
<Image
src={selectedItem.image}
alt={selectedItem.name}
fill
sizes="(max-width: 768px) 100vw, 400px"
className="object-cover"
/>
<button
className="absolute top-3 right-3 bg-black/50 text-white w-8 h-8 rounded-full flex items-center justify-center backdrop-blur-md transition-colors hover:bg-black/70"
onClick={() => setSelectedItem(null)}
+6 -3
View File
@@ -1,4 +1,5 @@
import React from "react";
import Image from "next/image";
import { MenuItem as MenuItemType } from "@/data/menu";
export function MenuItem({ item, onClick }: { item: MenuItemType; onClick?: () => void }) {
@@ -34,11 +35,13 @@ export function MenuItem({ item, onClick }: { item: MenuItemType; onClick?: () =
)}
</div>
{item.image && (
<div className="shrink-0">
<img
<div className="shrink-0 relative w-20 h-20">
<Image
src={item.image}
alt={item.name}
className="w-20 h-20 object-cover rounded-lg shadow-sm"
fill
sizes="80px"
className="object-cover rounded-lg shadow-sm"
/>
</div>
)}
+39
View File
@@ -0,0 +1,39 @@
'use client'
export default function openinaryLoader({ src, width, quality }: { src: string, width: number, quality?: number }) {
// Handle already absolute openinary URLs
let path = src;
if (src.startsWith('https://media.ayris.tech/t/')) {
// format: https://media.ayris.tech/t/w_800,h_800/moyqr/img.jpg
const parts = src.split('/');
path = parts.slice(5).join('/');
} else if (src.startsWith('https://media.ayris.tech/upload/')) {
// format: https://media.ayris.tech/upload/moyqr/img.jpg
const parts = src.split('/');
path = parts.slice(4).join('/');
} else if (src.includes('res.cloudinary.com')) {
// Correctly apply width & quality for unmigrated Cloudinary URLs
// e.g. https://res.cloudinary.com/domain/image/upload/v1234/path.jpg
// becomes: https://res.cloudinary.com/domain/image/upload/w_800,f_webp,q_75/v1234/path.jpg
const parts = src.split('/upload/');
if (parts.length === 2) {
return `${parts[0]}/upload/w_${width},f_webp,q_${quality || 75}/${parts[1]}`;
}
return src;
} else if (src.startsWith('http')) {
// For other external URLs, we append the width as a query parameter to satisfy Next.js.
const url = new URL(src);
url.searchParams.set('w', width.toString());
if (quality) {
url.searchParams.set('q', quality.toString());
}
return url.toString();
}
// Clean up any leading slash
if (path.startsWith('/')) {
path = path.substring(1);
}
return `https://media.ayris.tech/t/w_${width},f_webp,q_${quality || 75}/${path}`
}
+2
View File
@@ -6,6 +6,8 @@ const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
const nextConfig: NextConfig = {
output: "standalone",
images: {
loader: 'custom',
loaderFile: './lib/openinary-loader.ts',
remotePatterns: [
{
protocol: 'https',