بناء معرض صور احترافي باستخدام Next.js وواجهة Pexels API ومكتبة Chakra UI
Next.js. سنستفيد من قوة واجهة برمجة التطبيقات Pexels API لجلب مجموعة واسعة من الصور عالية الجودة، وسنعتمد على مكتبة Chakra UI v1، المعروفة بكونها مكتبة مكونات مرنة وسهلة الوصول، لتصميم واجهة المستخدم. علاوة على ذلك، سنتعمق في كيفية استخدام مكون الصور الخاص بـ Next.js (Next.js Image Component) لتحسين أداء الصور وجعلها أكثر كفاءة، وهو أمر بالغ الأهمية لتجربة المستخدم وتحسين محركات البحث (SEO).
إذا كنت تفضل الانتقال مباشرة إلى الكود المصدري، يمكنك الاطلاع على مستودع GitHub الخاص بالمشروع من هنا. ولرؤية النسخة المنشورة من المعرض، تفضل بزيارة: https://next-image-gallery.vercel.app/.
المفاهيم والتقنيات التي سنتناولها:
- كيفية تثبيت واستخدام مكتبة
Chakra UI v1معNext.js. - طرق جلب البيانات من واجهات برمجة التطبيقات (APIs) في
Next.js. - الاستفادة القصوى من مكون الصور
Next.js Image Componentلتحسين الأداء. - إعداد المسارات الديناميكية (
Dynamic Routes) فيNext.js.
سنبدأ الآن رحلتنا التقنية.
المتطلبات الأساسية لبدء المشروع
قبل الغوص في تفاصيل البناء، من الضروري أن تكون لديك بعض المعرفة الأساسية لضمان سير العمل بسلاسة:
- إلمام جيد بلغات تطوير الويب الأساسية:
HTML،CSS، وJavaScript. - فهم مبادئ
Reactومعرفة أساسية بإطار العملNext.js. - تثبيت بيئة التشغيل
Node.jsومدير الحزمnpm(أوyarn) على جهازك. - محرر أكواد تفضله (مثل
VS Code). - أدوات مطوري
React(React Dev Tools) للمتصفح (اختياري، لكنها مفيدة جدًا).
إذا شعرت أنك بحاجة إلى تعزيز معرفتك بأي من هذه المواضيع، فإن منصة freeCodeCamp تقدم موارد تعليمية ممتازة ستساعدك على الانطلاق بسرعة.
إعداد وتثبيت مشروع Next.js
للبدء، سنستخدم أداة Create Next App لتهيئة مشروع Next.js بسرعة وفعالية. افتح سطر الأوامر (terminal) في المجلد الجذر لمشروعك ونفّذ الأوامر التالية:
npx create-next-app next-image-gallery
cd next-image-gallery
npm run dev
سيقوم الأمر الأخير، npm run dev، بتشغيل خادم التطوير على المنفذ 3000 بجهازك. يمكنك الآن التوجه إلى المتصفح وزيارة العنوان http://localhost:3000. ستلاحظ ظهور صفحة الترحيب الافتراضية لـ Next.js:

بعد ذلك، سنقوم بتثبيت مكتبة Chakra UI التي ستساعدنا في بناء واجهة المستخدم. نفّذ الأمر التالي في سطر الأوامر:
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion @chakra-ui/icons
الخطوة التالية تتضمن تنظيف الكود النموذجي الذي تم إنشاؤه بواسطة create-next-app وتكوين المشروع لاستخدام Chakra UI. قم بحذف مجلدات styles وpages/api. ثم، حدّث ملف pages/_app.js ليصبح كالتالي:
// pages/_app.js
import { ChakraProvider } from "@chakra-ui/react";
function MyApp({ Component, pageProps }) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
}
export default MyApp;
بعد ذلك، قم بتعديل ملف pages/index.js على النحو التالي:
// pages/index.js
import Head from "next/head";
export default function Home() {
return (
<div>
<Head>
<title>NextJS Image Gallery</title>
<link rel="icon" href="/favicon.ico" />
</Head>
</div>
);
}
عد إلى http://localhost:3000. ستلاحظ أن التطبيق أصبح فارغًا، لكن العنوان في المتصفح قد تغير إلى NextJS Image Gallery. يمكنك الآن إغلاق خادم التطوير.
الحصول على مفتاح Pexels API
لاستعراض الصور في معرضنا، سنعتمد على Pexels API، وهي واجهة برمجة تطبيقات مجانية تمامًا تتيح لك جلب صور عالية الجودة. لاستخدامها، ستحتاج إلى إنشاء مفتاح API خاص بك لمصادقة طلباتك. تتيح لك Pexels API إجراء ما يصل إلى 200 طلب في الساعة و20,000 طلب شهريًا، وهي حدود كافية لمعظم المشاريع.
- إنشاء حساب Pexels: توجه إلى
https://www.pexels.com/join-consumer/وقم بإنشاء حساب جديد.
بعد ملء بياناتك، ستحتاج إلى تأكيد حسابك عبر البريد الإلكتروني قبل أن تتمكن من طلب مفتاح
API. - طلب مفتاح API: بعد تأكيد حسابك، انتقل إلى
https://www.pexels.com/api/new/واملأ التفاصيل المطلوبة لمفتاحAPIجديد، ثم انقر على زر “Request API Key”.
تذكر دائمًا الالتزام بإرشادات استخدام
API. - نسخ مفتاح API: ستظهر لك صفحة تحتوي على مفتاح
APIالخاص بك. قم بنسخه.
- تخزين مفتاح API بأمان: في المجلد الجذر لمشروعك، أنشئ ملفًا جديدًا باسم
.env.localلتخزين مفتاحAPIبشكل آمن. يمكنك استخدام الأمر التالي:touch .env.localداخل ملف
.env.local، قم بإنشاء متغير بيئة جديد باسمNEXT_PUBLIC_PEXELS_API_KEYوالصق مفتاحAPIالذي نسخته:NEXT_PUBLIC_PEXELS_API_KEY = 'YOUR_PEXELS_API_KEY_HERE'ملاحظة هامة:
Next.jsيدعم تحميل متغيرات البيئة من ملف.env.localإلىprocess.env. افتراضيًا، تكون هذه المتغيرات متاحة فقط في بيئةNode.js(على الخادم). ولكن باستخدام البادئةNEXT_PUBLIC_، يتم الكشف عن المتغير للمتصفح (العميل)، مما يجعله متاحًا للاستخدام في كود الواجهة الأمامية. يمكنك معرفة المزيد حول هذا الموضوع فيتوثيقات Next.js.
إضافة عنوان جذاب إلى المعرض
في هذا الجزء، سنقوم بإضافة عنوان رئيسي لمعرض الصور الخاص بنا، مستخدمين مكونات Chakra UI لتصميم أنيق ومتجاوب.
-
إضافة المكون
Box:قم باستيراد وإضافة المكون
Boxإلى ملفindex.jsعلى النحو التالي:// pages/index.js import Head from "next/head"; import { Box } from "@chakra-ui/react"; export default function Home() { return ( <div> <Head> <title>NextJS Image Gallery</title> <link rel="icon" href="/favicon.ico" /> </Head> <Box overflow="hidden" bg="purple.100" minH="100vh"> </Box> </div> ); }الآن، توجه إلى
http://localhost:3000. ستلاحظ أن خلفية تطبيقك أصبحت بلون أرجواني فاتح.
شرح الخصائص المستخدمة:
bg="purple.100": هذه الخاصية هي اختصار لـbackgroundفيChakra UI. تعيينها علىpurple.100يمنح التطبيق خلفية أرجوانية فاتحة. الرقم بعد اسم اللون (مثل100) يمثل درجة اللون؛ حيث50هو الأفتح و900هو الأغمق.
إليك توضيح لدرجات الألوان من توثيقات
Chakra UI:

