Automatic Poster Generator With Next.js & Faunadb

Eugene Musebe

As an event organizer, one of the most important tools to marketing your planned event is a poster displaying all the relevant information about the event to your target audience. In this article, we'll create an events poster generator to streamline the process and save time. We'll use Cloudinary and Next.js to create event posters automatically and store the manipulated image URLs in the Faunadb database as illustrated in the codesandbox below:

PROJECT SETUP

We'll need Node.js, and its package manager, npm, for this project. To check if we have them installed on our system, run the following command:

1node -v && npm -v

To create a Next.js app, open your terminal, cd into the directory you’d like to create the app in, and run the following command:

1npx create-next-app poster-generator

You now have a new directory called poster-generator. Let’s cd into it and run the following command to spin it up :

1npm run dev

CLOUDINARY SETUP

To upload and fetch images to and from Cloudinary, we need to first create an account. Cloudinary offers a free tier with enough resources to get you started with media manipulation and optimization.

After creating an account you will need to :

  • Obtain your API KEY and SECRET from the dashboard.

  • Under the Media Library tab, create a folder where all the posters will be stored and give it a name of your choice.

FAUNA SETUP

Fauna is a cloud-based transactional serverless database that makes that data available via a data API. This makes it ideal for use in a Jamstack application.

There are multiple ways to get started with fauna databases. You can use the web-based dashboard to create and manage new databases. However, you can also do most actions via the Fauna Shell. For this article, we shall manage the database through the shell. This can only be accomplished if you have created a Fauna account.

If you signed up using third-party authentication like Netlify or GitHub, you need to create the database via the web dashboard and get a security key from the security tab of the database.

Remember to store this key as we shall use it in the application to access the database.

The next step after creating an account is to login via the shell

At this point, one can create a database that will be used to store our transformed images. The database name shall be poster_generator :

1fauna create-database poster_generator

Once the database is created the next step is to create a collection where all the transformed image URLs will be stored. The name of the collection will be transformations:

1CreateCollection({ name: "transformations" })

Finally, We need to create an index for this collection. The index will make it easier to locate a document.

