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.
Your background job management for NextJS
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
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.
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.
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.
Home page π
Before we proceed, run the code snippet below to install theΒ React Tag Input package. It enables us to provide multiple inputs for a value.
npm install react-tag-input
Copy the code snippet below into the index.js
file.
"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.
Next, create a components folder containing the ViewMemes.jsx
file, where all the recently generated memes are displayed.
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.
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>
);
}
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Β and create a new account.
Next, create an organization and project name for your jobs.
Lastly, follow the steps shown to you.
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.
Copy your DEV server API key and run the code snippet below to install Trigger.dev. Follow the instructions carefully.
npx @trigger.dev/cli@latest init
Start your Next.js project.
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.
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.
How to generate meme templates and captions with OpenAI and ImgFlip
In this section, you'll learn how to fetch meme templates fromΒ ImgFlipΒ and generate meme captions fromΒ OpenAIΒ via Trigger.dev.
Before we proceed, update the index.js
file to send the user's input to the server.
//ππ» 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.
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.
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.
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.
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.
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.
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Β and generate an API Key.
Click View API key
from the dropdown to create an API Key.
Next, install the OpenAI package by running the code snippet below.
npm install @trigger.dev/openai
Add your OpenAI API key to the .env.local
file.
OPENAI_API_KEY=<your_api_key>
Import the OpenAI package into the jobs/functions.js
file.
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.
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, 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.
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Β and generate the meme caption viaΒ OpenAI. In the upcoming sections, you'll learn how to add the caption to the meme templates.
Set the memes title π
ImgFlip 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.
Create an account onΒ ImgFlip, and save your username and password into the .env.local
file.
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.
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.
Save the memes and display them πΎ
SupabaseΒ 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Β and create a new organization and project.
Next, create a new table containing the following columns, as shown below, and add some dummy data to the database table.
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.
npm install @supabase/supabase-js
Click API
on the sidebar menu and copy the project's URL and API into the .env.local
file.
NEXT_PUBLIC_SUPABASE_URL=<public_supabase_URL>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<supabase_anon_key>
Finally, create a src/supbaseClient.js
file and create the Supabase client for the application.
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.
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.
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.
import { supabase } from "@/supabaseClient";
Update the recently added job to save the meme to Supabase.
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.
Send email on completion π¬
ResendΒ 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.
Create an API Key
and save it into a .env.local
file within your Next.js project.
RESEND_API_KEY=<place_your_API_key>
Install the Trigger.dev Resend integration:
npm i @trigger.dev/resend
Import Resend into the @/jobs/functions.js
file as shown below.
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.
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.
Copy the code snippet below into the memes/[id].js
file.
"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.
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.
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!
If you can spend 10 seconds giving us a star, I would be super grateful π
https://github.com/triggerdotdev/trigger.dev
Top comments (9)
Great article!
I love how Trigger.dev can take all the background jobs without using an backend.
Great tutorial with step-by-step instructions. This is bananas.
Nice article!
Super informative :)
Brilliant! I'm already churning out memes π€£
Love that π Thanks!
Please do not copy my tutorials
Copy of dev.to/wasp/build-your-own-ai-meme... ?
Cool article. I've saved it and will go through the whole tutorial this weekend.
My Monday Memes are coming πβ
djobzy.com/blog-details/engineerin...