Generate Resumes with Cloudinary & NextJS

Ekene Eze

What’s more popular than landing your next tech job? Nothing. And even then, how are you sure you’re doing the right thing? Crafting excellent resumes for your different job interests can be a daunting task without some extra help or automation. In this post, we’ll walk through the process of generating a stellar resume on-demand (PDF) with Cloudinary by supplying only the necessary details via a Next.js form.

Cloudinary offers an end-to-end solution for all our image and video needs, including the upload, storage, administration, transformation, and optimized delivery.

Next.js is a React-based frontend development framework that supports server-side rendering and static site generation.

Prerequisites

This post requires the following:

  • Experience with JavaScript and react.js
  • Installation of Node.js
  • A Cloudinary account. Signup is free!

Getting started

We start by downloading this Canva resume template, and uploading the PDF file to Cloudinary.

Log in to Cloudinary and follow the steps below to upload the file.

  1. Click on "Media Library" from the dashboard.
  1. Click on the Upload button.

  1. In the dialog box, navigate to "My Files," drag and drop the PDF file or browse to its directory to upload them.

After successful upload we should note the file "Public-Id."; we'll be using it later.

Setting up Next.js application

Run the following command in the terminal to create a new Next.js application:

1npx create-next-app resume-generator

The above command creates a starter next.js application in the resume-generator folder.

Next, we'll navigate into the project directory and start the application with the following commands:

1cd resume-generator # to navigate into the project directory
2npm run dev # to run the dev server

Next.js will start a live development server at http://localhost:3000.

Building the Resume Generator

We'll start by installing the @cloudinary/url-gen package with the following command:

1npm install @cloudinary/url-gen #to access cloudinary SDK in react environment

Next, in the root directory of our project, let’s create a components folder and Form.js file with the following snippets:

1//components/Form.js
2 function Form({ userDetails, setUserDetails, handleSubmit }) {
3 const {
4 firstname,
5 lastname,
6 email,
7 mobile,
8 website,
9 course,
10 role,
11 company,
12 position,
13 school,
14 country,
15 state,
16 interest_1,
17 interest_2,
18 interest_3,
19 } = userDetails;
20
21 const handleChange = (e) => {
22 setUserDetails({ ...userDetails, [e.target.name]: e.target.value });
23 };
24
25 return (
26 //form inputs & Submit button here
27 );
28 }
29 export default Form;

Here:

  • We destructured userDetails, setUserDetails, and handleSubmit props that we'll pass to this component later in the index.js file.
  • Destructured user information from the userDetails and created the handleChange() function using the setUserDetails prop; this function will update the user information with the form input values.

In the return() function of the Form.js component, we:

  • Created the form to collect the user information and created a Submit button to submit the information.
  • Passed the handleChange() function and the handleSubmit() function to the form input and the Submit button, respectively

The complete snippets of the Form.js component are in this GitHub Gist.

Next, let's clean up the index.js file and update it with the following snippets:

1//pages/index.js
2 import Head from "next/head";
3 import { useState } from "react";
4 import styles from "../styles/Home.module.css";
5 import Form from "../components/Form";
6
7 const initialState = {
8 email: "",
9 phone: "",
10 role: "",
11 school: "",
12 // init more user details (website, course, etc)
13 };
14 export default function Home() {
15 const [userDetails, setUserDetails] = useState(initialState);
16 const [showPDF, setShowPDF] = useState(false);
17
18 const handleSubmit = async (e) => {
19 e.preventDefault();
20 setShowPDF(true);
21 };
22
23 return (
24 // return statement here
25 );
26 }

In the above snippets, we:

  • Imported useState from “react” and Form from the components folder.
  • Created initialState object that houses the information expected from a user.
  • Created userDetails and showPDF constants and used the useState hook to set their values to initialState and false, respectively.
  • Created handleSubmit() function that will update the value of the showPDF to true.

Next, let’s set up the return() statement in our index.js file to render the form on the browser:

1import Head from "next/head";
2 import { useState } from "react";
3 import styles from "../styles/Home.module.css";
4 import Form from "../components/Form";
5
6 const initialState = {
7 // Init user details here
8 };
9 export default function Home() {
10 const [userDetails, setUserDetails] = useState(initialState);
11 const [showPDF, setShowPDF] = useState(false);
12
13 // handleSubmit() function here
14 return (
15 <div className={styles.container}>
16 <Head>
17 <title>Resume Generator</title>
18 <meta name="description" content="Generated by create next app" />
19 <link rel="icon" href="/favicon.ico" />
20 </Head>
21 <main className={styles.main}>
22 <Form
23 userDetails={userDetails}
24 setUserDetails={setUserDetails}
25 handleSubmit={handleSubmit}
26 />
27 </main>
28 </div>
29 );
30 }

Here, we gave the application a title, rendered the Form we imported from the components folder, and passed userDetails, setUserDetails, and handleSubmit as props to it.

In the browser, we should have the application working like below:

Now let’s fetch the resume template we saved earlier and use Cloudinary transformations to overlay the user information on the template.

To achieve that, create a Template.js file inside the components folder with the following snippets.

