• AI CODING CLUB
  • Posts
  • Adapting a NEXTjs boilerplate with Codestral, the brand new model by Mistral

Adapting a NEXTjs boilerplate with Codestral, the brand new model by Mistral

How have you been since my last newsletter?

I’ve been coding with my dear AI assistants, adding a new member to the family!

As you may know, I’m the solo founder of the AI Jingle Maker (which enables anyone to create DJ Drops, Radio Jingles, Podcast Intros and Audio Promos).

I recently launched a managed service offering sung jingles to my users.

It’s truly mind blowing what you can achieve with Gen AI these days when you know how to prompt the model.

🎶I invite you to listen to some demos I shared on Soundcloud.

As I’ve been creating loads of radio-related AI images with MidJourney, Leonardo and Dreamstudio (Stable Diffusion), I wanted to share them with my community in a nice gallery format, enabling my customers to download those creatives, free of charge.

I recently graduated from Vanilla JS to NEXTjs so I searched on Google for a nice NEXTjs boilerplate of an image gallery.

It happens that Vercel (the company behind NEXTjs) open sourced an application they developed in 2022 to share the photos of their first in person conference in San Francisco.

Demo of NEXTjs image gallery boilerplate

I thought this would be a great starting point for my little project!

The first step was to initialize the app in Visual Studio.

I already had NODE JS installed, so I only had to type the following command in my terminal.

npx create-next-app --example with-cloudinary nextjs-image-gallery

As you can see, this app is using Cloudinary for image storage.

It’s a great solution but their free tier isn’t that generous in terms of viewing bandwidth.

To evaluate my runway, I asked the following question to ChatGPT.

If my monthly free allocation in terms of bandwidth is 25GB, how many visitors can I have on my single page app which displays 100 images of 2Mb? 

The answer was

With a monthly free allocation of 25GB and each visit consuming 200MB, you can have 128 visitors on your single page app displaying 100 images of 2MB each.

Since I have thousands of contacts in my AI Jingle Maker mailing list, I was afraid I would very quickly run out of free bandwidth.

FROM CLOUDINARY TO CLOUDFLARE R2

Being a Cloudflare user, I had noticed in my dashboard that they were offering a new storage service, called R2, compatible with Amazon S3.

I was intrigued and had a closer look at their pricing.

With Cloudflare R2, I could definitely accept more visitors viewing & downloading 100 images than with Cloudinary’s free plan.

So I decided to repurpose the boilerplate to use Cloudflare instead of Cloudinary.

Despite the fact that both services start with “Cloud-”, it wasn’t a walk in the park…

There were quite a few places which required refactoring, not only the main index.tsx page.

USING CODESTRAL, THE NEW CODING MODEL BY MISTRAL

Fortunately, Mistral had just released their Codestral model, which is MUCH faster than GPT4 (even quicker than ChatGPT 4o in most cases) and pretty reliable in terms of output. It can also ingest long code sections.

I first asked Codestral to refactor the content of index.tsx to switch from Cloudinary to Cloudflare R2.

In the original code, this is where the data from Cloudinary is processed to be displayed to the visitors.

export async function getStaticProps() {
  const results = await cloudinary.v2.search
    .expression(`folder:${process.env.CLOUDINARY_FOLDER}/*`)
    .sort_by('public_id', 'desc')
    .max_results(400)
    .execute()
  let reducedResults: ImageProps[] = []

  let i = 0
  for (let result of results.resources) {
    reducedResults.push({
      id: i,
      height: result.height,
      width: result.width,
      public_id: result.public_id,
      format: result.format,
    })
    i++
  }

  const blurImagePromises = results.resources.map((image: ImageProps) => {
    return getBase64ImageUrl(image)
  })
  const imagesWithBlurDataUrls = await Promise.all(blurImagePromises)

  for (let i = 0; i < reducedResults.length; i++) {
    reducedResults[i].blurDataUrl = imagesWithBlurDataUrls[i]
  }

  return {
    props: {
      images: reducedResults,
    },
  }
}

The code iterates through the elements of a specific Cloudinary folder.

The logic is different with Cloudflare R2. Here we iterate through the files in a bucket, after initializing a client, just as we do on S3 (note: you should definitely use the aws-sdk/client-s3 package).

import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";

export async function getStaticProps() {
  const S3 = new S3Client({
    region: "auto",
    endpoint: `https://${process.env.NEXT_PUBLIC_CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
    credentials: {
      accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID,
      secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY,
    },
  });

  try {
    const data = await S3.send(
      new ListObjectsV2Command({ Bucket: process.env.CLOUDFLARE_R2_BUCKET_NAME })
    );

    if (!data.Contents) {
      console.error('No objects found in bucket');
      return {
        props: {
          images: [],
        },
      };
    }

    const images = data.Contents.map((image, index) => {
      const imageName = image.Key;
      const imageUrl = `https://radioimages.aijinglemaker.com/${imageName}`;
      return {
        id: index,
        imageName,
        imageUrl,
      };
    });

    return {
      props: {
        images,
      },
    };
  } catch (error) {
    console.error('Error fetching images from Cloudflare R2:', error);
    return {
      props: {
        images: [],
      },
    };
  }
}

