Tips

Using PagedJS with Next.js

Using PagedJS with Next.js

Using PagedJS with Next.js

Create a nice PDF with Next.js and PagedJS

an oversized, whimsical printer machine labeled 'Doppio', creating a stream of PDF documents
an oversized, whimsical printer machine labeled 'Doppio', creating a stream of PDF documents

Set up a Next.js project

First, let's make a new Next.js project

Or you can follow the installation instructions from the official documentation.


Install the pagedjs npm module

The pagedjs library is available through a script, a command line or a npm module. We'll focus on the npm module here but feel free to go check their documentation for more solutions.

Install the pagedjs npm module.

npm install --save

Create a new page

Let's create a new page to display our PDF.

Create a new directory under the app folder named about and immediatly create a file called page.tsx with the following code.

export default function About() {
  return (
    <div className="App">
      <section className="chapter" id="about-page">
        <h2 className="title" id="label-title">About</h2>
        <p className="text">Lorem ipsum dolor sit amet consectetur adipiscing elit. Duis nibh tortor</p>
      </section>

      <section className="chapter" id="chapter1-page">
        <h2 className="title">Chapter 1</h2>
        <p className="text">Lorem ipsum dolor sit amet</p>
      </section>

      <section className="chapter" id="chapter2-page">
        <h2 className="title">Chapter 2</h2>
        <p className="text">consectetur adipiscing elit</p>
      </section>

      <section className="chapter" id="chapter3-page">
        <h2 className="title">Chapter 3</h2>
        <p className="text">Duis nibh tortor, pellentesque eu suscipit vel</p>
      </section>
    </div>


Setup PagedJS

We'll use the Previewer from pagedjs to display our page as a PDF.

First, import the Previewer from the pagedjs npm module by adding at the top of your file

import { Previewer } from 'pagedjs';

Then, add a hook effect to initialize the Previewer when the component is mounted.

  useEffect(() => {
    startPreview();

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

Don't forget to import the useEffect hook from React.

import { useEffect } from 'react';

And to indicate that this component is a client-side component, so we can use this effect (Next.js documentation is here).

"use client";

This is what the function startPreview looks like (pagedjs documentation is here).

const startPreview = () => {
  if (isInitialized) return;
  isInitialized = true;

  let DOMContent = document.querySelector('.App');
  let paged = new Previewer();

  paged.preview(DOMContent.content).then((flow: any) => {
    console.log('Rendered', flow.total, 'pages.');
  });
};

We use the variable isInitialized to make sure we don't initialize the Previewer more than once, as the component gets remounted by React in development mode. Be sure to initialize it

...

export default function About() {
  let isInitialized = false; // <-- add this line

  useEffect(() => {
    ...

To summarize, your page.tsx file should look like this

"use client";

import { useEffect } from 'react';
import { Previewer } from 'pagedjs';

export default function About() {
  let isInitialized = false;

  useEffect(() => {
    startPreview();

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const startPreview = () => {
    if (isInitialized) return;
    isInitialized = true;

    let DOMContent = document.querySelector('.App');
    let paged = new Previewer();

    paged.preview(DOMContent.content).then((flow: any) => {
      console.log('Rendered', flow.total, 'pages.');
    });
  };

  return (
    <div className="App">
      <section className="chapter" id="about-page">
        <h2 className="title" id="label-title">About</h2>
        <p className="text">Lorem ipsum dolor sit amet consectetur adipiscing elit. Duis nibh tortor</p>
      </section>

      <section className="chapter" id="chapter1-page">
        <h2 className="title">Chapter 1</h2>
        <p className="text">Lorem ipsum dolor sit amet</p>
      </section>

      <section className="chapter" id="chapter2-page">
        <h2 className="title">Chapter 2</h2>
        <p className="text">consectetur adipiscing elit</p>
      </section>

      <section className="chapter" id="chapter3-page">
        <h2 className="title">Chapter 3</h2>
        <p className="text">Duis nibh tortor, pellentesque eu suscipit vel</p>
      </section>
    </div>
  )
}


Styling the page

Now you are ready to add style for pagedjs to create your beautiful PDF.

Start by creating a css file named about.css in the same foler as your page.tsx file, with the following content.

.App {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  padding: 2rem 4rem;
  box-shadow: 0 0 0 1px grey;
}

h2 {
  text-align: center;
  color: #3c3c3c;
}

p {
  text-align: center;
  color: #a1a1a1;
}

This is just general styling for your page, it has nothing to do with pagedjs.

Now in this same file about.css, add the following css to separate each chapter on a new page.

.chapter {
  break-after: page;
}

Add the following to format the page

@page {
  size: 8.5in 11in;
  margin: 20mm 25mm;
}

And finally, add the following to add text at the top of the first page

@page:first {
  @top-center { content: 'START'; }
}


Display custom content through "named strings"

What if you want to display the name of the chapter you are in at the bottom of the PDF ?

Let's add the following css to the about.css file.

@page {
  @bottom-center { content: string(title); }
}

.chapter > h2 {
  string-set: title content(text);
}

The first block, is used to tell pagedjs to add content in the margin of the PDF, at the bottom center position (check all the positions available).

The second block, is used to make available the title of the chapter to pagedjs, we use the named strings feature from pagedjs to give the content of the h2 tag from every chapter into the named string "title".


Display custom content through HTML attributes

It is also possible to display content in the PDF declaring custom attributes into the HTML.

First, to every section tag in your component, add a custom attribute data-* where you replace * with whatever name you want to give to your content.

For example adding a data-reference to every chapter.

<section className="chapter" id="about-page" data-reference="001">
  <h2 className="title" id="label-title">About</h2>
  <p className="text">Lorem ipsum dolor sit amet consectetur adipiscing elit. Duis nibh tortor</p>
</section>

<section className="chapter" id="chapter1-page" data-reference="002">
  <h2 className="title">Chapter 1</h2>
  <p className="text">Lorem ipsum dolor sit amet</p>
</section>

<section className="chapter" id="chapter2-page" data-reference="003">
  <h2 className="title">Chapter 2</h2>
  <p className="text">consectetur adipiscing elit</p>
</section>

<section className="chapter" id="chapter3-page" data-reference="004">
  <h2 className="title">Chapter 3</h2>
  <p className="text">Duis nibh tortor, pellentesque eu suscipit vel</p>
</section>

Then, add the following to your about.css.

@page {
  @right-middle { content: string(ref); }
}
.chapter {
  string-set: ref attr(data-reference);
}

The first block, is used to tell pagedjs where to display the content (just as before, you can check all the positions available).

The second block, is used to make available the reference to pagedjs, we use the generated text feature from pagedjs.


What's next

Be sure to check the PagedJS documentation to see all the features available and how you can use them to render beautiful PDFs !

Set up a Next.js project

First, let's make a new Next.js project

Or you can follow the installation instructions from the official documentation.


Install the pagedjs npm module

The pagedjs library is available through a script, a command line or a npm module. We'll focus on the npm module here but feel free to go check their documentation for more solutions.

Install the pagedjs npm module.

npm install --save

Create a new page

Let's create a new page to display our PDF.

Create a new directory under the app folder named about and immediatly create a file called page.tsx with the following code.

export default function About() {
  return (
    <div className="App">
      <section className="chapter" id="about-page">
        <h2 className="title" id="label-title">About</h2>
        <p className="text">Lorem ipsum dolor sit amet consectetur adipiscing elit. Duis nibh tortor</p>
      </section>

      <section className="chapter" id="chapter1-page">
        <h2 className="title">Chapter 1</h2>
        <p className="text">Lorem ipsum dolor sit amet</p>
      </section>

      <section className="chapter" id="chapter2-page">
        <h2 className="title">Chapter 2</h2>
        <p className="text">consectetur adipiscing elit</p>
      </section>

      <section className="chapter" id="chapter3-page">
        <h2 className="title">Chapter 3</h2>
        <p className="text">Duis nibh tortor, pellentesque eu suscipit vel</p>
      </section>
    </div>


Setup PagedJS

We'll use the Previewer from pagedjs to display our page as a PDF.

First, import the Previewer from the pagedjs npm module by adding at the top of your file

import { Previewer } from 'pagedjs';

Then, add a hook effect to initialize the Previewer when the component is mounted.

  useEffect(() => {
    startPreview();

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

Don't forget to import the useEffect hook from React.

import { useEffect } from 'react';

And to indicate that this component is a client-side component, so we can use this effect (Next.js documentation is here).

"use client";

This is what the function startPreview looks like (pagedjs documentation is here).

const startPreview = () => {
  if (isInitialized) return;
  isInitialized = true;

  let DOMContent = document.querySelector('.App');
  let paged = new Previewer();

  paged.preview(DOMContent.content).then((flow: any) => {
    console.log('Rendered', flow.total, 'pages.');
  });
};

We use the variable isInitialized to make sure we don't initialize the Previewer more than once, as the component gets remounted by React in development mode. Be sure to initialize it

...

export default function About() {
  let isInitialized = false; // <-- add this line

  useEffect(() => {
    ...

To summarize, your page.tsx file should look like this

"use client";

import { useEffect } from 'react';
import { Previewer } from 'pagedjs';

export default function About() {
  let isInitialized = false;

  useEffect(() => {
    startPreview();

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const startPreview = () => {
    if (isInitialized) return;
    isInitialized = true;

    let DOMContent = document.querySelector('.App');
    let paged = new Previewer();

    paged.preview(DOMContent.content).then((flow: any) => {
      console.log('Rendered', flow.total, 'pages.');
    });
  };

  return (
    <div className="App">
      <section className="chapter" id="about-page">
        <h2 className="title" id="label-title">About</h2>
        <p className="text">Lorem ipsum dolor sit amet consectetur adipiscing elit. Duis nibh tortor</p>
      </section>

      <section className="chapter" id="chapter1-page">
        <h2 className="title">Chapter 1</h2>
        <p className="text">Lorem ipsum dolor sit amet</p>
      </section>

      <section className="chapter" id="chapter2-page">
        <h2 className="title">Chapter 2</h2>
        <p className="text">consectetur adipiscing elit</p>
      </section>

      <section className="chapter" id="chapter3-page">
        <h2 className="title">Chapter 3</h2>
        <p className="text">Duis nibh tortor, pellentesque eu suscipit vel</p>
      </section>
    </div>
  )
}


Styling the page

Now you are ready to add style for pagedjs to create your beautiful PDF.

Start by creating a css file named about.css in the same foler as your page.tsx file, with the following content.

.App {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  padding: 2rem 4rem;
  box-shadow: 0 0 0 1px grey;
}

h2 {
  text-align: center;
  color: #3c3c3c;
}

p {
  text-align: center;
  color: #a1a1a1;
}

This is just general styling for your page, it has nothing to do with pagedjs.

Now in this same file about.css, add the following css to separate each chapter on a new page.

.chapter {
  break-after: page;
}

Add the following to format the page

@page {
  size: 8.5in 11in;
  margin: 20mm 25mm;
}

And finally, add the following to add text at the top of the first page

@page:first {
  @top-center { content: 'START'; }
}


Display custom content through "named strings"

What if you want to display the name of the chapter you are in at the bottom of the PDF ?

Let's add the following css to the about.css file.

@page {
  @bottom-center { content: string(title); }
}

.chapter > h2 {
  string-set: title content(text);
}

The first block, is used to tell pagedjs to add content in the margin of the PDF, at the bottom center position (check all the positions available).

The second block, is used to make available the title of the chapter to pagedjs, we use the named strings feature from pagedjs to give the content of the h2 tag from every chapter into the named string "title".


Display custom content through HTML attributes

It is also possible to display content in the PDF declaring custom attributes into the HTML.

First, to every section tag in your component, add a custom attribute data-* where you replace * with whatever name you want to give to your content.

For example adding a data-reference to every chapter.

<section className="chapter" id="about-page" data-reference="001">
  <h2 className="title" id="label-title">About</h2>
  <p className="text">Lorem ipsum dolor sit amet consectetur adipiscing elit. Duis nibh tortor</p>
</section>

<section className="chapter" id="chapter1-page" data-reference="002">
  <h2 className="title">Chapter 1</h2>
  <p className="text">Lorem ipsum dolor sit amet</p>
</section>

<section className="chapter" id="chapter2-page" data-reference="003">
  <h2 className="title">Chapter 2</h2>
  <p className="text">consectetur adipiscing elit</p>
</section>

<section className="chapter" id="chapter3-page" data-reference="004">
  <h2 className="title">Chapter 3</h2>
  <p className="text">Duis nibh tortor, pellentesque eu suscipit vel</p>
</section>

Then, add the following to your about.css.

@page {
  @right-middle { content: string(ref); }
}
.chapter {
  string-set: ref attr(data-reference);
}

The first block, is used to tell pagedjs where to display the content (just as before, you can check all the positions available).

The second block, is used to make available the reference to pagedjs, we use the generated text feature from pagedjs.


What's next

Be sure to check the PagedJS documentation to see all the features available and how you can use them to render beautiful PDFs !

By Frédéric Llorca

January 8, 2024