1// components/template.js
2 import { Cloudinary } from "@cloudinary/url-gen";
3 import { source } from "@cloudinary/url-gen/actions/overlay";
4 import { text } from "@cloudinary/url-gen/qualifiers/source";
5 import { TextStyle } from "@cloudinary/url-gen/qualifiers/textStyle";
6 import { Position } from "@cloudinary/url-gen/qualifiers";
7 import { compass } from "@cloudinary/url-gen/qualifiers/gravity";
8 function Home({ userDetails, setUserDetails, setShowPDF, initialState }) {
9 const {
10 firstname,
11 lastname,
12 email,
13 mobile,
14 website,
15 // init more user details (school, course, etc)
16 } = userDetails;
17
18 const cld = new Cloudinary({
19 cloud: {
20 cloudName: "CLOUD_NAME",
21 },
22 });
23
24 const template = cld
25 .image("YOUR_IMAGE_URL")
26 .addFlag("rasterize");
27
28 return ();
29 }
30 export default Home;

In the above snippets, we:

  • Imported Cloudinary and some action qualifiers from “@cloudinary/url-gen.”
  • Destructured user information from the userDetails prop we’ll pass when rendering the Template.js component in the index.js file.
  • Instantiated a new Cloudinary instance with our cloudName, saved the Cloudinary instance as template constant, and passed the file public_id we held earlier.

Next, let’s overlay the user information on the PDF file using Cloudinary transformation.

1// components/template.js
2 template.overlay(
3 source(
4 text(`${firstname} ${lastname}`, new TextStyle("Nunito", 60)).textColor(
5 "black"
6 )
7 ).position(
8 new Position().gravity(compass("north")).offsetY(200).offsetX(-400)
9 )
10 );
11 //Education Overlay
12 template
13 .overlay(
14 source(text(`${school}`, new TextStyle("Times", 30))).position(
15 new Position().gravity(compass("west")).offsetY(50).offsetX(100)
16 )
17 )
18 .overlay(
19 source(
20 text(`${course}`, new TextStyle("Times", 30)).textColor("gray")
21 ).position(
22 new Position().gravity(compass("west")).offsetY(120).offsetX(100)
23 )
24 );

Here, we overlaid the user’s firstname, lastname, and education information onto the template directly from Cloudinary. Next, lets overlay the user’s contact and profile informations:

1// Contacts Overlay
2 template
3 .overlay(
4 source(
5 text(`${mobile}`, new TextStyle("Times", 30)).textColor("gray")
6 ).position(
7 new Position().gravity(compass("west")).offsetY(410).offsetX(100)
8 )
9 )
10 .overlay(
11 source(
12 text(`${email}`, new TextStyle("Times", 30)).textColor("gray")
13 ).position(
14 new Position().gravity(compass("west")).offsetY(510).offsetX(100)
15 )
16 )
17 .overlay(
18 source(
19 text(`${website}`, new TextStyle("Times", 30)).textColor("gray")
20 ).position(
21 new Position().gravity(compass("west")).offsetY(610).offsetX(100)
22 )
23 );
24 // Profile Overlays
25 template
26 .overlay(
27 source(
28 text(
29 `I'm ${firstname} ${lastname},`,
30 new TextStyle("Times", 40)
31 ).textColor("gray")
32 ).position(
33 new Position().gravity(compass("north")).offsetY(300).offsetX(200)
34 )
35 )
36 .overlay(
37 source(
38 text(
39 `a ${role} from ${state}, ${country}.`,
40 new TextStyle("Times", 30)
41 ).textColor("gray")
42 ).position(
43 new Position().gravity(compass("north")).offsetY(350).offsetX(200)
44 )
45 );

Repeat this process and overlay the user’s work experience information and any other interests onto the resume template. With all the necessary information overlaid onto the template, our resume is complete. Next, let’s transform it into a downloadable Image that users can access via a URL:

1const pdfTemplate = template.toURL();
2 return (
3 <div>
4 <iframe src={pdfTemplate} width="500" height="778"></iframe>
5 <div className="mt-3">
6 <button
7 onClick={() => {
8 setUserDetails(initialState);
9 setShowPDF(false);
10 }}
11 className="btn btn-primary"
12 >
13 Generate New Resume
14 </button>
15 </div>
16 </div>
17 );

Here, we:

  • Transformed the template to a URL with the toURL() Cloudinary function and displayed the template in return() function.
  • Created a Generate New Resume button that clears the form so a user can generate a new resume.

We can find the complete snippets of the Template.js in this Github Gist.

Next, let's import and render the Template.js component inside the index.js page and pass the userDetails to it. Update the index.js like in the below:

1// Other imports here
2 import Template from "../components/Template";
3 // Init state here
4
5 export default function Home() {
6 const [userDetails, setUserDetails] = useState(initialState);
7 const [showPDF, setShowPDF] = useState(false);
8
9 // handleSubmit function here
10
11 return (
12 <div className={styles.container}>
13 // Head tag here
14 <main className={styles.main}>
15 // Form component here
16 {showPDF && (
17 <Template
18 userDetails={userDetails}
19 setShowPDF={setShowPDF}
20 setUserDetails={setUserDetails}
21 initialState={initialState}
22 />
23 )}
24 </main>
25 </div>
26 );
27 }

In the above snippet, we import the Template.js file from the components folder, used the showPDF boolean to render it conditionally, and then passed-in the required properties.

In the browser, when a user fills the form and clicks on the Generate Resume button, the resume gets generated with the information the user provided.

Conclusion

This post explored rich Cloudinary transformation APIs and discussed building a resume generator app that collects user information and generates a PDF resume with Next.js.

Resources

Cloudinary text overlays might be a helpful resource.

Github Repository

Ekene Eze

Director of DX at Plasmic

I work at Plasmic as the Director of Developer Experience and I love executing projects that help other software engineers. Passionate about sharing with the community, I often write, present workshops, create courses, and speak at conferences about web development concepts and best practices.