Notice the process.env mentions. That’s how we retrieve environment variables in the code. In this specific case, the bucket has been made public (even if you need to iterate through the elements to display them).

👉This is how you should create the ACCESS_KEY_ID and SECRET_ACCESS_KEY for Cloudflare R2 https://developers.cloudflare.com/r2/api/s3/tokens/

But that was only one part of the equation. I also needed to adapt the logic of the Shared Modal (SharedModal.tsx in Components).

On Cloudinary the logic is based on the currentImage.public_id and format of the image.

 <a href={`https://res.cloudinary.com/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/image/upload/${currentImage.public_id}.${currentImage.format}`}
className="rounded-full bg-black/50 p-2 text-white/75 backdrop-blur-lg transition hover:bg-black/75 hover:text-white"
target="_blank"
title="Open fullsize version"
rel="noreferrer">
<ArrowTopRightOnSquareIcon className="h-5 w-5" />
</a>

On Cloudflare, it’s based on currentImage.imageName.

<a href={`https://radioimages.aijinglemaker.com/${currentImage.imageName}`}
className="rounded-full bg-black/50 p-2 text-white/75 backdrop-blur-lg transition hover:bg-black/75 hover:text-white"
target="_blank"
title="Open fullsize version"
rel="noreferrer"
>
<ArrowTopRightOnSquareIcon className="h-5 w-5" />
</a>

It took me a while with Codestral to figure out the exact approach.

I had also to add some code to next.config.js to handle a recurring SSL Handshake bug.

const https = require('https');

if (!https.globalAgent.options) {
  https.globalAgent.options = {};
}
https.globalAgent.options.secureProtocol = 'TLSv1_2_method';

COSMETIC UI MODIFICATIONS

I wanted to replace the elements in the intro box of the single page app.

Vercel used a SVG component (which you can find in components/Icons) called Bridge.tsx for the SF Bridge illustration, which you can (barely) notice in the background.

I ended up using a SVG file stored in my assets folder (assets/microphone.svg). Using it required a bit of CSS tweaking.

import Microphone from '/assets/microphone.svg'; 

//

<span className="flex max-h-full max-w-full items-center justify-center z-0">
<Image src={Microphone} alt="Microphone" /> 
</span>

I also added a SEARCH FEATURE in the intro box, which filters the images by name.

Here’s the code for the Search Field.

const [searchTerm, setSearchTerm] = useState(""); 

<input
  className="text-base sm:text-lg pointer z-10 mt-2 rounded-lg border border-white bg-white px-3 py-2 font-semibold text-black transition hover:bg-white/10 hover:text-white md:mt-4"
  type="text"
  placeholder="Search (e.g. microphone)..."
  value={searchTerm}
  onChange={(e) => setSearchTerm(e.target.value)}
/>

And the way it is used to filter the results mapped from the data.

{images
  .filter(({ imageName }) => imageName.toLowerCase().includes(searchTerm.toLowerCase()))
  .map(({ id, imageName, imageUrl }) =>  (

<Link
    key={id}
    href={`/?photoId=${id}`}
    as={`/p/${id}`}
    ref={id === Number(lastViewedPhoto) ? lastViewedPhotoRef : null}
    shallow
    className="after:content group relative mb-5 block w-full cursor-zoom-in after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:shadow-highlight"
  >
    <Image
      alt="Radio Images Photo"
      className="transform rounded-lg brightness-90 transition will-change-auto group-hover:brightness-110"
      style={{ transform: "translate3d(0, 0, 0)" }}
      src={imageUrl}
      width={800}
      height={800}
      sizes="(max-width: 800px) 100vw,
            (max-width: 1200px) 50vw,
            (max-width: 1600px) 33vw,
            25vw"
    />
  </Link>
))}

I changed the favicon and og-image in the public folder, which are declared, as other <HEAD> elements in _document.tsx in the pages folder.

That’s also where I adapted all the meta data.

Just to give you an idea, it took me roughly 4 hours from start to finish to refactor the whole code and deploy the app on Vercel (images on Cloudflare R2).

You can check out the result on https://freeimages.aijinglemaker.com/

If you’d like to have a closer look at the full code, let me know, I’ll be happy to show you around.

See you in the next edition of my newsletter!

Frédérick

If you’d like to get a private introduction to the art of AI-assisted coding and more broadly a detailed overview of today’s Gen AI capabilities, I’m offering one-on-one 2-hour mentoring sessions “How To Talk To An AI Agent”.

Here’s the link for bookings.

Sessions are tailored to your specific business needs.

I can also assist you in the development of your own micro SaaS project.