1CreateIndex({
2 name: "transformed_images",
3 source: Collection("transformations"),
4 terms: [{ field: ["data", "url"]]
5})

Our database is now ready to be used by the Next.js application after all of the above has been completed.

Additional Packages Installation

To interact with cloudinary and faunadb within the application, run the following command to install the cloudinary and faunadb package managers :

1npm i --save cloudinary faunadb

ENVIRONMENT VARIABLES

Create a.env.local file in your root project structure. Then go to your Cloudinary and Fauna dashboards and fill in the values for each of the following keys :

1NEXT_PUBLIC_CLOUDINARY_CLOUDNAME=
2NEXT_PUBLIC_CLOUDINARY_API_KEY=
3NEXT_PUBLIC_CLOUDINARY_API_SECRET=
4NEXT_PUBLIC_FAUNADB_SECRET_KEY=

UPLOAD COMPONENT

Create a Components folder in the root project structure and add a Uploads.js file to it. This is where all the project components will be stored.

To the Uploads file create a functional component and import useState to it. This will help us listen to any change that happens within the component and update its state

Then declare the initial value of the state as selectedFile and setSelectedFile file function that will be used to update the state as shown below :

1const [selectedFile, setSelectedFile] = useState();

The next step is to create a function that will handle any changes to the file and update its state:

1const handleFileChange = (e) => {
2 const file = e.target.files[0];
3 previewFile(file);
4 setSelectedFile(file);
5 setFileInputState(e.target.value);
6 };

Once the state of the component is updated with the correct data the next step is to create a function that will submit the data to Cloudinary.

1const handleSubmit = async (e) => {
2 e.preventDefault();
3 if (!selectedFile) return;
4 const reader = new FileReader();
5 reader.readAsDataURL(selectedFile);
6 reader.onloadend = () => {
7 uploadImage(reader.result);
8 };
9 reader.onerror = () => {
10 setErrMsg('something went wrong!');
11 };
12 };

Then create a helper function that will make a post request and return a response based on the status of the upload when used in the handlesubmit function as illustrated below :

1const uploadImage = async (base64EncodedImage) => {
2 try {
3 await fetch('/api/posters/upload', {
4 method: 'POST',
5 body: JSON.stringify({ data: base64EncodedImage }),
6 headers: { 'Content-Type': 'application/json' },
7 });
8 setFileInputState('');
9 imageUploaded();
10 setSuccessMsg('Image uploaded successfully');
11 } catch (err) {
12 console.error(err);
13 setErrMsg('Something went wrong!');
14 }
15 };

To complete the upload component, we'll need to create an upload form to select and submit files:

1<form onSubmit={handleSubmit}>
2 <div className={styles.file}>
3 <input type='file' onChange={handleFileChange} />
4 </div>
5 <input type='submit' value='Upload' className='btn' />
6</form>

UPLOAD API

To begin uploading and retrieving images from Cloudinary then storing the manipulated transformations to faunadb, create a posters folder inside the pages api directory and add a upload.js file to the directory.

Then create cloudinary and faunadb instances and imports the file as displayed below:

1import cloudinary from 'cloudinary';
2const faunadb = require('faunadb');
3
4const secret = process.env.NEXT_PUBLIC_FAUNADB_SECRET_KEY;
5const q = faunadb.query;
6const client = new faunadb.Client({ secret });
7
8cloudinary.config({
9 cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUDNAME,
10 api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,
11 api_secret: process.env.NEXT_PUBLIC_CLOUDINARY_API_SECRET,
12});

The next step is getting the submitted file from the triggered request then uploading it to cloudinary for storage purposes. By using the cloudinary uploader.upload function, one can tag and assign other variables to the image.

1export default async (req, res) => {
2 if (req.method === 'POST') {
3 try {
4 const fileStr = req.body.data;
5 const public_id = `${Date.now()}`;
6 const uploadResponse = await cloudinary.v2.uploader.upload(fileStr, {
7 tags: 'guest_speaker',
8 resource_type: 'auto',
9 public_id,
10 });
11 } catch (error) {
12 res.status(500).json({ error});
13 }
14 }
15};

IMAGE MANIPULATION

Once the image has been uploaded. We need to perform transformations to it and add it to our events speaker deck(template) which will act as the underlay for this project. You can download the template from this link : Download

The transformation below is going to transform the uploaded image into a circular shape and append the speakers' name below it then place it on top of the underlay image which acts as an events template.

1const cloudinaryTransforms = {
2 transformation: [
3 {
4 overlay: public_id,
5 width: 250,
6 height: 250,
7 radius: 250,
8 x: -450,
9 y: -57,
10 },
11 {
12 overlay: {
13 text: 'Speakers Name',
14 font_family: 'Arial',
15 font_size: 24,
16 },
17 x: -450,
18 y: 100,
19 effect: 'colorize',
20 color: '#fff',
21 },
22 ],
23 };

The last piece of the puzzle is to utilize the transformation above on the image uploaded and append it to the underlay image to get the final transformed image url :

1const transformedImg = await cloudinary.url(
2 'events_underlay.png',
3 cloudinaryTransforms
4 );

MANIPULATED IMAGE STORAGE

Once the image has been stored and transformed, cloudinary will return a url that contains the final transformed image that needs to be stored to Faunadb.

In order save a the transformed url collection to faunadb we shall leverage on the following query:

1client
2 .query(
3 q.Create(q.Collection('transformations'), {
4 data: {
5 url: transformedImg,
6 },
7 })
8 )
9 .then((ret) => console.log(ret));

FETCH IMAGES FROM FAUNADB

To fetch data from Fauna we need to create an API endpoint inside the pages folder. Inside the api directory, create a folder and name it getTransformations, then create an index.js file. This is where all the fauna query codebase will be written.

The next step is to import the faunadb library we installed.

1const faunadb = require('faunadb');

Then create an instance of the query object from the Fauna Javascript driver :

1const secret = process.env.NEXT_PUBLIC_FAUNADB_SECRET_KEY;
2const q = faunadb.query;
3const client = new faunadb.Client({ secret });

With the above set, We are ready to start querying the uploaded URLs from Faunadb.

Reading data from Fauna is relatively simple in terms of Fauna Query Language. We'll use the Map() function to iterate over the data returned from the database. The Paginate function will be used to return data in small chunks while the Match function will find the search term in the requested index as illustrated below.

1module.exports = async (req, res) => {
2 try {
3 const dbs = await client.query(
4 q.Map(
5 q.Paginate(
6 q.Match(
7 q.Index('transformations')
8 )
9 ),
10 (ref) => q.Get(ref)
11 )
12 );
13 res.status(200).json(dbs.data);
14 } catch (e) {
15 res.status(500).json({ error: e.message });
16 }
17};

Image Display Component

At this point, we have transformed and stored all the images to faunadb. The next step is to create a component that will hold each image on the main page. Inside the components, folder create a PosterItem file and add the following to it.

1import Image from 'next/image';
2export default function EventItem({ poster }) {
3 return (
4 <div className={styles.event}>
5 <div className={styles.img}>
6 <Image
7 src={poster.data.url ? poster.data.url : '/images/event-default.png'}
8 width={900}
9 height={500}
10 />
11 </div>
12 </div>
13 );
14}

On the component, we use the Next.js image component to optimize all the transformed images.

To display all the images. Inside the pages folder on the Index.js file create a functional component.

At the bottom of the file, we shall fetch data from the database using Next's data pre-rendering technique getStaticProps which will enable us to fetch data from the database at build time.

1export async function getStaticProps() {
2 const res = await fetch(`${API_URL}/api/getTransformations`);
3 const posters = await res.json();
4 return {
5 props: { posters: posters.slice(0, 10) },
6 revalidate: 1,
7 };
8}

To ensure we fetch all the latest data we used the incremental static regeneration feature to revalidate the data after every 1 minute.

We pass the data to the component as props to display all of the images in our return function. This is accomplished by:

1export default function HomePage({ posters }) {
2 return (
3 <div>
4 <Layout>
5 <h1>Upcoming Events Posters</h1>
6
7 {posters.length === 0 && <h3>No Event Posters Created</h3>}
8
9 {posters.map((poster) => (
10 <EventItem key={poster.ts} poster={poster} />
11 ))}
12 {posters.length > 0 && (
13 <Link href='/posters'>
14 <a className='btn-secondary'>View All Event Posters</a>
15 </Link>
16 )}
17 </Layout>
18 </div>
19 );
20}

With all the above done. You should be able to upload, transform and display transformed images as shown in the codesandbox above :

Conclusion

In this article, we learned how to leverage Next.js, Faunadb, and Cloudinary's massive capabilities such as storage and transformations to create an event poster. With the same capabilities, one can create way more powerful applications to serve their users.

Learn More

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.