Image Gallery with NextJS
Around the holidays I created an image gallery with NextJS. It turned out I had to handle a lot of images (almost 1000!).
I wanted to avoid any existing bloated image gallery with a lot of configuration options. A simple three-column gallery for desktops and a single column for the mobile view.
When the user clicks an image it should be opened and when clicked again, it should close – simple.
But how do I process the images?
– Do I store them along with the source code?
– Should I have them on a CDN like cloudinary?
– Are there any limits in file size, amount of pictures or anything else that could come up?
The best thing is probably just to dwelve into it and start coding! :)
The Tech Stack
The software stack is supposed to be simple. As my rule of thumb, I try to avoid using too many libraries.
Each library is a dependency is a liability.
I wanted to move fast, so I chose:
– NextJS as the React Framework
– Tailwind CSS for the styling
Reading images from disk
My first naïve approach was to simply load the images from a folder public/images
which I set to .gitignore
for first.
I started with a handful of images which I read with the function getStaticProps
which is handled by NextJS in order to get data for the page. This function will be evaluated at build time, so it´s not run each time a request hits the server.
1 2 3 4 5 6 7 8 9 10 |
export async function getStaticProps() { const imagesDirectory = path.join(process.cwd(), "public/images"); const filenames = await fs.readdir(imagesDirectory); return { props: { images: filenames, }, }; } |
The images´ file names are then passed along to the page Home
which houses the gallery.
1 |
export default function Home({ images }: { images: string[] }) { // … |
Problems with this approach
As long as you only want to store a few images (10, 20, maybe 100) you could even think about providing them along the code and storing them next to your source code.
The recommendation given by GitHub is to have a maximum size of 3 GB, definetely not higher than 5 GB for a repository.
When I had realised that my picture selection grew to almost 1000 pictures I needed a different approach:
cloud delivery on a CDN.
Moving images towards the cloud
There is a free service cloudinary with which you can manage your assets (images, videos). You just upload them and cloudinary offers you a generous storage size (25 GB).
Cloudinary Limits:
– 25k Monthly Transformations or
– 25GB Managed Storage or
– 25GB Monthly Net Viewing Bandwidth
Set up a cloudinary account
I created a simple, free account on cloudinary. Once logged in, I uploaded all my pictures by bulk sizes of around 100 images each time. This went quite fast.
The next thing was to figure out how to get a list of all my images from cloudinary, so that I can list them on my gallery.
Preparations
Three data items are needed in my NextJS code later on:
– The Cloud Name (which you can set individually)
– The API Key
– The API Secret
This is __sensitive__ data which needs to be treated as such.
You don´t want this information committed in your source code on GitHub.
That´s why I use environment variables which will later be read by the source code.
So I take these pieces and create a file in my repository called .env.local
with the following content:
1 2 3 |
CLOUDINARY_CLOUD_NAME=tk-one CLOUDINARY_API_KEY=YOUR_API_KEY CLOUDINARY_API_SECRET=YOUR_VERY_SECRET_API_KEY |
Now comes the fun part – connecting the API.
Connect to the cloudinary API
With the connection settings at hand it is now possible to talk to the cloudinary API.
- How do I get a list of all the images for my gallery?
- Will that be a simple fetch call or is there a more elegant, simpler solution?
It turns out that cloudinary offers several SDKs for different programming languages.
I simply installed the cloudinary SDK for nodeJS.
1 |
npm install cloudinary |
Once the module is installed, the connection can be made.
1 2 3 4 5 6 7 8 9 |
export async function getStaticProps() { const imageResources = await cloudinaryImageResources(); return { props: { images: imageResources, }, }; } |
As you can see, I extracted the function which holds the logic for the API call.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import * as cloudinary from 'cloudinary'; export default async function fetchImageResources() { // Read environment variables from .env.local const { CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET } = process.env; // Provide the cloudinary configuration cloudinary.v2.config({ cloud_name: CLOUDINARY_CLOUD_NAME, api_key: CLOUDINARY_API_KEY, api_secret: CLOUDINARY_API_SECRET, }); // Make the call (maximum number if images to retrieve is 500!) const result = await cloudinary.v2.api.resources({ type: "upload", prefix: "this_is_a_folder_name", max_results: 500 }); |
Hitting a limit of maximum provided images
“But I have around 1000 images”, and I am only allowed to fetch up to 500 images!
There is a simple solution to that.
The first call delivers a pointer or cursor to the next 500 images. So I will just do another call and join all the images together. These are resources as JSON data provided by the API.
1 2 3 4 5 6 7 8 |
const nextResult = await cloudinary.v2.api.resources({ type: "upload", prefix: "this_is_a_folder_name", next_cursor: result.next_cursor, max_results: 500 }); return [...result.resources, ...nextResult.resources]; |
Fine, now I get almost 1000 pictures returned which I can pass to my NextJS page, again.
Rendering 1000 images with the <Image>
component
Once all the pictures are passed into my page I loop over them for showing them on my gallery.
It looks similar to this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{images.map((image, index) => ( <div className="relative bg-stone-300" key={image.asset_id} onClick={() => setSelectedImage(image)} style={{ width: "375px", height: "254px" }} > <Image src={image.secure_url} alt="" fill style={{ objectFit: "cover", objectPosition: "top center" }} priority={index < 12} /> </div> ))} |
The reference to the image, which is now on the cloudinary CDN, is held in the property image.secure_url
.
A great advantage when you deal with (a lot of) images in NextJS is that all pictures are lazy loaded. More pictures will be loaded when the user scrolls down the page — not all at once!
The first 12 pictures should be prepared and loaded though. That´s why I give them the priority
flag (index < 12
).
Deploying on Vercel
Hitting the Image Optimzation limit (on the free tier)
Every image that is used with the <Image>
component is processed by Vercel.
This is for providing several image sizes to users, depending on which device they use.
Smaller devices get small images, desktop computers will receive full resolution images.
I hit the limit with almost 1000 images on Vercel.
This is also something, cloudinary can clearly provide for us.
In these cases NextJS offers a loader
function which can be utilized to provide the optimal image sources.
Thus, I see two options right now to move on:
– Implement a loader function to offload the image optimization to cloudinary
– Use a library (next-cloudinary) which provides a wrapper component to NextJS´s own <Image>
component
I will dive into that in another blog post. For now, the gallery is displayed but I am not able to add many more pictures ?
How would you proceed from here on?