minH="100vh": تحدد هذه الخاصية أن يكون الحد الأدنى لارتفاع التطبيق 100% من ارتفاع نافذة العرض (viewport height).minHهو اختصار لـmin-height.overflow="hidden": تُستخدم هذه الخاصية للتخلص من أشرطة التمرير الزائدة في حال تجاوز المحتوى للعنصر الأب، مما يضمن مظهرًا نظيفًا.
-
إضافة مكونات
TextوContainer:لإضافة العنوان، سنستخدم مكوني
TextوContainerمنChakra UI. قم بتعديل استيرادBoxفيindex.jsليشمل هذه المكونات:import { Box, Container, Text } from "@chakra-ui/react";الآن، أضف المكون
Containerداخل المكونBox:<Box overflow="hidden" bg="purple.100" minH="100vh"> <Container> </Container> </Box>لن تلاحظ أي تغيير مرئي مباشر في تطبيقك، ولكن المكون
Containerقد أضاف بعض الحشوة الأفقية (horizontal padding) التي ستصبح أكثر وضوحًا عند إضافة مكونText.أضف الكود التالي داخل المكون
Container:<Container> <Text color="pink.800" fontWeight="semibold" mb="1rem" textAlign="center" textDecoration="underline" fontSize={["4xl", "4xl", "5xl", "5xl"]} > NextJS Image Gallery </Text> </Container>إليك كيف سيبدو عنوان تطبيقك الآن:

تحليل خصائص العنوان:
color="pink.800": تحدد لون النص باللون الوردي الداكن.fontWeight="semibold": تضبط سمك الخط ليكون شبه غامق.mb="1rem": اختصار لـmargin-bottom، وتحدد هامشًا سفليًا بمقدار1rem(أي 16 بكسل).textAlign="center": توسيط النص أفقيًا.textDecoration="underline": تضيف خطًا تحت النص.fontSize={["4xl", "4xl", "5xl", "5xl"]}: تحدد حجم الخط.
قد تتساءل لماذا يتم تمرير أربع قيم لـ
fontSizeكـarrayداخل أقواس متعرجة{}؟
تُستخدم الأقواس{}فيJSXلتفسير التعبير بداخلها كـJavaScript. هنا، يتم استخدامarrayلتمرير قيمfontSize، وهو اختصار للاستعلامات الإعلامية (media queries) فيChakra UI. يتم تمرير القيم في مصفوفة لجعل النص متجاوبًا (responsive) وتغيير حجم الخط وفقًا لأحجام الشاشات المختلفة.const breakpoints = { sm: "30em", md: "48em", lg: "62em", xl: "80em", }تتبع هذه الطريقة نهج “الجوال أولاً” (
mobile-first)، حيث تكون القيمة الأولى للأجهزة الأصغر، والقيمة الأخيرة لأجهزة سطح المكتب. هذا يعني أنfont-sizeيتغير بناءً على نقطة التوقف (breakpoint) المحددة. يمكنك قراءة المزيد حول هذا الموضوع فيتوثيقات Chakra UI.الكود أعلاه سيولد
CSSمشابهًا لهذا:.css-px6f4t { text-align: center; -webkit-text-decoration: underline; text-decoration: underline; font-size: 2.25rem; color: #702459; font-weight: 600; margin-bottom: 1rem; } @media screen and (min-width: 30em) { .css-px6f4t { font-size: 2.25rem; } } @media screen and (min-width: 48em) { .css-px6f4t { font-size: 3rem; } } @media screen and (min-width: 62em) { .css-px6f4t { font-size: 3rem; } }إليك مقارنة لأحجام العناوين المختلفة على شاشات متعددة كما تظهر في
Polypane:
جلب البيانات من واجهة Pexels API
بعد الحصول على مفتاح API، حان الوقت لكتابة الكود اللازم لجلب الصور. سنقوم بإنشاء ملف منفصل لتنظيم دوال جلب البيانات.
-
إنشاء ملف
api.js:في المجلد الجذر لمشروعك، أنشئ مجلدًا باسم
lib، وداخله أنشئ ملفًا باسمapi.js. يمكنك استخدام الأوامر التالية في سطر الأوامر:mkdir lib cd lib touch api.jsعنوان
URLالأساسي لـPexels APIللصور هو:https://api.pexels.com/v1/.
توفرPexels APIثلاث نقاط نهاية (endpoints) رئيسية:/curated: لجلب صور منتقاة بعناية بواسطة فريقPexelsفي الوقت الفعلي./search: للبحث عن الصور بناءً على استعلام محدد./photos/:id: للحصول على صورة واحدة محددة باستخدام معرفها (ID).
في البداية، سنستخدم نقطة النهاية
/curatedلعرض الصور المنتقاة على الصفحة الرئيسية لتطبيقنا. -
إضافة دالة
getCuratedPhotos:أضف الكود التالي إلى ملف
api.js:const API_KEY = process.env.NEXT_PUBLIC_PEXELS_API_KEY; export const getCuratedPhotos = async () => { const res = await fetch( `https://api.pexels.com/v1/curated?page=11&per_page=18`, { headers: { Authorization: API_KEY, }, } ); const responseJson = await res.json(); return responseJson.photos; };شرح الكود أعلاه:
- نبدأ بتعريف متغير
API_KEYالذي يصل إلى مفتاحAPIالمخزن في متغير البيئةNEXT_PUBLIC_PEXELS_API_KEYباستخدامprocess.env. - ثم ننشئ دالة غير متزامنة (
async function) باسمgetCuratedPhotos()تستخدم طريقةfetch()لجلب البيانات منAPI. - إذا نظرت عن كثب إلى
URLالخاص بالطلب، ستلاحظ أننا أضفنا?page=11&per_page=18بعد نقطة النهاية/curated. هذه هي معاملات اختيارية (optional parameters) يمكنك تمريرها إلى نقطة النهاية/curatedكسلاسل استعلام (query strings). هنا،page=11تعني جلب الصفحة الحادية عشرة، وper_page=18تعني جلب 18 صورة لكل صفحة. يمكنك إزالة هذه المعاملات الاختيارية، وفي هذه الحالة ستعيدAPI15 صورة من الصفحة الأولى. يمكنك الحصول على ما يصل إلى 80 صورة في طلب واحد. - يتم تمرير مفتاح
Pexels APIفي حقلAuthorizationضمنheadersالطلب. - تقوم الدالة
res.json()بتحليل الاستجابة بتنسيقJSON. - يحتوي الكائن
responseJsonعلى حقول مثلpageوper_pageوغيرها، والتي لا يستخدمها تطبيقنا حاليًا. لذلك، يتم إرجاع حقلphotosفقط من الاستجابة، والذي يبدو كالتالي:
[ { id: 4905078, width: 7952, height: 5304, url: "https://www.pexels.com/photo/ocean-waves-under-blue-sky-4905078/", photographer: "Nick Bondarev", photographer_url: "https://www.pexels.com/@nick-bondarev", photographer_id: 2766954, src: { original: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg", large2x: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940", large: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&h=650&w=940", medium: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&h=350", small: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&h=130", portrait: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=1200&w=800", landscape: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=627&w=1200", tiny: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&dpr=1&fit=crop&h=200&w=280", }, liked: false, }, ];في حقل
src، يتم توفير العديد من تنسيقات الصور المختلفة للاختيار من بينها. في هذا الدرس، سنستخدم صورًا من نوعportraitعلى صفحتنا الرئيسية، ولكن يمكنك استكشاف التنسيقات الأخرى بحرية. مع تطور تطبيقنا، سنقوم بكتابة دوال للبحث عن الصور والحصول على صورة واحدة محددة في ملفapi.js. في الوقت الحالي، سنستخدم هذه الدالة لعرض الصور على صفحتنا الرئيسية. - نبدأ بتعريف متغير
عرض الصور على الصفحة الرئيسية
بعد أن أنشأنا الدالة اللازمة لجلب البيانات، حان الوقت لعرض هذه الصور على صفحتنا.
-
استيراد دالة
getCuratedPhotos():أولاً، قم باستيراد الدالة
getCuratedPhotos()في ملفindex.js:import Head from "next/head"; import { Box, Container, Text } from "@chakra-ui/react"; import { getCuratedPhotos } from "../lib/api"; -
استخدام
getServerSideProps()لجلب البيانات:سنستخدم دالة
getServerSideProps()المتاحة فيNext.jsلجلب البيانات منPexels APIوحقنها مباشرة في صفحتنا قبل عرضها. هذه الدالة تُنفذ على جانب الخادم (server-side)، مما يجعل المحتوى جاهزًا عند تحميل الصفحة الأولى، وهو أمر مفيد جدًا لتحسين محركات البحث (SEO) وتجربة المستخدم. يمكنك قراءة المزيد حولgetServerSideProps()فيتوثيقات Next.js.أضف الكود التالي في نهاية ملف
index.js:export async function getServerSideProps() { const data = await getCuratedPhotos(); return { props: { data }, }; }تقوم هذه الدالة غير المتزامنة باستخدام
getCuratedPhotos()لجلب الصور منPexels APIوتخزينها في المتغيرdata. ثم يتم توفير هذا المتغير كخاصية (prop) للمكونHome.لجعل
dataمتاحة كمُدخل للمكونHome، قم بتعديل تعريف الدالةHomeكالتالي:export default function Home({ data }) { // ... }أعد تشغيل خادم التطوير الخاص بك. يمكنك الآن داخل المكون
Homeطباعة محتوىdataإلى وحدة التحكم (console) للتحقق:export default function Home({ data }) { console.log(data); return ( // ... ); }توجه إلى
http://localhost:3000/وافتح وحدة تحكم المطور (بالضغط علىCTRL + Shift + JفيChromeأوCTRL + Shift + KفيFirefox). ستشاهد البيانات التي تم جلبها:
-
إدارة حالة الصور باستخدام
useState():قم بإزالة سطر
console.log(). ثم أضف الكود التالي في الجزء العلوي من ملفindex.jsلاستيراد الخطافuseState()من مكتبةreact:import React, { useState } from "react";سنقوم بتخزين البيانات التي تم جلبها من
Pexels APIفي حالة (state) باسمphotos. أضف الكود التالي قبل عبارةreturnفي دالةHome:const [photos, setPhotos] = useState(data); -
عرض الصور باستخدام عنصر
<img>(النهج الأولي):لعرض الصور، سنقوم بعمل حلقة تكرارية (
map) على مصفوفةphotosونمررpic.src.originalإلى خاصيةsrcلعنصر<img>. أضف الكود التالي بعد مكونContainer:{photos.map((pic) => ( <img src={pic.src.original} width="500" height="500" /> ))}سيبدو تطبيقك الآن بهذا الشكل:

مشكلة الأداء مع عنصر
<img>التقليدي:
بصرف النظر عن أن الصور لا يتم تغيير حجمها بشكل صحيح، هناك مشكلة أخرى خطيرة في استخدامنا لعنصر<img>التقليدي لعرض الصور.توجه إلى
http://localhost:3000/وافتح أدوات المطور، ثم انتقل إلى تبويب “الشبكة” (Network tab) (Ctrl + Shift + EفيFirefoxوCtrl + Shift + JفيChrome). ستبدو فارغة في البداية:
الآن، أعد تحميل الصفحة. ستلاحظ أن تبويب الشبكة قد امتلأ بالبيانات:

كما ترى في الصورة أعلاه، يبلغ حجم الملف المطلوب أكثر من 11 ميجابايت، وهذا لملف صورة واحد فقط! يمكن أن تتراوح الأحجام من 10 إلى 100 ميجابايت أو أكثر بناءً على جودة الصورة. تخيل أن لديك 80 صورة على الصفحة الرئيسية لتطبيقك. هل من المنطقي نقل حوالي 800 ميجابايت من الملفات في كل مرة يزور فيها شخص معرضك أو موقعك الإلكتروني؟ بالتأكيد لا.

لهذا السبب، يتم اليوم تقديم معظم الصور على الويب بتنسيق
WebP. يقلل هذا التنسيق بشكل كبير من حجم الصورة، وبالكاد يمكنك اكتشاف أي فرق بصري. لذا، نحتاج إلى تغيير تنسيق الصورة إلىwebp، ولكن السؤال هو: كيف؟ هل تحتاج إلى القيام بذلك يدويًا؟ إذا كان الأمر كذلك، ألن يكون ذلك مضيعة للوقت ومجهدًا؟ -
تحسين الصور باستخدام مكون
Next.js Image:لحسن الحظ، لا تحتاج إلى القيام بذلك يدويًا! يأتي
Next.jsفي إصداره العاشر وما بعده بدعم مدمج لتحسين الصور باستخدام مكونImage. يمكنك قراءة المزيد حول هذا التحديث فيتوثيقات Next.js Image.لذا، دعنا نستبدل عنصر
<img>بمكونNext.js Image. أولاً، قم باستيراد هذا المكون داخل ملفindex.jsالخاص بك:import Image from "next/image";ولكن قبل استخدام هذا المكون في الكود، نحتاج إلى إخبار
Next.jsبأن صورنا تأتي من مصدر خارجي، مثلPexels. أوقف خادم التطوير الخاص بك وأنشئ ملفnext.config.jsبتشغيل الأمر التالي:touch next.config.jsأضف الكود التالي إلى ملف
next.config.js:module.exports = { images: { domains: ["images.pexels.com"], }, };هذا كل ما في الأمر. هناك تكوينات أخرى مثل
pathوimageSizesوdeviceSizesوما إلى ذلك يمكنك إضافتها في حقلimages. ولكن في هذا الدرس، سنتركها كقيم افتراضية. يمكنك قراءة المزيد حول التكوين فيتوثيقات Next.js Image.الآن، استبدل عنصر
<img>بمكونImageومرر الخصائص (props) كما هو موضح أدناه:{photos.map((pic) => ( <Image src={pic.src.portrait} height={600} width={400} alt={pic.url} /> ))}كما ناقشنا سابقًا، توفر
Pexels APIتنسيقات أو أحجامًا مختلفة لنفس الصورة، مثلportraitوlandscapeوtinyوما إلى ذلك، ضمن حقلsrc. يستخدم هذا الدرس صورportraitعلى الصفحة الرئيسية، ولكنك حر في استكشاف الأحجام الأخرى.مثال على حقل
src:src: { original: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg", large2x: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940", large: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&h=650&w=940", medium: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&h=350", small: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&h=130", portrait: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=1200&w=800", landscape: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&fit=crop&h=627&w=1200", tiny: "https://images.pexels.com/photos/4905078/pexels-photo-4905078.jpeg?auto=compress&cs=tinysrgb&dpr=1&fit=crop&h=200&w=280", }كما ترى في حقل
srcالنموذجي أعلاه، يبلغ عرض تنسيقportraitللصورة 800 بكسل وارتفاعه 1200 بكسل. لكن هذا الحجم كبير جدًا للعرض على صفحة الويب، لذلك سنقوم بتقليصه عن طريق قسمة العرض والارتفاع على 2. لذا، تم تمرير600للارتفاع و400للعرض لمكونImage.أعد تشغيل خادم التطوير الخاص بك وتوجه إلى
http://localhost:3000/. ستلاحظ أن التطبيق نفسه يبدو تمامًا كما كان. ولكن هذه المرة، إذا فتحت تبويب الشبكة وأعدت تحميل الصفحة، سترى شيئًا سحريًا حقًا! صورك الآن بتنسيقwebp، وقد تم تقليل أحجامها بشكل كبير.
التحميل الكسول (Lazy Loading) مع مكون
Next.js Image:
لقد أضاف مكونNext.js Imageأيضًا ميزة التحميل الكسول (lazy loading) للصور تلقائيًا. إليك مثال لشرح كيفية ولماذا يجب عليك استخدام التحميل الكسول إذا لم تكن مألوفًا به:على الرغم من أن الصور أصبحت الآن بتنسيق
webp، هل من الضروري تحميل جميع الصور في كل مرة يزور فيها شخص موقعك الإلكتروني؟ وإذا زار الزائر الصفحة وغادر دون التمرير، فهل من المنطقي تحميل الصور الموجودة في أسفل الصفحة؟لا توجد حاجة لتحميل الصور التي لن يراها المستخدم أو الزائر في معظم الحالات. وهنا يأتي دور التحميل الكسول لإنقاذ الموقف. فهو يؤخر طلبات الصور إلى الوقت الذي تكون فيه مطلوبة، أو في هذه الحالة، عندما تظهر الصور في مجال الرؤية (
viewport). هذا يساعد بشكل كبير في تقليل وزن الصفحة الأولي ويزيد من أداء الموقع.إذا توجهت إلى
http://localhost:3000/وقمت بالتمرير عبر جميع الصور، فسترى أن الصور التي ليست في مجال الرؤية لا يتم تحميلها في البداية. ولكن كلما قمت بالتمرير لأسفل، يتم نقلها وتحميلها.
بشكل افتراضي، تكون قيمة الخاصية
layoutلمكونImageهيintrinsic، مما يعني أن الصورة ستقوم بتقليص الأبعاد لأحجام الشاشات الأصغر ولكنها ستحافظ على الأبعاد الأصلية لأحجام الشاشات الأكبر. هناك العديد من الخصائص الأخرى التي يمكنك تمريرها إلى مكونImageلتعديله بشكل أكبر. يمكنك قراءة المزيد عنها فيتوثيقات Next.js Image.
تنسيق وعرض الصور باستخدام Chakra UI
لتنسيق الصور بشكل جذاب ومنظم، سنستخدم مكون Wrap من Chakra UI. هذا المكون هو مكون تخطيط (layout component) يضيف مسافة محددة بين عناصره الفرعية (الصور في حالتنا)، ويقوم بتغليفها تلقائيًا (wraps) إذا لم يكن هناك مساحة كافية لاستيعابها جميعًا في سطر واحد.
-
استيراد
WrapوWrapItem:قم باستيراد
WrapوWrapItemمنChakra UIعن طريق تعديل سطر الاستيراد:import { Box, Container, Text, Wrap, WrapItem } from "@chakra-ui/react";يحتوي
WrapItemعلى العناصر الفرعية الفردية (الصور)، بينما يحيطWrapبجميع مكوناتWrapItem. -
تعديل عرض الصور باستخدام
Wrap:عدّل التعبير الخاص بعرض الصور ليصبح كالتالي:
<Wrap px="1rem" spacing={4} justify="center"> {photos.map((pic) => ( <Image src={pic.src.portrait} height={600} width={400} alt={pic.url} /> ))} </Wrap>شرح الخصائص في الكود أعلاه:
px="1rem": هي خاصية اختصار لـpadding-leftوpadding-right. تضيف هذه الخاصية حشوة أفقية بمقدار1rem.spacing={4}: تطبق تباعدًا بين كل عنصر فرعي. سيظهر هذا التباعد بوضوح بمجرد تغليف كل صورة بـWrapItem.justify="center": تقوم بمحاذاة الصور في المنتصف أفقيًا.
-
تغليف الصور بـ
WrapItemوإضافة التنسيقات:الآن، قم بتغليف كل صورة بمكون
WrapItem. أضف الكود التالي داخل تعبيرJavaScript:<Wrap px="1rem" spacing={4} justify="center"> {photos.map((pic) => ( <WrapItem key={pic.id} boxShadow="base" rounded="20px" overflow="hidden" bg="white" lineHeight="0" _hover={{ boxShadow: "dark-lg" }} > <Image src={pic.src.portrait} height={600} width={400} alt={pic.url} /> </WrapItem> ))} </Wrap>شرح الخصائص الممررة إلى
WrapItem:key={pic.id}: تُعطي كل صورة مفتاحًا فريدًا حتى يتمكنReactمن التمييز بين العناصر الفرعية أو الصور.boxShadow="base": تضيف ظلًا أساسيًا إلىWrapItem.rounded="20px": تضيف نصف قطر حدود (border-radius) بمقدار20px، مما يجعل زوايا الصورة مستديرة.overflow="hidden": تضمن عدم تجاوز الصورة لحدودWrapItem، مما يحافظ على شكل الزوايا المستديرة.bg="white": تضيف خلفية بيضاء إلىWrapItem.lineHeight="0": تضبط خاصيةline-heightعلى صفر، وهي مفيدة أحيانًا لتجنب المسافات الإضافية حول الصور._hover={{ boxShadow: "dark-lg" }}: تغير خاصيةboxShadowإلى ظل أغمق (dark-lg) عند مرور مؤشر الفأرة فوق الصورة، مما يضيف تأثيرًا بصريًا جذابًا.

ستلاحظ أن خاصية
spacing={4}قد أصبحت فعالة الآن بعد أن أضفناWrapItemللصور، مما يوفر تباعدًا متناسقًا بينها.
إضافة وظيفة البحث إلى المعرض
الخطوة التالية هي تمكين المستخدمين من البحث عن الصور وعرض النتائج لهم. لتحقيق ذلك، سنستخدم نقطة النهاية /search في Pexels API.
-
إنشاء دالة
getQueryPhotos():في ملف
lib/api.js، أنشئ دالة جديدة باسمgetQueryPhotos()للبحث عن الصور بناءً على مدخلات المستخدم:export const getQueryPhotos = async (query) => { const res = await fetch( `https://api.pexels.com/v1/search?query=${query}`, { headers: { Authorization: API_KEY, }, } ); const responseJson = await res.json(); return responseJson.photos; };تتشابه دالة
getQueryPhotos()أعلاه مع دالةgetCuratedPhotos()، ولكن هنا أضفنا معاملqueryإلى الدالة وقمنا بتعديل نقطة نهايةAPIلتضمين هذا الاستعلام:`https://api.pexels.com/v1/search?query=${query}` -
استيراد دالة
getQueryPhotos():قم باستيراد دالة
getQueryPhotos()في ملفindex.js:import { getCuratedPhotos, getQueryPhotos } from "../lib/api"; -
بناء نموذج البحث باستخدام Chakra UI:
الآن، سننشئ نموذجًا لأخذ مدخلات المستخدم والبحث بناءً عليها. سنقوم باستيراد واستخدام مكونات
Input،IconButton،InputRightElement، وInputGroupمنChakra UIلإنشاء هذا النموذج.عدّل استيراد
Chakra UIوأضف استيرادًا لـSearchIcon:import { Box, Container, Text, Wrap, WrapItem, Input, IconButton, InputRightElement, InputGroup, } from "@chakra-ui/react"; import { SearchIcon } from "@chakra-ui/icons";أضف الكود التالي لنموذج الإدخال داخل مكون
Containerفي ملفindex.js:<InputGroup pb="1rem"> <Input placeholder="Search for Apple" variant="ghost" /> <InputRightElement children={ <IconButton aria-label="Search" icon={<SearchIcon />} bg="pink.400" color="white" /> } / </InputGroup>شرح المكونات:
InputGroup: يُستخدم لتجميع مكونيInputوInputRightElement. الخاصيةpbهي اختصار لـpadding-bottom.Input: هو حقل الإدخال حيث سيقوم المستخدمون بكتابة استعلاماتهم. يحتوي على نص توضيحي (placeholder) “Search for Apple”.InputRightElement: يُستخدم لإضافة عنصر إلى يمين مكونInput. هنا، تم تمرير زر أيقونة (IconButton) مع أيقونة البحث إلى خاصيةchildren.IconButton: مكون فيChakra UIمفيد عندما تريد استخدام أيقونة كزر. يتم تمرير الأيقونة المراد عرضها داخل خاصيةicon.
إليك كيف سيبدو حقل الإدخال:
-
إدارة حالة الإدخال ومعالجة الإرسال:
هذا النموذج لا يفعل شيئًا بعد. دعنا نغير ذلك.
عرّف حالة جديدة باسمqueryلتخزين مدخلات المستخدم:export default function Home({ data }) { const [photos, setPhotos] = useState(data); const [query, setQuery] = useState(""); // ... }عدّل مكون
Inputلإنشاء ربط ثنائي الاتجاه (two-way bind) بين حقل الإدخال وحالةqueryباستخدام خاصيةvalueوحدثonChange:<Input placeholder="Search for Apple" variant="ghost" value={query} onChange={(e) => setQuery(e.target.value)} />الآن، أنشئ دالة باسم
handleSubmit()لمعالجة حدث النقر على أيقونة البحث. في الوقت الحالي، سنقوم فقط بطباعة الاستعلام إلى وحدة التحكم ومسح الحقل بعد ذلك:export default function Home({ data }) { const [photos, setPhotos] = useState(data); const [query, setQuery] = useState(""); const handleSubmit = async (e) => { await e.preventDefault(); await console.log(query); await setQuery(""); }; // ... }أضف هذه الدالة إلى حدث
onClickلـIconButton:<InputRightElement children={ <IconButton aria-label="Search" icon={<SearchIcon />} onClick={handleSubmit} bg="pink.400" color="white" /> } /توجه إلى
http://localhost:3000/واكتب شيئًا في حقل الإدخال وانقر على زر البحث.
ولكن هذا النموذج لا يزال ينقصه شيء: إذا حاولت البحث عن شيء بالضغط على
Enterبدلاً من زر البحث، فستتم إعادة تحميل الصفحة، ولن يتم تسجيل الاستعلام. لإصلاح ذلك، قم بتغليفInputGroupبعنصر<form>ومرر دالةhandleSubmitإلى حدثonSubmitكالتالي:<form onSubmit={handleSubmit}> <InputGroup pb="1rem"> <Input placeholder="Search for Apple" variant="ghost" value={query} onChange={(e) => setQuery(e.target.value)} / <InputRightElement children={ <IconButton aria-label="Search" icon={<SearchIcon />} onClick={handleSubmit} bg="pink.400" color="white" / } / </InputGroup> </form>الآن، ستلاحظ أن الضغط على
Enterسيعمل بشكل صحيح. -
تحديث دالة
handleSubmitلجلب الصور:حدّث دالة
handleSubmitعلى النحو التالي لجلب الصور بناءً على استعلام المستخدم:const handleSubmit = async (e) => { await e.preventDefault(); const res = await getQueryPhotos(query); await setPhotos(res); await setQuery(""); };تقوم الدالة أعلاه بتمرير متغير
queryإلى دالةgetQueryPhotos()، وتقوم البيانات التي يتم إرجاعها من الدالة بتجاوز القيمة السابقة في متغيرphotosباستخدامsetPhotos(res). وبهذا تكون قد انتهيت! يمكنك الآن البحث عن الصور في تطبيقك.
-
معالجة استعلام البحث الفارغ باستخدام
Toast:لا يزال هناك شيء مفقود. ماذا لو حاول المستخدم البحث بدون أي استعلام، أي بسلسلة نصية فارغة (
empty string)؟ سيظل الكود الحالي يحاول إجراء طلب باستخدام""، وسنواجه الخطأ التالي:
لمعالجة هذه المشكلة، سنستخدم مكون
ToastمنChakra UIلعرض رسالة خطأ للمستخدم.
قم باستيرادuseToastمنChakra UI:import { Box, Container, Text, Wrap, WrapItem, Input, IconButton, InputRightElement, InputGroup, useToast, } from "@chakra-ui/react";أضف الكود التالي مباشرة بعد تعريف الحالات (
states) لتهيئةToast:export default function Home({ data }) { const [photos, setPhotos] = useState(data); const [query, setQuery] = useState(""); const toast = useToast(); // ... }عدّل دالة
handleSubmit()كالتالي:const handleSubmit = async (e) => { await e.preventDefault(); if (query === "") { toast({ title: "خطأ في البحث.", description: "الرجاء إدخال كلمة للبحث.", status: "error", duration: 9000, isClosable: true, position: "top", }); } else { const res = await getQueryPhotos(query); await setPhotos(res); await setQuery(""); } };في الكود أعلاه، نتحقق مما إذا كان
queryفارغًا أم لا باستخدام عبارةif/elseبسيطة. وإذا كان فارغًا، فإننا نعرض رسالة خطأ باستخدامtoastمع نص “الرجاء إدخال كلمة للبحث.”.جرب الضغط على
Enterدون كتابة أي شيء في حقل الإدخال. سترى رسالةtoastبهذا الشكل:
إضافة مسارات ديناميكية للصور
سنقوم بإنشاء مسار ديناميكي (dynamic route) لكل صورة، مما يسمح للمستخدمين بالنقر على الصور للحصول على مزيد من المعلومات التفصيلية عنها. تتميز Next.js بميزة رائعة تتيح لك إنشاء مسارات ديناميكية عن طريق إضافة أقواس مربعة إلى اسم الصفحة (مثل [param])، حيث يمكن أن يكون param عبارة عن معرف (ID) أو جزء من URL. في حالتنا هذه، سيكون param هو id، نظرًا لأننا نحتاج إلى توفير معرف الصورة للحصول على صورة محددة من Pexels API.
-
إنشاء ملف المسار الديناميكي:
في المجلد الجذر لمشروعك، قم بتشغيل الأوامر التالية لإنشاء ملف
[id].jsداخل مجلدphotosضمن مجلدpages:mkdir pages/photos cd pages/photos touch [id].js -
استيراد مكون
Link:قم باستيراد مكون
Linkمنnext/linkفي ملفindex.js. يساعدLinkفي الانتقالات من جانب العميل (client-side transitions) بين المسارات، مما يوفر تجربة تصفح أسرع. يمكنك قراءة المزيد حولLinkفيتوثيقات Next.js Link.import Link from "next/link"; -
إضافة
Linkإلى كل صورة:أضف
Linkإلى كل صورة كالتالي:<Link href={`/photos/${pic.id}`}> <a> <Image src={pic.src.portrait} height={600} width={400} alt={pic.url} /> </a> </Link>توجه إلى تطبيقك وحاول النقر على أي صورة. ستظهر لك رسالة خطأ لأننا أنشأنا ملف
photos/[id].jsولكن لم نضف أي كود بداخله بعد. ولكن إذا لاحظتURLلهذه الصفحة، فسيكون شيئًا كهذا:http://localhost:3000/photos/2977079 -
إنشاء دالة
getPhotoById():سنقوم الآن بإنشاء دالة ثالثة باسم
getPhotoById()في ملفlib/api.jsللحصول على صورة محددة بناءً على معرفها (ID). أضف الكود التالي إلى ملفapi.js:export const getPhotoById = async (id) => { const res = await fetch(`https://api.pexels.com/v1/photos/${id}`, { headers: { Authorization: API_KEY, }, }); const responseJson = await res.json(); return responseJson; };يستخدم الكود أعلاه نقطة النهاية
/photosللحصول على صورة واحدة منPexels API. ستلاحظ أنه على عكس دالتيgetCuratedPhotosوgetQueryPhotos، فإن دالةgetPhotoByIdتُرجع الكائنresponseJsonبالكامل وليس فقط حقلresponseJson.photos. -
تكوين صفحة الصورة الفردية (
photos/[id].js):أضف الكود التالي إلى ملف
photos/[id].js:import { getPhotoById } from "../../lib/api"; import { Box, Divider, Center, Text, Flex, Spacer, Button, } from "@chakra-ui/react"; import Image from "next/image"; import Head from "next/head"; import Link from "next/link"; import { InfoIcon, AtSignIcon } from "@chakra-ui/icons"; export default function Photos({ pic }) { return ( <Box p="2rem" bg="gray.200" minH="100vh"> <Head> <title>Image: {pic.id}</title> <link rel="icon" href="/favicon.ico" /> </Head> <Flex px="1rem" justify="center" align="center"> <Text letterSpacing="wide" textDecoration="underline" as="h2" fontWeight="semibold" fontSize="xl" as="a" target="_blank" href={pic.photographer_url} > <AtSignIcon /> {pic.photographer} </Text> <Spacer /> <Box as="a" target="_blank" href={pic.url}> <InfoIcon focusable="true" boxSize="2rem" color="red.500" />{" "} </Box>{" "} <Spacer /> <Link href={`/`}> <Button as="a" borderRadius="full" colorScheme="pink" fontSize="lg" size="lg" cursor="pointer" > 🏠 الرئيسية </Button> </Link> </Flex> <Divider my="1rem" /> <Center> <Box as="a" target="_blank" href={pic.url}> <Image src={pic.src.original} width={pic.width / 4} height={pic.height / 4} quality={50} priority loading="eager" / </Box> </Center> </Box> ); } export async function getServerSideProps({ params }) { const pic = await getPhotoById(params.id); return { props: { pic }, }; }لقد أضفنا لون خلفية رمادي فاتح باستخدام خاصية
bgومكونBox. لتوفير الوقت، قمنا باستيراد جميع المكونات والأيقونات مسبقًا.أعد تشغيل خادم التطوير الخاص بك. قد تتساءل كيف تحصل دالة
getServerSideProps()على معرف الصورة (id) من معاملparams؟ نظرًا لأن هذه الصفحة تستخدم مسارًا ديناميكيًا، فإن الكائنparamsيحتوي على معاملات المسار. هنا، اسم الصفحة هو[id].js، لذا سيبدو الكائنparamsكالتالي:{ id: ... }. يمكنك محاولة طباعةconsole.log(params)وسترى شيئًا كهذا:{ id: '4956064' }قم بتمرير خاصية
picهذه إلى دالة مكونPhotosكمعامل.إليك كيف ستبدو صفحتك الآن:

تحليل الكود خطوة بخطوة:
-
تعديل عنوان الصفحة:
نقوم أولاً بتعديل عنوان الصفحة، عن طريق تمرير معرف الصورة بعد نص “Image”:
<Head> <title>Image: {pic.id}</title> <link rel="icon" href="/favicon.ico" /> </Head> -
إنشاء شريط التنقل (Navbar):
ثم نقوم بإنشاء شريط تنقل باستخدام مكون
Flex:<Flex px="1rem" justify="center" align="center"> // ... </Flex>هنا،
pxهو اختصار لخاصيتيpadding-leftوpadding-right، وjustifyوalignهما اختصار لـjustify-contentوalign-itemsعلى التوالي. -
إضافة رابط المصور:
بعد ذلك، نضيف رابطًا للمصور باستخدام مكون
TextوأيقونةAtSignIcon. يمكنك أيضًا استخدام علامة@بدلاً منAtSignIcon.<Text letterSpacing="wide" textDecoration="underline" as="h2" fontWeight="semibold" fontSize="xl" as="a" target="_blank" href={pic.photographer_url} > <AtSignIcon /> {pic.photographer} </Text>خاصية
asهي ميزة فيChakra UIتسمح لك بتمرير وسمHTMLأو مكون ليتم عرضه. هنا، نستخدمها مع وسم<a>بحيث يتم عرض مكونTextكـ وسم<a>على الصفحة. تضمنtarget="_blank"أن يفتح الرابط في نافذة أو علامة تبويب جديدة.
-
مكون
Spacer:ثم نضيف مكون
Spacerالذي، عند استخدامه معFlex، يوزع المساحة الفارغة بين العناصر الفرعية لـFlex. يمكنك قراءة المزيد حوله فيتوثيقات Chakra UI Spacer.
-
أيقونة المعلومات:
بعد ذلك، نضيف أيقونة معلومات ترتبط بالصورة الأصلية على
Pexels.<Box as="a" target="_blank" href={pic.url}> <InfoIcon focusable="true" boxSize="2rem" color="red.500" /> </Box>{" "} <Spacer />
-
زر العودة للصفحة الرئيسية:
ثم نضيف زر “الرئيسية” في شريط التنقل لإعادة المستخدم إلى الصفحة الرئيسية للتطبيق باستخدام مكون
Linkمنnext/link.<Link href={`/`}> <Button as="a" borderRadius="full" colorScheme="pink" fontSize="lg" size="lg" cursor="pointer" > 🏠 الرئيسية </Button> </Link>
-
مكون
Divider:ثم نستخدم مكون
Dividerلتقسيم شريط التنقل والصورة.<Divider my="1rem" />هنا،
myهو اختصار لخاصيتيmargin-topوmargin-bottom. -
عرض الصورة المركزية:
أخيرًا، نضيف الصورة إلى الصفحة باستخدام مكون
Center، والذي، كما يوحي اسمه، يقوم بتوسيط عناصره الفرعية.<Center> <Box as="a" target="_blank" href={pic.url}> <Image src={pic.src.original} width={pic.width / 4} height={pic.height / 4} quality={50} priority loading="eager" / </Box> </Center>في الكود أعلاه، نستخدم مكون
Boxلإضافة رابط للصورة الأصلية علىPexelsباستخدام خاصيةas. ستلاحظ أيضًا أننا مررنا بعض الخصائص الإضافية إلى مكونImage:src: نمرر الصورة الأصلية (original) هذه المرة. نقوم بتغيير حجم الصورة عن طريق قسمة العرض والارتفاع الأصليين على 4.priority: بتمرير هذه الخاصية، تُعتبر الصورة ذات أولوية عالية ويتم تحميلها مسبقًا (preloaded).quality={50}: بشكل افتراضي، يقلل مكونImageجودة الصور المحسنة إلى 75%، ولكن نظرًا لأن الصورة لا تزال كبيرة جدًا، فإننا نقلل جودتها إلى 50% لزيادة التحسين.loading="eager": بشكل افتراضي، يكون سلوك التحميل كسولًا (lazy) في مكونImage، ولكن هنا نريد عرض الصورة فورًا، وبالتالي نمررloading="eager".
إليك الكود أعلاه وهو يعمل:

لقد نجحت! 🎉 تهانينا 👏 على بناء مشروع معرض صور
Next.jsهذا.
-
الخلاصة التقنية
في هذا الدرس الشامل، استعرضنا خطوة بخطوة كيفية بناء معرض صور متكامل وديناميكي باستخدام إطار العمل Next.js. لقد تعلمنا كيفية الاستفادة من Pexels API لجلب الصور، وتطبيق مكتبة Chakra UI لتصميم واجهة مستخدم عصرية ومتجاوبة. الأهم من ذلك، تعمقنا في أهمية تحسين الصور باستخدام مكون Next.js Image Component لضمان أفضل أداء وتحميل سريع للصفحات، وهو عامل حاسم لتجربة المستخدم ونجاح SEO. كما اكتشفنا قوة المسارات الديناميكية في Next.js لإنشاء صفحات تفصيلية لكل صورة.
هذا المشروع يمثل نقطة انطلاق ممتازة لأي مطور يرغب في بناء تطبيقات ويب غنية بالصور مع التركيز على الأداء. إن استخدام getServerSideProps يضمن أن المحتوى جاهز للعرض على الخادم، مما يعزز من قابلية فهرسة الموقع من قبل محركات البحث.
واجهات برمجة تطبيقات أخرى يمكنك استكشافها:
مصادر إضافية مفيدة:
هل ترغب في جزء ثانٍ لهذا الدرس، حيث نضيف تأثيرات حركية للصور باستخدام Framer Motion؟ أخبرني على Twitter!
ما هي المشاريع أو الدروس التعليمية الأخرى التي ترغب في رؤيتها؟ تواصل معي على Twitter، وسأقوم بتغطيتها في مقالي القادم!
إذا ألهمك هذا المقال لإضافة ميزات بنفسك، يرجى المشاركة ووضع علامة لي – أحب أن أسمع عنها 🙂
أشوتوش ك. سينغ
اقرأ المزيد من المنشورات. إذا كان هذا المقال مفيدًا، شاركه.
تعلم البرمجة مجانًا. ساعد منهج freeCodeCamp مفتوح المصدر أكثر من 40,000 شخص في الحصول على وظائف كمطورين. ابدأ الآن.