How to get started with OneDoc in NextJS

So you recently stumbled upon OneDoc, an awesome tool for generating PDFs using modern web technologies like React, NextJS and so on. But you are finding it difficult to generate your first document. Don't worry since in this tutorial, I will show you how you can generate your first document using NextJS. I will guide you with whatever experience I have gained till now.

💡
The full source code for this tutorial is here. In case, you are looking for a ready-made app with PDF templates available, visit my app here.

After you have completed the tutorial, you will be able to reuse the code for generating different PDFs in the future or you can further improvise it accordingly since you would have understood how it works.

Lets create our NextJS app. I will be using VSCode as my code editor. You are free to choose your own.

npx create-next-app onedoc-next-starter

Next, you will be asked various prompts. Here's what I have selected:

√ Would you like to use TypeScript? ... Yes
√ Would you like to use ESLint? ...  Yes
√ Would you like to use Tailwind CSS? ... Yes
√ Would you like to use `src/` directory? ... No 
√ Would you like to use App Router? (recommended) ... Yes
√ Would you like to customize the default import alias (@/*)? ... No

Don't worry if you don't know Typescript. It's not a Typescript-heavy project.

Okay let's first dive into the frontend.

First I will first clean the globals.css file.

app\globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;

*{
  padding:0;
  margin:0;
}

Now, I will be reusing the code for a template that I had made earlier for the community templates on OneDoc's website. It's basically a CV.

app\page.tsx

import Image from "next/image";

export default function Main() {
  return (
    <div className="cv  font-[500] min-h-screen px-6 py-4 text-left">
          <div className="font-bold mb-2 text-3xl text-center">Meirul Krosh</div>
          <div className="details flex flex-col mb-4">
          <div className="flex justify-between mb-2">
              <span>(+41) 18 935 16 99 / (+33) 12 45 45 59 47 </span>
              <span>Kaferhölzstrasse 54</span>
          </div>
          <div className="flex justify-between mb-2">
              <span>marahul@student.main.com, mierat.gorde@ironlpartners.com </span>
              <span>Zürich, Switzerland</span>
          </div>
          </div>
          <div className="education flex flex-col mb-4">
              <span className="text-center text-xl p-1 border-b-2 border-black mb-2">EDUCATION</span>
              <div className="flex justify-between mb-2">
                  <span className="font-[550]">EIDGENOSSISCHE TECHNISCHE HOCHSCHULE ZURICH, ETHZ </span>
                  <span>Zurich, ZH ,Switzerland</span>
              </div>
              <div className="flex justify-between mb-2">
                  <i>Master of Science in Computer Science  </i>
                  <span>2021-</span>
              </div>
              <div className="flex justify-between mb-2">
                  <span><span className="font-bold">• Major :</span>Machine Intelligence, minor in Information Security  </span>
              </div>
              <div className="flex justify-between mb-2">
                  <span className="font-[600]">ÉCOLE POLYTECHNIQUE FÉDÉRALE DE LAUSANNE, EPFL </span>
                  <span>Lausanne, VD, Switzerland</span>
              </div>
              <div className="flex justify-between mb-2">
                  <i>Bachelor of Science in Commmunication Systems  </i>
                  <span>2017-2020</span>
              </div>
              <div className="flex justify-between mb-2">
                  <span><span className="font-bold">• Mentoring and Team Work:</span>Teaching assistant in the Computer Architecture course</span>
              </div>
          </div>
          <div className="work-exp  flex flex-col mb-4">
              <span className="text-xl text-center p-1 border-b-2 border-black mb-2">WORK EXPERIENCE</span>
              <div className="work-exp1 flex flex-col mb-4">
              <div className="flex justify-between mb-1">
                  <span className="font-[600]">LLEED AND PARTNERS, Digital Consulting </span>
                  <span>Geneva, GE, Switzerland</span>
              </div>
              <div className="flex justify-between mb-1">
                  <i>Co-Founder / Technology</i>
                  <span>06/2020-</span>
              </div>
              <div className="flex justify-between mb-1">
                  <span><span className="font-bold">• Task:</span>Co-founded a digital consulting firm, working with large multinational companies contributing to their digital transforma-
tion, with references such as Louis Dreyfus Company and Rio Tinto. Our projects involved optimizing metals sales and processing
unstructured data for OTC trading. www.lleedpartners.com</span>
              </div>
              </div>
              <div className="work-exp2 flex flex-col mb-4">
              <div className="flex justify-between mb-1">
                  <span className="font-[600]">Louis Dreyfus Company, Commodities Merchant and </span>
                  <span>Singapore, Singapore and Geneva GE,</span>
               </div>
               <div className="flex justify-between mb-1">
                  <span className="font-[600]">Trading </span>
                  <span>Switzerland</span>
              </div>
              <div className="flex justify-between mb-1">
                  <i>Junior Data Scientist Engineer </i>
                  <span>06/2019-03/2020</span>
              </div>
              <div className="flex justify-between mb-1">
                  <span><span className="font-bold">• Task:</span>Developed internal web applications involving data aggregation, natural language processing and more. My work in the
field of FFA trading led to starting my company in which LDC and Rio Tinto are clients</span>
              </div>
              </div>
              <div className="work-exp3 flex flex-col mb-4">
              <div className="flex justify-between mb-1">
                  <span className="font-[600]">ECCO2 Solutions AG, Energy Optimization Startup </span>
                  <span>Givisiez, FR, Switzerland</span>
              </div>
              <div className="flex justify-between mb-1">
                  <i>Junior Software Engineer </i>
                  <span>08/2019-09/2020</span>
              </div>
              <div className="flex flex-col justify-between mb-1">
                  <span><span className="font-bold">• Software Architecture:</span>Contributed to designing the architecture of a complex software solution involving IoT in C# using the
.Net Framework.</span>
                          <span><span>• Software Development:</span>Developed some back-end components for user management and real-time data feeds aggregation from
thousands of wireless sensors in C# using the .Net Framework.</span>
              </div>
              </div>
              <div className="work-exp4 flex flex-col mb-4">
              <div className="flex justify-between mb-1">
                  <span className="font-[600]">Junior Entreprise EPFL</span>
                  <span>Lausanne, VD, Switzerland</span>
              </div>
              <div className="flex justify-between mb-1">
                  <i>IT Consultant </i>
                  <span>06/2019-09/2020</span>
              </div>
              <div className="flex justify-between mb-1">
                  <span><span className="font-bold">• Task:</span>Worked on 2 projects, designed the NO SQL database architecture and developed web applications using React JS and
Node JS.</span>
              </div>
              </div>
              <div className="work-exp5 flex flex-col mb-4">
              <div className="flex justify-between mb-1">
                  <span className="font-[600]">Energisme, Energy Optimization Startup </span>
                  <span>Bourlogne-Billancourt 92, France</span>
              </div>
              <div className="flex justify-between mb-1">
                  <i>Summer Software Developer Intern </i>
                  <span>08/2018-09/2018</span>
              </div>
              <div className="flex justify-between mb-1">
                  <span><span className="font-bold">• Task:</span>Developed a web tool to generate Finite State Machines for graphics animations</span>
              </div>
              </div>
          </div>
          <div className="academic flex flex-col mb-4">
              <span className="text-xl text-center p-1 border-b-2 border-black mb-2">ACADEMIC PROJECTS</span>
              <div className="work-exp1 flex flex-col mb-4">
              <div className="flex justify-between mb-1">
                  <span className="font-[600]">Align Technologies, Dental Med-tech Company </span>
                  <span>Zurich, ZH, Switzerland</span>
              </div>
              <div className="flex justify-between mb-1">
                  <i>Computer Vision Research Intern - Masters Semester Project at ETHZ</i>
                  <span>03/2022 - 07/2022-</span>
              </div>
              <div className="flex justify-between mb-1">
                  <span><span className="font-bold">• Task:</span>Using computer vision techniques and machine learning to analyze 3D scans of teeth in order to identify dental illnesses</span>
              </div>
              </div>
              <div className="work-exp2 flex flex-col mb-4">
              <div className="flex justify-between mb-1">
                  <span className="font-[600]">Miraex, Quantum Technology Startup</span>
                  <span>Lausanne, VD, Switzerland</span>
              </div>
              <div className="flex justify-between mb-1">
                  <i>AI research Intern - Bachelor Thesis Project at EPFL </i>
                  <span>02/2020 - 07/20200</span>
              </div>
              <div className="flex justify-between">
                  <span><span className="font-bold">• Model Development:</span>Performed pattern detection on acoustic signals using signal processing methods such as Wavelet Transform
analysis and machine learning models such as SVMs, auto encoders and convolutional encoders.</span>
              </div>
              </div>
          </div>
          <div className="coursework flex flex-col mb-4">
              <span className="text-xl text-center p-1 border-b-2 border-black mb-2">COURSEWORK/SKILLS</span>
                      <span className="mb-1"><span className="font-bold">• Mathematics:</span>Calculus, Linear Algebra, Information Theory, Signal Processing, Discrete Mathematics, Set Theory, Algebra, Algo-
rithms, Statistics, Probability Theory, Stochastic Processes, Computational Statistics, Computational Intelligence Lab</span>
                      <span className="mb-1"><span className="font-bold">• Computer Science:</span>Advanced Machine Learning, Probabilistic Artificial Intelligence, Parallelism and Concurrency, OO Program-
ming, Functional Programming, Database Systems, Computer Architecture, Network Security, Theory of Computation, Visual
Computing, Digital Signatures</span>
                      <span className="mb-1"><span className="font-bold">• Programming:</span>C, R, Python (Django and data science libraries), Java, SQL, Scala (and Spark), C# (.NET), Javascript (React Js,
Node Js), SQL, Assembly, LaTex</span>
          </div>
          <div className="additional-info flex flex-col">
              <span className="text-xl text-center p-1 border-b-2 border-black mb-2">ADDITIONAL INFO</span>
                      <span className="mb-2"><span className="font-bold">• Personal Interests:</span>Blockchain, Politics, Music, Study of Latin and Ancient Greek.</span>
                      <span className="mb-2"><span className="font-bold">• Activites:</span>Classical Guitar and Solfeggio in regional conservatory with DEM and CEM state certificates (9 years of practice and
ability to teach), Tennis, Soccer, Hiking, Chess</span>
                      <span className="mb-2"><span className="font-bold">• Languages (speaking and writing):</span>French (Native), English (Native), Spanish (B1)</span>
          </div>
    </div>
  );
}

Okay, that's a lot of code but you don't really need to worry about it that much. It's just a React functional component meant to render some UI for the home page. So let's see what it looks like. Run the app:

npm run dev

Go to http://localhost:3000 (or in your case, the port could be different so make sure to check out your terminal):

Doesn't it look kinda more spread? The main div's width needs to be less.
But a tip from my side:

💡
You do not necessarily need to set the width to around 700px , since by default when you will be generating the PDF , the width won't be full screen (though I will show you how you can change it).

Lets reduce the viewport width for now.

Looks good to me. So this is my way of knowing how my PDF is going to look. And its quite easy to work with React components, I can easily make changes to the code and see the rendered screen to see whether it is good enough or not. After being fully satisfied, I can think about generating the equivalent PDF (I mean similar looking PDF).

Okay now the following steps are going to be very important.

Install @onedoc/react-print :

npm i @onedoc/react-print

Now make a separate component where we will be calling the JSX of the main component , wrap it using <Tailwind> from react-print so that the tailwind classes which we are using for styling the HTML, can be used once it is sent to the onedoc servers (don't worry, we will get there).
We will be also seeing the use of <CSS> which can be used to shape the size of the final PDF.

parent folder\components\OneDocComponent.tsx

import React from 'react'
import { CSS, Tailwind } from '@onedoc/react-print';
import Main from "../app/page"
const OnedocComponent = () => {
  return (
    <Tailwind>
      <CSS>
      {
        String.raw`@import url('https://fonts.googleapis.com/css2?family=Libre+Baskerville&family=Literata:opsz@7..72&family=Lora&family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700;1,900&family=Montserrat&family=Mulish&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto&family=Varela+Round&display=swap');
        @page{
          size:8in 13in;
          margin:0;
        }`
      }
      </CSS>
      <Main/>
    </Tailwind>
  )
}

export default OnedocComponent;

Here <CSS> can be used for embedding other styles as well. But for the size of the final PDF, you need to use in which basically means inches.

There are other utilities as well like <PageBreak> and <Footer> which react-print offers but these two were the most important. You can always read their documentation if you like.

First create the folder structure accordingly and use this code:

app\api\v1\getPDF\route.ts


import OnedocComponent from "@/components/OnedocComponent";
import { Onedoc } from "@onedoc/client";
import { compile } from "@onedoc/react-print";
import { readFileSync, writeFileSync } from "fs";
import { join } from "path";
import { JSXElementConstructor, ReactElement, ReactNode } from "react";

const onedoc = new Onedoc(process.env.ONEDOC_API_KEY as string);
//go to the OneDoc website and grab your API key

export async function GET(){
  const { file, error } = await onedoc.render({
      html: await compile(OnedocComponent() as ReactElement<any, string | JSXElementConstructor<any>>)
      //we are using the JSX returned by the OneDocComponent and converting it into html
      ,
    test: false,
    assets: [
      {
            path: "./util/util.css",//you can have a util folder in parent folder 
          //with a util.css file for some specific styles like for fonts
        content: readFileSync(join(process.cwd(), "./util/util.css")).toString(),
      },
    ],
  });

  if (error) {
    throw error;
  }

  const pdfBuffer = Buffer.from(file);

  // Return the PDF
  return new Response(pdfBuffer, {
    headers: {
      "Content-Type": "application/pdf",
    },
  });
}

Now by looking you can understand you need to do three things:

First install @onedoc/client

npm i @onedoc/client

Second, go to the OneDocLabs website and grab your API key. The make a .env file in the parent folder:

.env

ONEDOC_API_KEY=....//put your API key here

Then in the .gitignore file, add .env so that on pushing to github, your secrets don't get revealed.

Thirdly, create a util folder in parent folder and under it keep the util.css file. Keep it empty if you wish, it is basically for extra styles like fonts .

Okay, in case you are not able to understand whats happening in the route.ts file, basically we are calling the <OnedocComponent/> to get the JSX and then using the compile function, which basically sends it to the onedoc servers under the hood to get the equivalent HTML and returns it back. This HTML is used to generate the pdfData corresponding to which the PDF will look similar to the HTML or basically our original component.

Okay lets move back to our frontend.

Now lets create the page where the generated PDF will be displayed.

app\generate\page.tsx

"use client"
import React, { useState, useEffect } from "react";
import Link from "next/link";

interface OneDocGenerateProps {}

const OneDocGenerate: React.FC<OneDocGenerateProps> = () => {
  const [pdfData, setPdfData] = useState<string | null>(null);

  useEffect(() => {
    const fetchPdf = async () => {
      try {
          const response = await fetch(`/api/v1/getPDF`);//makes a GET request and receives 
          //the pdfData
        const blob = await response.blob();
        setPdfData(URL.createObjectURL(blob));
      } catch (error) {
        console.error("Error fetching PDF:", error);
      }
    };

    fetchPdf();
  }, []);

  return (
    <div style={{ height: "100vh", width: "100vw", overflow: "hidden" }}>
      {pdfData ? (
        <object
          data={pdfData}
          type="application/pdf"
          style={{ width: "100%", height: "100%", border: "none" }}
        >
          <MobileView pdfData={pdfData} />
        </object>
      ) : (
        <Loading />
      )}
    </div>
  );
};

interface LoadingProps {}

//Loader component to be displayed when the pdf data is yet to be received
const Loading: React.FC<LoadingProps> = () => {
  return (
    <div className="h-screen flex flex-col gap-y-[2vh] justify-center items-center bg-[#001428]">
      <div className="spinner"></div>
      <p className="text-[#009ff9] text-lg">Loading your PDF...</p>
      <p className="text-[#009ff9] text-lg">Make sure to reload in case of error.</p>
      <style>
        {`.spinner {
   width: 56px;
   height: 56px;
   border-radius: 50%;
   background: radial-gradient(farthest-side,#009ff9 94%,#0000) top/9px 9px no-repeat,
          conic-gradient(#0000 30%,#009ff9);
   -webkit-mask: radial-gradient(farthest-side,#0000 calc(100% - 9px),#000 0);
   animation: spinner-c7wet2 1s infinite linear;
}

@keyframes spinner-c7wet2 {
   100% {
      transform: rotate(1turn);
   }
}
`}
      </style>
    </div>
  );
};

interface MobileViewProps {
  pdfData: string;
}

//for mobile view, since in some old phones, preview of PDF might not be available
//user needs to download it
const MobileView: React.FC<MobileViewProps> = ({ pdfData }) => {
  return (
    <div className="mobile h-screen flex flex-col justify-center items-center text-center">
      <p className="text-lg">Sorry you cannot preview this file in mobile view</p>
      <p className="text-lg">Shift to PC view</p>
      <p className="text-lg">Or</p>
      <Link href={pdfData}>
        <button className="px-8 py-2 rounded-full bg-gradient-to-b from-blue-500 to-blue-600 text-white focus:ring-2 focus:ring-blue-400 hover:shadow-xl transition duration-200">
          Download the File
        </button>
      </Link>
    </div>
  );
};

export default OneDocGenerate;

Simply for the sake of our tutorial, we will manually move to this page. For an original app, you might need to make some button in the home page and on click, some javascript function will run which will redirect the page to this.

See, looks fine to me.

Hope you guys were able to generate your first PDF using Onedoc. The full source code is available here. Thanks for reading and I will meet you in my next blog!