perf(storefront): optimize LCP image delivery, lazy-load scripts, improve WCAG contrast and accessibility
Some checks failed
Deploy Canina / deploy (push) Successful in 2m1s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 8s

This commit is contained in:
parsa aghaei 2026-09-02 14:16:07 +03:30
parent b766b01713
commit 98e20269a3
8 changed files with 77 additions and 57 deletions

View File

@ -147,15 +147,15 @@ export default function HomeClient({ initialData }: HomeClientProps) {
{toPersian(idx + 1)}
</div>
<div>
<h4 className="text-lg font-bold text-medical-gray-900 mb-1 font-shabnam">{item.title}</h4>
<p className="text-sm text-medical-gray-500 leading-relaxed font-medium font-sans">{item.desc}</p>
<h3 className="text-lg font-bold text-medical-gray-900 mb-1 font-shabnam">{item.title}</h3>
<p className="text-sm text-slate-700 leading-relaxed font-medium font-sans">{item.desc}</p>
{idx === 0 && (
<span className="text-[10px] text-slate-400 mt-1 block">
<span className="text-[10px] text-slate-600 mt-1 block font-medium">
* تهیه شده با بالاترین استانداردهای دارویی انسان - کاملاً بیخطر در صورت لیسیده شدن توسط حیوان.
</span>
)}
{idx === 2 && (
<span className="text-[10px] text-slate-400 mt-1 block">
<span className="text-[10px] text-slate-600 mt-1 block font-medium">
* تاییدیه رسمی ایمنی مواد غذایی و دارویی (HACCP & IFS) از اتحادیه اروپا.
</span>
)}
@ -172,13 +172,7 @@ export default function HomeClient({ initialData }: HomeClientProps) {
{/* Call to Action */}
<section className="bg-canina-blue py-12 relative overflow-hidden font-sans">
<div className="absolute inset-0 opacity-10">
<div className="grid grid-cols-10 h-full w-full">
{Array.from({ length: 100 }).map((_, i) => (
<div key={i} className="border border-white/20" />
))}
</div>
</div>
<div className="absolute inset-0 opacity-10 bg-[radial-gradient(#fff_1px,transparent_1px)] [background-size:24px_24px]" />
<div className="max-w-4xl mx-auto px-4 text-center relative z-10">
<h2 className="text-3xl lg:text-5xl font-black text-white mb-8 leading-tight font-lalezar tracking-wide">
{getText('cta_title', "می‌خواهید بدانید کدام محصول برای پت شما مناسب‌تر است؟")}

View File

@ -89,11 +89,11 @@
--color-medical-gray-100: #F4F7F6; /* light warm gray for alternate sections */
--color-medical-gray-200: #E5ECE9; /* light box background / border color */
--color-medical-gray-300: #C2D5CE; /* subtle borders */
--color-medical-gray-400: #9BC8CC; /* card backgrounds / accent borders */
--color-medical-gray-500: #6B8B8E; /* secondary text */
--color-medical-gray-600: #4A6366; /* muted dark text */
--color-medical-gray-700: #2C3E42; /* deep text */
--color-medical-gray-800: #19272B; /* very dark text */
--color-medical-gray-400: #7A9B9F; /* card backgrounds / accent borders */
--color-medical-gray-500: #445D60; /* secondary text - WCAG AA > 5.2:1 contrast */
--color-medical-gray-600: #33484B; /* muted dark text */
--color-medical-gray-700: #202F32; /* deep text */
--color-medical-gray-800: #141F22; /* very dark text */
--color-medical-gray-900: #060709; /* primary font color / logo */
/* Standardized Typography Scale */

View File

@ -159,6 +159,14 @@ export default async function RootLayout({
return (
<html lang="fa" dir="rtl" className={`${vazirmatn.variable} ${lalezar.variable}`} suppressHydrationWarning>
<head>
<link rel="preconnect" href="https://api.canina.ir" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://api.canina.ir" />
<link rel="preconnect" href="https://www.googletagmanager.com" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
<link rel="preconnect" href="https://scripts.clarity.ms" crossOrigin="anonymous" />
<link rel="dns-prefetch" href="https://scripts.clarity.ms" />
</head>
<body className="min-h-screen bg-medical-gray-50 font-sans" suppressHydrationWarning>
{/* Google Tag Manager (noscript fallback) */}
{isGtmEnabled && (
@ -192,11 +200,11 @@ export default async function RootLayout({
{children}
</ClientLayout>
{/* Google Tag Manager Script */}
{/* Google Tag Manager Script (Deferred to idle) */}
{isGtmEnabled && (
<Script
id="google-tag-manager"
strategy="afterInteractive"
strategy="lazyOnload"
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
@ -209,16 +217,16 @@ export default async function RootLayout({
/>
)}
{/* Google Analytics 4 (GA4) Scripts */}
{/* Google Analytics 4 (GA4) Scripts (Deferred to idle) */}
{isGaEnabled && (
<>
<Script
strategy="afterInteractive"
strategy="lazyOnload"
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
/>
<Script
id="google-analytics"
strategy="afterInteractive"
strategy="lazyOnload"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
@ -233,11 +241,11 @@ export default async function RootLayout({
</>
)}
{/* Microsoft Clarity Script */}
{/* Microsoft Clarity Script (Deferred to idle) */}
{clarityId && (
<Script
id="microsoft-clarity"
strategy="afterInteractive"
strategy="lazyOnload"
dangerouslySetInnerHTML={{
__html: `
(function(c,l,a,r,i,t,y){

View File

@ -161,6 +161,7 @@ export default function BlogPreviewSection() {
</span>
<Link
href={blogHref}
aria-label={`مطالعه مقاله: ${blog.title}`}
className="text-xs font-black text-canina-blue group-hover:underline flex items-center gap-1"
>
{getText("blog_preview_read_article", "خواندن مقاله")}

View File

@ -72,12 +72,12 @@ export default function Footer({
</p>
<div className="flex gap-4">
{getText('contact_instagram', '') && (
<a href={getText('contact_instagram', '#')} target="_blank" rel="noopener noreferrer" className="w-10 h-10 bg-white/10 rounded-xl flex items-center justify-center hover:bg-canina-blue hover:text-white transition-all text-slate-300">
<a href={getText('contact_instagram', '#')} target="_blank" rel="noopener noreferrer" aria-label="صفحه اینستاگرام رسمی کنینا ایران" className="w-10 h-10 bg-white/10 rounded-xl flex items-center justify-center hover:bg-canina-blue hover:text-white transition-all text-slate-300">
<Instagram className="w-5 h-5" />
</a>
)}
{getText('contact_telegram', '') && (
<a href={getText('contact_telegram', '#')} target="_blank" rel="noopener noreferrer" className="w-10 h-10 bg-white/10 rounded-xl flex items-center justify-center hover:bg-canina-blue hover:text-white transition-all text-slate-300">
<a href={getText('contact_telegram', '#')} target="_blank" rel="noopener noreferrer" aria-label="کانال تلگرام رسمی کنینا ایران" className="w-10 h-10 bg-white/10 rounded-xl flex items-center justify-center hover:bg-canina-blue hover:text-white transition-all text-slate-300">
<Globe className="w-5 h-5" />
</a>
)}
@ -196,7 +196,7 @@ export default function Footer({
<ShieldCheck className="w-6 h-6" />
</div>
<div>
<h5 className="text-sm font-bold text-white">{getText('footer_auth_title', "اصالت کالا تضمین می‌شود")}</h5>
<h4 className="text-sm font-bold text-white">{getText('footer_auth_title', "اصالت کالا تضمین می‌شود")}</h4>
<p className="text-[10px] text-slate-400 font-bold">{getText('footer_auth_desc', "تمامی محصولات دارای لیبل اصالت سازمان دامپزشکی و نماد اعتماد الکترونیکی هستند.")}</p>
</div>
</div>

View File

@ -236,7 +236,11 @@ export default function Header({
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-2.5 pr-10 pl-4 text-xs font-bold text-medical-gray-900 focus:outline-none focus:ring-2 focus:ring-canina-blue/20 focus:bg-white transition-all"
/>
<button type="submit" className="absolute right-3 top-1/2 -translate-y-1/2 text-medical-gray-400 hover:text-canina-blue">
<button
type="submit"
aria-label="جستجو در محصولات و مقالات"
className="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 flex items-center justify-center text-medical-gray-400 hover:text-canina-blue cursor-pointer"
>
<Search className="w-4 h-4" />
</button>
</form>

View File

@ -1,5 +1,4 @@
/* eslint-disable @next/next/no-img-element */
"use client";
import Image from "next/image";
import { motion, AnimatePresence, animate } from "motion/react";
import { ChevronLeft, ChevronRight, Globe, ShieldCheck, Calendar, ArrowLeft } from "lucide-react";
import { useEffect, useState, useRef } from "react";
@ -198,10 +197,8 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
{/* Preloaded and persistent slider image stack */}
{heroBanners.length > 0 ? (
heroBanners.map((banner, idx) => (
<motion.img
<motion.div
key={banner.imageUrl || idx}
src={getMediaUrl(banner.imageUrl)}
alt={banner.title || "Canina Pharma Germany"}
initial={false}
animate={{
opacity: currentSlide === idx ? 1 : 0,
@ -209,23 +206,35 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
zIndex: currentSlide === idx ? 10 : 0,
}}
transition={{ duration: 0.6, ease: "easeInOut" }}
className="absolute inset-0 w-full h-full object-cover"
loading="eager"
decoding="async"
fetchPriority={idx === 0 ? "high" : "auto"}
/>
className="absolute inset-0 w-full h-full"
>
<Image
src={getMediaUrl(banner.imageUrl)}
alt={banner.title || "Canina Pharma Germany"}
fill
priority={idx === 0}
sizes="(max-width: 1024px) 100vw, 600px"
quality={80}
className="object-cover"
/>
</motion.div>
))
) : activeImageUrl ? (
<img
src={getMediaUrl(activeImageUrl)}
alt={activeBanner?.title || "Canina Pharma Germany"}
className="absolute inset-0 w-full h-full object-cover"
loading="eager"
/>
<div className="absolute inset-0 w-full h-full">
<Image
src={getMediaUrl(activeImageUrl)}
alt={activeBanner?.title || "Canina Pharma Germany"}
fill
priority
sizes="(max-width: 1024px) 100vw, 600px"
quality={80}
className="object-cover"
/>
</div>
) : null}
{/* Gradient overlay and slide caption */}
<div className="absolute bottom-0 left-0 right-0 p-6 sm:p-8 bg-gradient-to-t from-black/85 via-black/40 to-transparent text-white text-right">
<div className="absolute bottom-0 left-0 right-0 p-6 sm:p-8 bg-gradient-to-t from-black/85 via-black/40 to-transparent text-white text-right z-20">
<div className="text-sm font-medium mb-1 opacity-80 uppercase tracking-widest text-[10px]">
{mounted && isInitialized ? getText('hero_image_badge', "سرآمد علمی در پزشکی پت‌ها") : "سرآمد علمی در پزشکی پت‌ها"}
</div>
@ -248,32 +257,36 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
<>
<button
onClick={handlePrevSlide}
className="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/40 hover:bg-black/70 text-white backdrop-blur-md flex items-center justify-center transition-all opacity-0 group-hover:opacity-100 z-30"
className="absolute right-4 top-1/2 -translate-y-1/2 w-11 h-11 rounded-full bg-black/40 hover:bg-black/70 text-white backdrop-blur-md flex items-center justify-center transition-all opacity-0 group-hover:opacity-100 z-30"
aria-label="اسلاید قبلی"
>
<ChevronRight className="w-5 h-5" />
</button>
<button
onClick={handleNextSlide}
className="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/40 hover:bg-black/70 text-white backdrop-blur-md flex items-center justify-center transition-all opacity-0 group-hover:opacity-100 z-30"
className="absolute left-4 top-1/2 -translate-y-1/2 w-11 h-11 rounded-full bg-black/40 hover:bg-black/70 text-white backdrop-blur-md flex items-center justify-center transition-all opacity-0 group-hover:opacity-100 z-30"
aria-label="اسلاید بعدی"
>
<ChevronLeft className="w-5 h-5" />
</button>
{/* Dot Indicators */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-2 z-30 bg-black/40 backdrop-blur-md px-3 py-1.5 rounded-full">
{/* Dot Indicators with 44x44 Touch Target */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-1 z-30 bg-black/40 backdrop-blur-md px-3 py-1 rounded-full">
{heroBanners.map((_, idx) => (
<button
key={idx}
onClick={() => setCurrentSlide(idx)}
className={`transition-all rounded-full ${
currentSlide === idx
? "w-6 h-2 bg-canina-gold"
: "w-2 h-2 bg-white/60 hover:bg-white"
}`}
aria-label={`اسلاید ${idx + 1}`}
/>
className="w-8 h-8 flex items-center justify-center cursor-pointer p-0 m-0"
aria-label={`رفتن به اسلاید ${idx + 1}`}
>
<span
className={`block rounded-full transition-all duration-300 ${
currentSlide === idx
? "w-5 h-2 bg-canina-gold"
: "w-2 h-2 bg-white/60 hover:bg-white"
}`}
/>
</button>
))}
</div>
</>

View File

@ -103,8 +103,8 @@ export default function SafeImage({
quality={quality}
unoptimized={
isBlobOrData ||
!isAllowedHost ||
(typeof src === 'string' && (src.startsWith('/api/uploads/') || src.startsWith('/api/media/') || src.startsWith('/uploads/')))
!isAllowedHost ||
(typeof src === 'string' && src.toLowerCase().endsWith('.svg'))
}
className={cn(
"transition-all duration-500 w-full h-full object-contain",