Building a Photo Gallery with Strapi, Nextjs and Cloudinary
Tags: strapi, nextjs, cloudinary, webdev
Published: 2022-08-23 08:01:00

_This article explains how to build a photo gallery with Strapi, Next.js and Cloudinary._

**Author: Mary Okosun**

Headless Content Management Systems are powerful in many ways; they give us the flexibility to do whatever we want to do with our preferred frontend technology. [Strapi is one of the most popular headless CMSes out there](https://strapi.io/), and it makes working with the backend side of things a breeze.
This tutorial will discuss how to make a photo gallery with Strapi and [Next.js](https://strapi.io/integrations/nextjs-cms), using [Cloudinary](https://www.npmjs.com/package/strapi-upload-cloudinary) to store our images.

## Prerequisites

To follow along with this tutorial, you should have the following:
- A Github account
- Node.js v12 and above
- Yarn 1.22+ or npm (version 6 only) to run the CLI installation scripts.
- A Cloudinary account

## Setting up Cloudinary

One of the first things you will need to do is to create a free account on [Cloudinary](https://cloudinary.com/users/register/free). Once you have successfully created your free account, you will be redirected to the management dashboard of your account. On the dashboard page, you will find your **Account Details**, which you will need to save for later:

- **Cloud Name**
- **API Key**
- **API Secret**

Make sure to keep these details **secret** and do not share them with anyone.

![Cloudinary Dashboard](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660062158280_Screenshot+2022-08-09+at+5.16.41+PM.png)

## Installing a Strapi Instance

After creating a Cloudinary account, it is time to install your Strapi instance. Run the following command:

```bash
    yarn create strapi-app strapi-photo --quickstart 
    #OR
    npm create strapi-app strapi-photo --quickstart 
```

This command will create a folder named `strapi-photo` and install the Strapi instance to it.

![Running a Strapi Instance terminal](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660062127697_Screenshot+2022-08-09+at+5.11.16+PM.png)


After installation, Strapi will automatically run a build command at `http://localhost:1337/admin`, redirecting you immediately `http://localhost:1337/admin/auth/register-admin` because it is the first time you are starting it. You will need to register yourself as a `superuser`.

![Strapi Admin Registration Page](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1659881231873_Czs03FlA.png)

![Strapi Admin Dashboard](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1659881272878_rO0i5M-g.png)


Now, it is time to create your first collection. Click on **Content-Types Builder** and then click on **Create new collection type.**

![Strapi Content-Types-Builder](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660063147695_Screenshot+2022-08-09+at+5.38.38+PM.png)


Type **Photos** for your Display name and click on the **Continue** button to add fields.

![Strapi Collection Type Configuration](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660063415113_Screenshot+2022-08-09+at+5.42.33+PM.png)

## Adding Fields

We will be adding four fields, which are name, date, location, and img. Follow the instructions below:

- Click the Text field.
- Type **name** in the *Name* field.
- Switch to the **Advanced Settings** tab, and check the **Required** field.
- Click on **Add another Field**.
- Click the Date field.
- Type **Date** in the Name field.
- Select **date** under type drop-down
- Switch to the **Advanced Settings** tab, and check the **Required** field.
- Click on **Add another Field**.
- Click the Text field.
- Type **location** in the Name field.
- Switch to the **Advanced Settings** tab, and check the **Required** field.
- Click on **Add another Field**.
- Click the Media field.
- Type **img** in the Name field and select **Single Media** under the type checkbox,
- Switch to the **Advanced Settings** tab, and check the **Required** field.
- Select **Images** only under **Select allowed types of media.**
- Click on **Finish.**

![Strapi Content-Types-Builder Page](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660063853388_Screenshot+2022-08-09+at+5.49.44+PM.png)

- Click on **Save.** Clicking the save button will restart your server. Your Strapi instance should look like so:
![Strapi Project Structure](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1659881526656_Screenshot+2022-08-07+at+3.11.33+PM.png)

## Connecting Cloudinary

Before adding data to the Photos collection we have created, we need to connect our Cloudinary account to the Strapi instance. It would help if you stopped the server before you run the command below.

Run this command inside the root folder of your application:

```bash
    npm install @strapi/provider-upload-cloudinary
    #OR    
    yarn add @strapi/provider-upload-cloudinary
```
After the Cloudinary package has been added, you can restart your server by running.

```bash
    npm run develop
    #OR
    yarn run develop
```

Create a file named `plugins.js` inside the `config` folder, and paste the following code into it:

```js
    module.exports = ({ env }) => ({
      upload: {
        config: {
          provider: 'cloudinary',
          providerOptions: {
            cloud_name: env('CLOUDINARY_NAME'),
            api_key: env('CLOUDINARY_KEY'),
            api_secret: env('CLOUDINARY_SECRET'),
          },
          actionOptions: {
            upload: {},
            delete: {},
          },
        },
      },
    });
```

Add the following variables in the `.env` file. Fill the missing values with the corresponding values found in your [Cloudinary dashboard](https://cloudinary.com/) under **Account Details**, and make sure to restart your server.

```
    CLOUDINARY_NAME=xxxxxxxxxxxxxxxxxxxxxx
    CLOUDINARY_KEY=xxxxxxxxxxxxxxxxxx
    CLOUDINARY_SECRET=xxxxxxxxxxxxxxxx
```

Ensure your `.env` file has the necessary variables which can be random strings in place of the missing values. The content of the `.env` file should be similar to the code snippets below:

```
    HOST=0.0.0.0
    PORT=1337
    APP_KEYS=xxxxxxxxxxxxx,xxxxxxxxxxxxx
    API_TOKEN_SALT=xxxxxxxxxxxxxx
    ADMIN_JWT_SECRET=xxxxxxxxxxxx
    JWT_SECRET=xxxxxxxxxxxxxxxxxxxxx
    CLOUDINARY_NAME=xxxxxxxxxxxx
    CLOUDINARY_API_KEY=xxxxxxxxxxxxxxxxx
    CLOUDINARY_API_SECRET=xxxxxxxxxxxxxxxx
```

After adding the missing variables, the server can be restarted by running `npm run develop`

### Add Data to Photos Collection
Go back to your Strapi project at [http://localhost:1337/admin](http://localhost:1337/admin) and click on **Content Manager.** Click on **Photos**, then **Create new entry****.**

![Strapi Content Manager Page](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660063997332_Screenshot+2022-08-09+at+5.52.47+PM.png)


I have decided to use J Cole’s and Vector’s pictures for this. You can use any image you want to follow along. Make sure you **save** and **publish.**

![Strapi Photos Content Builder](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660065381188_Screenshot+2022-08-09+at+6.15.53+PM.png)


I have added four entries. 

![Strapi Photos Content Builder](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660065247388_Screenshot+2022-08-09+at+6.12.54+PM.png)


Log in to your Cloudinary to make sure the images are there.

## Set Roles & Permissions in Strapi

To make these data available for consumption by any client-side technology, we need to set some roles and permissions — who has access to what and to what extent.
Now go to **Settings→(USER & PERMISSION PLUGIN)→Roles→Public**

![Strapi Admin Dashboard- Settings](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660065726527_Screenshot+2022-08-09+at+6.21.48+PM.png)

1. Scroll down under **Permissions**.
2. In the **Application** tab, find **Photos**.
3. Click the checkboxes next to **find** and **findone**.
![Strapi Admin Dashboard- Permissions CheckBox](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660065604077_Screenshot+2022-08-09+at+6.19.40+PM.png)

1. Click **Save**.
2. Go to http://localhost:1337/api/photos?populate=* [](http://localhost:1337/api/photos?populate=*)on your browser or any API client such as Postman and make sure you have a similar response like so:
![GET Endpoint (http://localhost:1337/api/photos)](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660066313305_Screenshot+2022-08-09+at+6.31.28+PM.png)

## Installing and Setting up Next.js

Yes, we have successfully spun up the backend side of things in our application. Now let us use [Next.js](https://strapi.io/integrations/nextjs-cms) to consume its API. Exit your Strapi instance folder and run the following command to install Next.js.
```bash
    yarn create next-app next-photo
    #OR
    npm create next-app next-photo
```

This command sets up everything automatically for us (next-photo is my folder name, you can name yours differently). Move into `next-photo`:

```bash
    cd next-photo

    yarn dev
    #OR
    npm run dev
```
One of the main benefits of Next.js applications is that everything is pre-rendered or built at first load. At [http://localhost:3000](http://localhost:3000/), we should see a default Next.js instance:

![Nextjs Layout](https://paper-attachments.dropbox.com/s_77DFC755CA6BD561E547438E475058E5717C7FCA267376A2A89881FBAE4A5796_1626458531555_next.png)

Since we will be working with images from an external source, Cloudinary, we need to configure *the* `*next.config.js*` file for image optimization that NextJS provides. Make sure to upload images greater than the sizes listed below for better optimization.

```js
        const nextConfig = {
          //..
          images: {
            deviceSizes: [320, 420, 768, 1024, 1200],
            loader: "default",
            domains: ["res.cloudinary.com"],
          },
        }
        module.exports = nextConfig
```

Now, we are going to create a `components` folder and `ImageDetail.js` file within it. Paste the following code inside:

```js
            import Image from "next/image";
            import Link from "next/link";
            export default function Gallery({ thumbnailUrl, title, id }) {
              return (
                <div>
                  <Link as={`/preview/${id}`} href="/preview/[id]">
                    <a>
                      <Image width={250} height={200} src={thumbnailUrl} />
                      <div className="photoid"> {title}</div>
                    </a>
                  </Link>
                </div>
              );
            }
```

After importing `Image` and `Link` from `next`, a `gallery-component` has three props `( thumbnailUrl, title, id)` and returning a `link` that will dynamically redirect to `preview/$id` of each photo in our backend. I have decided to make the width and height 250px and 200px, respectively.

Create another folder named `preview` in the `pages` folder **and** create a file with square brackets like so `[id].js` inside the just created folder.

![Nextjs Project Structure](https://paper-attachments.dropbox.com/s_77DFC755CA6BD561E547438E475058E5717C7FCA267376A2A89881FBAE4A5796_1626898100036_id.png)

We will come back to this file, but for now, go to your `index.js` file in `pages` folder and replace the existing code with this:
```js
        import Head from "next/head";
        import { useState } from "react";
        import Gallery from "../components/ImageDetail";
        import styles from "../styles/Home.module.css";
        export default function Home({ stuff }) {
          const [photos, setPhotos] = useState(stuff);
          const [search, setSearch] = useState("");
          return (
            <div className={styles.container}>
              <Head>
                <title>Photo Gallery</title>
                <link rel="icon" href="/favicon.ico" />
              </Head>
              <main className={styles.main}>
                <div className={styles.fade}>
                  <div className={styles.gridContainer}>
                    {photos &&
                      photos.data.map((detail) => (
                        <Gallery
                          key={detail.id}
                          thumbnailUrl={detail.attributes.img.data.attributes.formats.thumbnail.url}
                          title={detail.attributes.name}
                          id={detail.id}
                        />
                      ))}
                  </div>
                </div>
              </main>
            </div>
          );
        }
        export async function getStaticProps() {
          const results = await fetch("http://localhost:1337/api/photos?populate=*");
          const stuff = await results.json();
          return {
            props: { stuff },
          };
        }
```
We imported and used `Gallery` from the `ImageDetail.js` in our `components` folder. We mapped through every instance of photos states we created. Line `32` is essential here because it uses a Next.js, `getStaticProps`, which fetches data at build time from our Strapi instance at `http://localhost:1337/api/photos`. Your application should look like this:

![Frontend Implementation](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660066766163_Screenshot+2022-08-09+at+6.39.09+PM.png)


### Responsiveness
Let us make everything responsive with the following steps.
- Copy and replace the following css code from [here](https://github.com/bigpreshy/solid-journey/blob/main/styles/Home.module.css) to `Home.module.css` in the `styles` folder.
- Copy and replace the following css code from [here](https://github.com/bigpreshy/solid-journey/blob/main/styles/globals.css) to `global.css` in the `styles` folder.

![Styling Components](https://paper-attachments.dropbox.com/s_77DFC755CA6BD561E547438E475058E5717C7FCA267376A2A89881FBAE4A5796_1626907260448_css.png)


Your application should now look like this:

![Responsive Frontend View](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660066937029_Screenshot+2022-08-09+at+6.41.42+PM.png)

## Adding Search Functionality

We have gotten the home page up and running. It'd be nice to have a search input field where users can find a specific image by its name. This will be most useful when the photos get populated.
In your `index.js` file add the following code immediately after the opening of the `<main>` tag:
```js
            <input
                  onChange={(e) => setSearch(e.target.value)}
                  className={styles.searchInput}
                  type="text"
                  placeholder="Search for an image"
                ></input>
                <button
                  className="button"
                  disabled={search === ""}
                  onClick={async () => {
                    const results = await fetch(
                      `http://localhost:1337/api/photos?populate=*&filters\[name\][$eq]=${search}`
                    );
                    const details = await results.json();
                    setPhotos(await details);
                  }}
                >
                  Find
                </button>
```

Line `1 to 6` takes care of the input. It targets the value in the input field. Pay attention to what is being fetched at Line `12`. It uses filtering techniques. You can [read more about it here](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest/filtering-locale-publication.html#filtering). Make sure you had set a search state. Your final `index.js` file should look like this:

```js
        import Head from "next/head";
        import { useState } from "react";
        import Gallery from "../components/ImageDetail";
        import styles from "../styles/Home.module.css";
        export default function Home({ stuff }) {
          const [photos, setPhotos] = useState(stuff);
          const [search, setSearch] = useState("");
          return (
            <div className={styles.container}>
              <Head>
                <title>Photo Gallery</title>
                <link rel="icon" href="/favicon.ico" />
              </Head>
              <main className={styles.main}>
                <input
                  onChange={(e) => setSearch(e.target.value)}
                  className={styles.searchInput}
                  type="text"
                  placeholder="Search for an image"
                ></input>
                <button
                  className="button"
                  disabled={search === ""}
                  onClick={async () => {
                    const results = await fetch(
                      `http://localhost:1337/api/photos?populate=*&filters\[name\][$eq]=${search}`
                    );
                    const details = await results.json();
                    setPhotos(await details);
                  }}
                >
                  Find
                </button>
                <div className={styles.fade}>
                  <div className={styles.gridContainer}>
                    {photos &&
                      photos.data.map((detail) => (
                        <Gallery
                          key={detail.id}
                          thumbnailUrl={detail.attributes.img.data.attributes.formats.thumbnail.url}
                          title={detail.attributes.name}
                          id={detail.id}
                        />
                      ))}
                  </div>
                </div>
              </main>
            </div>
          );
        }
        export async function getStaticProps() {
          const results = await fetch("http://localhost:1337/api/photos?populate=*");
          const stuff = await results.json();
          return {
            props: { stuff },
          };
        }
```
Your application should look like so with the search input and **Find** button:

![Frontend search functionality](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660066998036_Screenshot+2022-08-09+at+6.42.56+PM.png)

When you do a **Search** and hit **Find**, this is how it should look:

![Result from Search functionality](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660081740313_Screenshot+2022-08-09+at+6.44.42+PM.png)


Now, it is time to take care of what happens when a photo is clicked. Remember that our *Gallery* component in `ImageDetail.js` inside the `component` folder has a Link. Clicking on any photos right now will produce this error page:

![Error viewing single photo](https://paper-attachments.dropbox.com/s_77DFC755CA6BD561E547438E475058E5717C7FCA267376A2A89881FBAE4A5796_1626937753715_error.png)

This is because nothing has been done inside the `[id].js` we created inside the `preview` folder. Let us fix this. To fix the error, paste the following code inside `[id].js`.

```js
        import { useRouter } from "next/router";
        import Image from "next/image";
        import Link from "next/link";
        export default function photo({ photo, location, name, date }) {
            const router = useRouter();
            if (!router.isFallback && !photo) {
                return <ErrorPage statusCode={404} />;
            }
            return (
                <div>
                    <div className="Imagecontainer">
                        <Link className="homeButton" href="/">
                            <a className="homeButton">
                                <button className="button"> Home </button>
                            </a>
                        </Link>
                    </div>
                    <div className="Imagecontainer">
                        {router.isFallback ? (
                            <div>Loading…</div>
                        ) : (
                            <>
                                <Image width={960} priority height={540} src={photo} />
                            </>
                        )}
                    </div>
                    <div className="Imagecontainer">Name : {name}</div>
                    <div className="Imagecontainer">Location {location}</div>
                    <div className="Imagecontainer">Date: {date}</div>
                    <div className="Imagecontainer">
                        <Link className="homeButton" href="/">
                            <a className="homeButton">
                                <button className="button"> Back </button>
                            </a>
                        </Link>
                    </div>
                </div>
            );
        }
        export async function getStaticProps({ params }) {
            const photoid = params.id;
            const results = await fetch(`http://localhost:1337/api/photos/${photoid}?populate=*`);
            const previews = await results.json();
            const photo = await previews.data.attributes.img.data.attributes.url;
            const name = await previews.data.attributes.name;
            const location = previews.data.attributes.location;
            const date = await previews.data.attributes.createdAt.toString();
            return {
                props: { photo, name, location, date },
            };
        }
        export async function getStaticPaths() {
            const results = await fetch("http://localhost:1337/api/photos?populate=*");
            const previews = await results.json();
            return {
                paths:
                    previews?.data.map((pic) => ({
                        params: { id: pic.id.toString() },
                    })) || [],
                fallback: true,
            };
        }
```

I will explain what most parts of this code do.

- The `getStaticPaths` in from Line `52` is a Next.js primary data-fetching method required because of our application's dynamic routes. Read more about it [static generation](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation).
- The `getStaticProps` will fetch the `params.id` defined in `getStaticPaths`. Since that is available, we then fetch each id dynamically it JSON in Line `43` before accessing each of the things we need.
- Line `27 to 29` displays all other fields (location, name, date) right below the image component showing each image detail in 960px x 540px. Note that we have already defined them as props in Line 4, our photo component.

If you did everything right, you should have yourself something like this yourself when you click any photo.

![Get photo by ID](https://paper-attachments.dropbox.com/s_483DD79B55C9780E3FF48276ED1B203AA5514E6A988BBAE26F187B217F11AF82_1660082079119_Screenshot+2022-08-09+at+10.54.15+PM.png)


## Conclusion

We set up and connected our Cloudinary account to the [Strapi](https://strapi.io/) instance. In addition, we played around Strapi and its permissions and roles, thereby creating our collection to suit what we have in mind.
We talked about [Next.js](https://strapi.io/integrations/nextjs-cms) and some of its out-of-the-box methods like `getStaticProps` and `getStaticPaths`. Finally, we were able to put all these together to build our photo gallery app.

The repository for the [frontend implementation](https://github.com/Marienoir/Photo-Gallery-Strapi-Nextjs-and-Cloudinary/tree/main/next-photo) and [backend implementation](https://github.com/Marienoir/Photo-Gallery-Strapi-Nextjs-and-Cloudinary/tree/main/strapi-photo) can be found on Github.