🧞‍♂️ Generator unlocked: Create memes with ChatGPT and NextJS 🚀 💥
Tags: webdev, javascript, programming, tutorial
Published: 2023-09-18 13:00:42

# TL;DR

In this tutorial, you'll learn how to build a meme generator in Next.js. We will be using:
- Trigger.dev for handling the meme's creation in the background.
- Supabase for storing and saving the memes
- Resend for sending the generated meme to the user.

![Memes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vkp2ek3p9fp8jmaysyio.gif)

<hr />

## Your background job management for NextJS

[Trigger.dev](https://trigger.dev/) is an open-source library that enables you to create and monitor long-running jobs for your app with NextJS, Remix, Astro, and so many more!

If you can spend 10 seconds giving us a star, I would be super grateful 💖
https://github.com/triggerdotdev/trigger.dev

[![GiveStar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9wsljq85zi71b0pksld7.png)](https://github.com/triggerdotdev/trigger.dev
)


<hr />

## Let's set it up 🔥

Here, I'll walk you through creating the user interface for the meme generator application.

Create a new Next.js project by running the code snippet below.

```bash
npx create-next-app memes-generator
```

First, you need to create a homepage displaying a form that enables users to enter the meme audience and topic. You can also add another field for the user's email address to receive the meme once it is ready.
Below the form, users should be able to view the recently generated memes, as shown below.


![Final](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bf0tj9suk1mld93iou1e.gif)


When the user submits the form, they will be redirected to a submission page while the application generates the meme in the background and sends it to the user's email.


![Thank you](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/82i4he751fg16isc2nqv.png)


### Home page 🏠

Before we proceed, run the code snippet below to install the [React Tag Input package](https://www.npmjs.com/package/react-tag-input). It enables us to provide multiple inputs for a value.

```bash
npm install react-tag-input
```

Copy the code snippet below into the `index.js` file.

```jsx
"use client";
import { Space_Grotesk } from "next/font/google";
import { useState } from "react";
import { WithContext as ReactTags } from "react-tag-input";
import { useRouter } from "next/navigation";
import ViewMemes from "@/components/ViewMemes";

const KeyCodes = {
  comma: 188,
  enter: 13,
};
const inter = Space_Grotesk({ subsets: ["latin"] });
const delimiters = [KeyCodes.comma, KeyCodes.enter];

export default function Home() {
  const [audience, setAudience] = useState("");
  const [email, setEmail] = useState("");
  const [memes, setMemes] = useState([]);
  const router = useRouter();
  const [topics, setTopics] = useState([
    { id: "Developers", text: "Developers" },
  ]);

  const handleDelete = (i) =>
    setTopics(topics.filter((topic, index) => index !== i));

  const handleAddition = (topic) => setTopics([...topics, topic]);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      audience,
      topics,
      email,
    });
    router.push("/submit");
  };

  return (
    <main className={`w-full min-h-screen ${inter.className}`}>
      <header className="bg-[#F8F0E5] min-h-[95vh] flex items-center justify-center flex-col">
        <h2 className="text-4xl font-bold mb-2 text-[#0F2C59]">Meme Magic</h2>
        <h3 className="text-lg opacity-60 mb-8">
          Creating memes with a touch of magic
        </h3>
        <form
          className="flex flex-col md:w-[70%] w-[95%]"
          onSubmit={handleSubmit}
        >
          <label htmlFor="audience">Audience</label>
          <input
            type="text"
            name="audience"
            value={audience}
            required
            className="px-4 py-2 rounded mb-4"
            onChange={(e) => setAudience(e.target.value)}
          />
          <label htmlFor="email">Your email address</label>
          <input
            type="email"
            name="email"
            value={email}
            required
            className="px-4 py-2 rounded mb-4"
            onChange={(e) => setEmail(e.target.value)}
          />

          <ReactTags
            tags={topics}
            delimiters={delimiters}
            handleDelete={handleDelete}
            handleAddition={handleAddition}
            inputFieldPosition="top"
            autocomplete
            placeholder="Enter a topic for the meme and press enter"
          />
          <button
            type="submit"
            className="bg-[#0F2C59] text-[#fff] mt-[30px] py-4 rounded text-lg font-bold hover:bg-[#151d2b]"
          >
            GENERATE MEME
          </button>
        </form>
      </header>
      <ViewMemes memes={memes} />
    </main>
  );
}
```

The code snippet creates input fields that enable users to enter the email, audience, and topic of the meme to be generated. The `<ReactTags>` component allows us to enter multiple values as the meme topic.


![TagInput](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7bzm5ktubiku1ulgp111.gif)


Next, create a components folder containing the `ViewMemes.jsx` file, where all the recently generated memes are displayed.

```jsx
import React from "react";
import Image from "next/image";

const ViewMemes = ({ memes }) => {
  return (
    <section className="h-[100vh] px-4 py-10 flex items-center flex-col">
      <h2 className="text-3xl font-bold mb-8">Recent Memes</h2>
      <div className="w-full flex flex-wrap md:space-x-4">
        {memes.map((meme) => (
          <div
            className="md:w-[30%] w-full m-2 flex flex-col items-center"
            key={meme.id}
          >
            <Image
              src={`${meme.meme_url}`}
              alt={meme.name}
              className="hover:scale-105 rounded"
              width={400}
              height={400}
            />
          </div>
        ))}
      </div>
    </section>
  );
};

export default ViewMemes;
```

### Meme submission 🐤

Create a `submit.js` file that displays a "Thank You" message to the user after submitting the form.

```jsx
import Link from "next/link";
import React from "react";

export default function Submit() {
  return (
    <div className="w-full h-screen flex flex-col items-center justify-center">
      <h2 className="font-extrabold text-4xl mb-4 text-[#DAC0A3]">
        Thank You!
      </h2>
      <p className="mb-6">
        Your newly generated meme has been sent to your email.
      </p>
      <Link href="/">
        <p className="text-[#662549]">Go Home</p>
      </Link>
    </div>
  );
}
```

<hr />

## Generate memes in the background 🐥

In this section, I will walk you through adding Trigger.dev to a Next.js project. First, you need to visit the [Trigger.dev homepage](https://trigger.dev/) and create a new account.

Next, create an organization and project name for your jobs.


![Prod](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2p2gpwxegxlflkru4m2p.png)

Lastly, follow the steps shown to you.


![Cli](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zd0g7nmq66r302u18ko5.png)

If this is not your initial Trigger.dev project, follow the steps below.
Click `Environments & API Keys` on the sidebar menu of your project dashboard.


![DEV Api](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/osgbyha7u7t30zw1vz5q.png)

Copy your DEV server API key and run the code snippet below to install Trigger.dev. Follow the instructions carefully.

```bash
npx @trigger.dev/cli@latest init
```

Start your Next.js project.

```bash
npm run dev
```

Also, in another terminal, run the code snippet below to create a tunnel between the Trigger.dev and your Next.js project.

```bash
npx @trigger.dev/cli@latest dev
```

Finally, rename the `jobs/examples.js` file to `jobs/functions.js`. This is where all the jobs are processed.

Congratulations!🎉 You've successfully added Trigger.dev to your Next.js app.

<hr />

## How to generate meme templates and captions with OpenAI and ImgFlip

In this section, you'll learn how to fetch meme templates from [ImgFlip](https://imgflip.com/api) and generate meme captions from [OpenAI](https://platform.openai.com/docs/guides/gpt/function-calling) via Trigger.dev. 

Before we proceed, update the `index.js` file to send the user's input to the server.

```jsx
//👇🏻 runs when a user submits the form
const handleSubmit = (e) => {
  e.preventDefault();
  if (topics.length > 0) {
    let topic = "";
    topics.forEach((tag) => {
      topic += tag.text + ", ";
    });
    //👇🏻 sends the data to the backend
    postData(topic);
    router.push("/submit");
  }
};

//👇🏻 sends the data to the backend server
const postData = async (topic) => {
  try {
    const data = await fetch("/api/api", {
      method: "POST",
      body: JSON.stringify({
        audience,
        topic,
        email,
      }),
      headers: {
        "Content-Type": "application/json",
      },
    });
    const response = await data.json();
    console.log(response);
  } catch (err) {
    console.error(err);
  }
};
```

The `postData` function sends the user's input to the `/api/api` endpoint on the server when the user submits the form.

Next, create an `api.js` file within the `api` folder that accepts the user's data.

```jsx
export default function handler(req, res) {
  const { topic, audience, email } = req.body;

  if (topic && audience && email) {
    console.log({ audience, topic, email });
  }
  res.status(200).json({ message: "Successful" });
}
```

### Meme background job 👨🏻‍🔧

There are three ways of communicating via Trigger which are webhook, schedule, and event. Webhook triggers a job in real-time when specific events occur, Schedule works for repeating tasks, and Events trigger a job when you send a payload.

Here, I'll guide you through using Event triggers to execute jobs in the application.

Within the `jobs/functions.js` file, update the `client.defineJob` function as done below.

```jsx
client.defineJob({
  id: "generate-meme",
  name: "Generate Meme",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "generate.meme",
  }),
  run: async (payload, io, ctx) => {
    const { audience, topic, email } = payload;
    // This logs a message to the console and adds an entry to the run dashboard
    await io.logger.info("Meme request received!✅");
    await io.logger.info("Meme generation in progress 🤝");

    // Wrap your code in io.runTask to get automatic error handling and logging
    const selectedTemplate = await io.runTask("fetch-meme", async () => {
      const fetchAllMeme = await fetch("https://api.imgflip.com/get_memes");
      const memesData = await fetchAllMeme.json();
      const memes = memesData.data.memes;

      const randInt = Math.floor(Math.random() * 101);

      return memes[randInt];
    });

    const userPrompt = `Topics: ${topic} \n Intended Audience: ${audience} \n Template: ${selectedTemplate.name} \n`;

    const sysPrompt = `You are a meme idea generator. You will use the imgflip api to generate a meme based on an idea you suggest. Given a random template name and topics, generate a meme idea for the intended audience. Only use the template provided.`;

    await io.logger.info("✨ Yay! You've gotten a template for the meme ✨", {
      userPrompt,
      sysPrompt,
      selectedTemplate,
      email,
    });
  },
});
```

The code snippet above creates a new job that fetches a random meme template from ImgFlip, creates a user, and a system prompt for OpenAI to generate a meme caption based on the data provided by the user.

To trigger this job, update the `/api/api.js` file to send the user's data as a payload to the job.

```jsx
import { client } from "@/trigger";

export default function handler(req, res) {
  const { topic, audience, email } = req.body;
  try {
    async function fetchMeme() {
      if (topic && audience && email) {
        await client.sendEvent({
          name: "generate.meme",
          payload: {
            audience,
            topic,
            email,
          },
        });
      }
    }
    fetchMeme();
  } catch (err) {
    return res.status(400).json({ message: err });
  }
}
```

The code snippet above triggers the job via its name - `generate.meme` and sends the audience, topic, and the user's email as a payload to the job.

Once this is successful, you should see the status of the job on your Trigger.dev dashboard and monitor its activity.


![Oncesuccessful](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zgmaj9nx1x4aoziuc79k.png)

Trigger.dev also allows us to nest jobs within jobs; that is, you can trigger another job within a job. This makes it possible for you to trigger a job that will execute all the nested jobs within itself.

In the previous job, we logged the data retrieved to the console; Next, let's trigger another job within the job.

```jsx
client.defineJob({
  id: "generate-meme",
  name: "Generate Meme",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "generate.meme",
  }),
  run: async (payload, io, ctx) => {
    const { audience, topic, email } = payload;
    // This logs a message to the console and adds an entry to the run dashboard
    await io.logger.info("Meme request received!✅");
    await io.logger.info("Meme generation in progress 🤝");

    // Wrap your code in io.runTask to get automatic error handling and logging
    const selectedTemplate = await io.runTask("fetch-meme", async () => {
      const fetchAllMeme = await fetch("https://api.imgflip.com/get_memes");
      const memesData = await fetchAllMeme.json();
      const memes = memesData.data.memes;

      const randInt = Math.floor(Math.random() * 101);

      return memes[randInt];
    });

    const userPrompt = `Topics: ${topic} \n Intended Audience: ${audience} \n Template: ${selectedTemplate.name} \n`;

    const sysPrompt = `You are a meme idea generator. You will use the imgflip api to generate a meme based on an idea you suggest. Given a random template name and topics, generate a meme idea for the intended audience. Only use the template provided.`;

    // Trigger any job listening for the gpt.text event
    await io.sendEvent("generate-gpt-text", {
      name: "gpt.text",
      payload: {
        userPrompt,
        sysPrompt,
        selectedTemplate,
        email,
      },
    });
  },
});
```

The code snippet above sends the data retrieved from the job as a payload to an event - where you can generate the meme caption. Each event has a unique name, and any job that is triggered by an event with a matching name will be run. You can send events using the `io.sendEvent` method.

Now that we are selecting a template and generate prompts, we can implement the job for generating meme text by listening for the `gpt.text` event.

<hr />

### Generate the meme text with ChatGPT 🤖

Here, you'll learn how to communicate with OpenAI using the Trigger.dev OpenAI integration. We'll use function calls for generating captions for the meme templates. Before we proceed, [create an OpenAI account](https://platform.openai.com/) and generate an API Key.


![ChatGPT](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ashau6i2sxcpd0qcxuwq.png)

Click `View API key` from the dropdown to create an API Key.


![ApiKey](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bzc6e7f7avemeuuaygr.png)

Next, install the OpenAI package by running the code snippet below.

```bash
npm install @trigger.dev/openai
```

Add your OpenAI API key to the `.env.local` file.

```bash
OPENAI_API_KEY=<your_api_key>
```

Import the OpenAI package into the `jobs/functions.js` file.

```jsx
import { OpenAI } from "@trigger.dev/openai";

const openai = new OpenAI({ id: "openai", apiKey: process.env.OPENAI_API_KEY });
```

Finally, create the job that generates the meme captions via [OpenAI function call](https://platform.openai.com/docs/guides/gpt/function-calling).

```jsx
client.defineJob({
  id: "chatgpt-meme-text",
  name: "ChatGPT Meme Text",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "gpt.text",
  }),
  run: async (payload, io, ctx) => {
    const { userPrompt, sysPrompt, selectedTemplate, email } = payload;
    await io.logger.info("✨ Talking to ChatGPT 🫂");
    const messages = [
      { role: "system", content: sysPrompt },
      { role: "user", content: userPrompt },
    ];
    const functions = [
      {
        name: "generateMemeImage",
        description:
          "Generate meme via the imgflip API based on the given idea",
        parameters: {
          type: "object",
          properties: {
            text0: {
              type: "string",
              description: "The text for the top caption of the meme",
            },
            text1: {
              type: "string",
              description: "The text for the bottom caption of the meme",
            },
          },
          required: ["templateName", "text0", "text1"],
        },
      },
    ];
    const response = await openai.chat.completions.create("create-meme", {
      model: "gpt-3.5-turbo",
      messages,
      functions,
      function_call: "auto",
    });

    const responseMessage = response.choices[0];
    const texts = extractSentencesInQuotes(responseMessage.message.content);

    await io.logger.info("✨ Yay! You've gotten a text for your meme ✨", {
      texts,
    });

    await io.sendEvent("caption-save-meme", {
      name: "caption.save.meme",
      payload: {
        texts: ["Text0", "Text1"],
        selectedTemplate,
        email,
      },
    });
  },
});
```

The code snippet above generates a caption for the meme via the [OpenAI function calling feature](https://platform.openai.com/docs/guides/gpt/function-calling), extracts the caption, and passes it as a payload to another job. The payload contains the caption, selected meme template, and the user's email.

The `extractSentencesInQuotes` function retrieves the caption from the data returned by OpenAI.

```jsx
const extractSentencesInQuotes = (inputString) => {
  const sentenceRegex = /"([^"]+)"/g;
  const sentences = [];
  let match;
  while ((match = sentenceRegex.exec(inputString))) {
    sentences.push(match[1]);
  }
  return sentences;
};
```

So far, we've been able to select a meme template from [ImgFlip](https://imgflip.com/api) and generate the meme caption via [OpenAI](https://platform.openai.com/). In the upcoming sections, you'll learn how to add the caption to the meme templates.

### Set the memes title 📝

[ImgFlip API](https://imgflip.com/api) provides a `/caption_image` endpoint that accepts required parameters, such as template ID, username, password, and the captions - `text0`and `text1`. `Text0` is the Top text for the meme, and `Text1` is the Bottom text for the meme.

![Text0](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7m66iq4e1fq68gmt85jb.png)

Create an account on [ImgFlip](https://imgflip.com/api), and save your username and password into the `.env.local` file.

```bash
IMGFLIP_USERNAME=<your_username>
IMGFLIP_PW=<your_password>
```

Add a new job to the `jobs/functions.js` file that adds a caption to the meme and saves it to [Supabase](https://supabase.com).

```jsx
client.defineJob({
  id: "caption-save-meme",
  name: "Caption and Save Meme",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "caption.save.meme",
  }),
  run: async (payload, io, ctx) => {
    const { texts, selectedTemplate, email } = payload;

    await io.logger.info("Received meme template and texts 🎉");

    const formatData = new URLSearchParams({
      template_id: selectedTemplate.id,
      username: process.env.IMGFLIP_USERNAME,
      password: process.env.IMGFLIP_PW,
      text0: texts[0],
      text1: texts[1],
    });

    const captionedMeme = await io.runTask("caption-meme", async () => {
      const response = await fetch("https://api.imgflip.com/caption_image", {
        method: "POST",
        body: formatData.toString(),
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
      });
      return await response.json();
    });

    await io.logger.info("✨ Yay! Your meme has been captioned! ✨", {
      captionedMeme,
    });
  },
});
```

The code snippet above accepts the required parameters and sends them to the endpoint provided by ImgFlip. The endpoint receives the meme ID, caption text, and user's credentials in a URL-encoded format.

In the upcoming section, you'll learn how to save the captioned meme to Supabase.

<hr />

## Save the memes and display them 💾

[Supabase](https://supabase.com/) is an open-source Firebase alternative that enables you to add authentication, file storage, Postgres, and real-time database to your software applications. With Supabase, you can build secured and scalable applications in a few minutes.

In this section, I'll guide you through setting up and interacting with a Supabase backend from a Next.js app.

Visit the [Supabase homepage](https://supabase.com/) and create a new organization and project.


![CreatOrg](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jhvo2ddmd6y1vwelmjb3.png)

Next, create a new table containing the following columns, as shown below, and add some dummy data to the database table.


![Org](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/29pyu6d6i4it81s05n0r.png)


The table stores the `id`,`name`, and `meme_url` properties received from the application. The `created_at` is auto-generated and saves the data timestamp.

Run the code snippet below to install the Supabase package into Next.js.

```bash
npm install @supabase/supabase-js
```

Click `API` on the sidebar menu and copy the project's URL and API into the `.env.local` file.

```bash
NEXT_PUBLIC_SUPABASE_URL=<public_supabase_URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<supabase_anon_key>
```


![Finally](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ggav9qjnd9z6urkqtaml.png)

Finally, create a `src/supbaseClient.js` file and create the Supabase client for the application.

```jsx
import { createClient } from "@supabase/supabase-js";

const supabaseURL = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;

export const supabase = createClient(supabaseURL, supabaseAnonKey, {
  auth: { persistSession: false },
});
```

Congratulations!🎉 You've successfully added Supabase to the Next.js application.

### Fetching all the available memes 

Create an `api/all-memes` endpoint on the Next.js server that retrieves all the memes from Supabase.

```jsx
import { supabase } from "@/supabaseClient";

export default function handler(req, res) {
  const fetchMemes = async () => {
    try {
      const { data, error } = await supabase
        .from("memes")
        .select("*")
        .order("created_at", { ascending: false });
      res.status(200).json({ data });
    } catch (err) {
      res.status(400).json({ error: err });
    }
  };
  fetchMemes();
}
```

Then, send a request to the endpoint when a user visits the application's homepage.

```jsx
useEffect(() => {
  fetchMemes();
}, []);

const fetchMemes = async () => {
  try {
    const request = await fetch("/api/all-memes");
    const response = await request.json();
    setMemes(response.data);
  } catch (err) {
    console.error(err);
  }
};
```

### Saving the memes to Supabase

Import Supabase from the `@/supabaseClient` file into the `@/jobs/functions.js` file.

```jsx
import { supabase } from "@/supabaseClient";
```

Update the recently added job to save the meme to Supabase.

```jsx
client.defineJob({
  id: "caption-save-meme",
  name: "Caption and Save Meme",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "caption.save.meme",
  }),
  run: async (payload, io, ctx) => {
    const { texts, selectedTemplate, email } = payload;

    await io.logger.info("Received meme template and texts 🎉");

    const formatData = new URLSearchParams({
      template_id: selectedTemplate.id,
      username: process.env.IMGFLIP_USERNAME,
      password: process.env.IMGFLIP_PW,
      text0: texts[0],
      text1: texts[1],
    });

    const captionedMeme = await io.runTask("caption-meme", async () => {
      const response = await fetch("https://api.imgflip.com/caption_image", {
        method: "POST",
        body: formatData.toString(),
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
      });
      return await response.json();
    });

    await io.logger.info("✨ Yay! Your meme has been captioned! ✨", {
      captionedMeme,
    });

    await supabase.from("memes").insert({
      id: selectedTemplate.id,
      name: selectedTemplate.name,
      meme_url: captionedMeme.data.url,
    });

    await io.sendEvent("email-meme", {
      name: "send.meme",
      payload: {
        email,
        meme_url: `http://localhost:3000/memes/${selectedTemplate.id}`,
      },
    });

    await io.logger.info(
      "✨ Yay! Your meme has been saved to the database! ✨"
    );
  },
});
```

The code snippet above adds a caption to the meme template and saves the newly created meme to Supabase. It also triggers another job, which sends the meme page URL to the user's email. You'll learn how to do this in the upcoming section.

<hr />

## Send email on completion 🎬

[Resend](https://resend.com/) is an email API that enables you to send texts, attachments, and email templates easily. With Resend, you can build, test, and deliver transactional emails at scale.

Here, you'll learn how to send emails via Resend. Go to the [Signup page and create an account](https://resend.com/signup).

Create an `API Key` and save it into a `.env.local` file within your Next.js project.

```bash
RESEND_API_KEY=<place_your_API_key>
```

![Resize](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t5yfqbb239703v6mbxiq.png)

Install the Trigger.dev Resend integration:

```bash
npm i @trigger.dev/resend
```

Import Resend into the `@/jobs/functions.js` file as shown below.

```jsx
import { Resend } from "@trigger.dev/resend";
const resend = new Resend({ id: "resend", apiKey: process.env.RESEND_API_KEY });
```

Add a final job that receives the meme URL and user's email from the previous job and sends the newly generated meme to the user upon completion.

```jsx
client.defineJob({
  id: "send-meme",
  name: "Send Meme",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "send.meme",
  }),
  run: async (payload, io, ctx) => {
    const { meme_url, email } = payload;

    await io.logger.info("Sending meme to the user 🎉");

    await resend.sendEmail("📧", {
      from: "onboarding@resend.dev",
      to: [email],
      subject: "Your meme is ready!",
      text: `Hey there, Your meme is ready.\n Access it here: ${meme_url}`,
    });

    await io.logger.info("✨ Yay! Your meme has been emailed to the user! ✨");
  },
});
```

The user receives an email containing a link in this format: `http://localhost:3000/memes/${meme.id}`. This link redirects the user to a specific page in the application that displays the meme. We will create this page in the upcoming section.

### Displaying the newly generated meme

Create a `memes/[id].js` file that accepts the meme ID from the URL, get the meme from Supabase via its ID, and displays the meme on the page.

![ThisIsFine](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9koao6mln4zfka2jipa.png)

Copy the code snippet below into the `memes/[id].js` file.

```jsx
"use client";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import { usePathname } from "next/navigation";
import Link from "next/link";

export default function Meme() {
  const params = usePathname();
  const [meme, setMeme] = useState({});

  useEffect(() => {
    const fetchMeme = async () => {
      if (params) {
        const id = params.split("/")[2];
        const request = await fetch("/api/meme", {
          method: "POST",
          body: JSON.stringify({ id }),
          headers: {
            "Content-Type": "application/json",
          },
        });
        const response = await request.json();
        setMeme(response.data[0]);
      }
    };

    fetchMeme();
  }, [params]);

  return (
    <div className="w-full min-h-screen flex flex-col items-center justify-center">
      {meme?.meme_url && (
        <Image
          src={meme.meme_url}
          alt={meme.name}
          className="hover:scale-105 rounded"
          width={500}
          height={500}
        />
      )}

      <Link href="/" className="mt-6 text-blue-500">
        Go back home
      </Link>
    </div>
  );
}
```

The code snippet above retrieves the meme ID from the URL, sends it to the server via the `/api/meme` endpoint, and receives the meme details as a response.

Next, create the `/api/meme` endpoint.

```jsx
import { supabase } from "@/supabaseClient";

export default function handler(req, res) {
  const getMeme = async () => {
    const { id } = req.body;
    try {
      const { data, error } = await supabase
        .from("memes")
        .select("*")
        .eq("id", parseInt(id));
      res.status(200).json({ data });
    } catch (err) {
      res.status(400).json({ error: err });
    }
  };
  getMeme();
}
```

The code snippet above fetches the data whose property matches the ID from Supabase.

Congratulations! You've completed the project.

<hr />

## Conclusion

So far, you've learnt how to create jobs within your codebase with Trigger, generate texts from OpenAI, save and retrieve data from Supabase, and send emails via Resend.

The source code for this tutorial is available here:
https://github.com/triggerdotdev/blog/tree/main/memes-generator

Thank you for reading!

<hr />

If you can spend 10 seconds giving us a star, I would be super grateful 💖
https://github.com/triggerdotdev/trigger.dev

[![GiveStar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9wsljq85zi71b0pksld7.png)](https://github.com/triggerdotdev/trigger.dev
)