I wanted to create a server that returns a page written in React as a PDF, but I couldn't find a standard method for doing so. Therefore, I tried various approaches and am sharing the one that worked for me. If you know of a better way, I'd appreciate your comments.
What I Want to Do
- Write the page to be converted to PDF in React.
- Allow the page created in step 1 to be downloadable as a PDF.
The server will work as follows: access to / will display the page from step 1, and access to /pdf will allow downloading the page as a PDF.
Steps and Explanation
Here's a step-by-step guide, starting from project creation.
1. Creating a Remix Server
First, create the project using the command from Remix's official Quick Start guide:
npx create-remix@latest
This will create the project in an interactive format. I opted to use pnpm
instead of npm
:
> Install dependencies with npm?
> No
After the project is created, install the necessary packages:
pnpm i
Next, start the development server:
pnpm dev
Now, if you access http://localhost:5173
, you should see the initial page.
To ensure that the build passes and the production server can start, run:
pnpm build
pnpm start
You can confirm the operation by accessing http://localhost:3000
.
2. Configuration Adjustments
I wanted the development server to run on port 3000, so I modified the scripts
in package.json
:
"scripts": {
"build": "remix vite:build",
- "dev": "remix vite:dev",
+ "dev": "remix vite:dev --port 3000",
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc"
},
With this change, pnpm dev
will allow access via http://localhost:3000
.
3. Creating the PDF Endpoint
To make the content of /
available as a PDF at /pdf
, I added puppeteer
to the dependencies to convert HTML to PDF:
pnpm add puppeteer
Next, I added /app/routes/pdf.tsx
. By placing it under the routes
directory, Remix automatically detects it and creates the /pdf
endpoint:
import puppeteer from "puppeteer";
export async function loader() {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto("http://localhost:3000", { waitUntil: 'networkidle0' });
const pdf = await page.pdf({ format: 'A4' });
await browser.close();
return new Response(pdf, {
status: 200,
headers: {
"Content-Type": "application/pdf",
'Content-Disposition': 'attachment; filename=remix.pdf',
},
})
}
Now, if you access http://localhost:3000/pdf
on either the development or production server, the same content as http://localhost:3000
will be downloaded as a PDF.
4. Font Configuration
To use custom fonts (e.g., NotoSerifJP.ttf
), place the font file in the /public directory, which is automatically created by npx create-remix@latest
.
Next, create a CSS file to configure the font and place it under /app
. Here’s the content of /app/base.css
:
@font-face {
font-family: 'NotoSerifJP';
src: url('/NotoSerifJP.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'NotoSerifJP', sans-serif;
}
Finally, import the CSS in /app/root.tsx
:
import "./tailwind.css";
+ import "./base.css";
Conclusion
Now, you can check the page display at http://localhost:3000
and download the PDF at http://localhost:3000/pdf
. By the way, I also tried React-pdf, but since it required building the PDF from scratch with limited styling options, I decided to go with the current method.
Top comments (1)
Neat idea