Building a PDF Download Form with Notion Integration in Docusaurus: A Complete Guide

Introduction

Implementing a “Download PDF Report” feature with user data collection in a Docusaurus website presents unique challenges, especially when integrating with external services like Notion. This article documents our journey building this functionality for the FemTech Weekend website, covering the obstacles we faced and the solutions we implemented. By the end, you’ll understand how to create a PDF download component that collects user information and stores it in a Notion database, working seamlessly in both local development and Vercel deployment.

Project Overview

Our goal was to create a component that would:

  1. Display a “Download Full PDF Report” button on blog posts
  2. Show a form modal when clicked to collect user information
  3. Submit this information to a Notion database
  4. Allow the user to download the PDF after form submission
  5. Work in both development and production environments

The Architecture

The final solution involved several key components:

  1. A DownloadPdfButton React component
  2. An API route handler in src/api/pdf-form-submit.js
  3. A forwarding API endpoint in api/pdf-form-submit/index.js
  4. A custom Docusaurus plugin for API route handling
  5. Development scripts to run both Docusaurus and API servers

Challenge 1: Understanding the Docusaurus API Route System

The Problem

Unlike Next.js, Docusaurus doesn’t have a built-in API routes system. Our initial attempts to create API endpoints failed because we didn’t understand how API routes are handled in Docusaurus.

The Solution

We discovered that Docusaurus requires a custom plugin to handle API routes. This plugin sets up a proxy in development mode that forwards API requests to a separate Node.js server running on port 3001.

// src/plugins/api-routes.js
module.exports = function apiRoutesPlugin(context, options) {
return {
name: 'docusaurus-api-routes-plugin',
configureWebpack(config, isServer) {
if (!isServer && process.env.NODE_ENV === 'development') {
return {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3001',
secure: false,
changeOrigin: true,
// Additional configuration...
},
},
},
};
}
return {};
},
// Additional plugin configuration...
};
};

This plugin must be registered in docusaurus.config.ts to work properly.

Challenge 2: CORS and Request Handling

The Problem

Even after setting up the API routes plugin, we encountered CORS errors when submitting the form, particularly in the development environment.

The Solution

We added proper CORS headers to our API server configuration and ensured that the proxy settings correctly handled both OPTIONS preflight requests and actual POST requests:

// In the plugin's proxy configuration
onProxyRes: (proxyRes, req, res) => {
// Add CORS headers to the response
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS';
proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type';
},

Challenge 3: API Server Configuration

The Problem

We needed a separate API server to handle requests when in development mode, but weren’t sure how to structure it properly.

The Solution

We created an api-server.js file that:

  1. Sets up a Node.js HTTP server on port 3001
  2. Loads environment variables from .env.local
  3. Routes API requests to the appropriate handlers in the src/api directory
  4. Provides proper error handling and logging

For Vercel deployment, we created a parallel structure in the api folder that forwards requests to the main implementation.

Challenge 4: Notion Database Integration

The Problem

Connecting to Notion and storing form submissions required specific configuration and error handling to work reliably.

The Solution

We implemented a comprehensive Notion integration in our pdf-form-submit.js handler:

// src/api/pdf-form-submit.js
const { Client } = require('@notionhq/client');

// Initialize Notion client
const notion = new Client({
auth: process.env.NOTION_TOKEN,
});

async function handler(req, res) {
// Validation and error handling
// Create Notion page properties
const properties = {
Name: {
title: [{ text: { content: fullName } }],
},
// Additional properties...
};

// Create the page in Notion
const response = await notion.pages.create({
parent: { database_id: process.env.PDF_FORM_DATABASE_ID },
properties: properties,
});
// Return success response
}

Challenge 5: Development Environment Setup

The Problem

Running both the Docusaurus server and the API server simultaneously was cumbersome and error-prone.

The Solution

We created a custom start-dev.js script that:

  1. Checks if required environment variables are present
  2. Creates a default .env.local if it doesn’t exist
  3. Checks if necessary ports are available
  4. Starts both servers in the correct order
  5. Provides clear console logging for debugging
// start-dev.js
async function startServers() {
// Start API server first
const apiProcess = spawn('node', ['api-server.js'], {
stdio: 'inherit',
shell: true
});
// Wait a moment for API server to initialize
await new Promise(resolve => setTimeout(resolve, 2000));
// Start Docusaurus server
const docusaurusProcess = spawn('npm', ['run', 'start'], {
stdio: 'inherit',
shell: true,
env: { ...process.env, BROWSER: 'none' }
});

// Handle process errors and shutdown
// ...
}

Challenge 6: Environment Variables

The Problem

Managing environment variables between development and production environments was challenging, especially for the Notion integration.

The Solution

We implemented a system that:

  1. Uses .env.local for local development
  2. Relies on Vercel environment variables for production
  3. Exposes necessary variables to client-side code through the plugin
// In the plugin's injectHtmlTags method
injectHtmlTags() {
return {
headTags: [
{
tagName: 'script',
innerHTML: `
window.ENV = {
NOTION_DATABASE_ID: '${process.env.NOTION_DATABASE_ID || ''}',
// Other variables...
};
`,
},
],
};
},

The Final Implementation

Component Structure

Our final DownloadPdfButton component:

  1. Shows a button that triggers a modal form
  2. Collects user information
  3. Submits the form to our API endpoint
  4. Provides feedback on submission status
  5. Opens the PDF link after successful submission

API Handler Structure

The API handler follows these steps:

  1. Validates incoming request data
  2. Formats the data for Notion
  3. Creates a new page in the Notion database
  4. Returns appropriate success/error responses

Development Setup

To run the project locally:

  1. Create a .env.local file with required Notion credentials
  2. Run node start-dev.js to start both servers
  3. The Docusaurus site runs on port 3000, while the API server runs on port 3001

Production Deployment

For Vercel deployment:

  1. Configure environment variables in the Vercel project settings
  2. Vercel’s serverless functions automatically handle the API routes
  3. The same code works in both environments thanks to our API forwarding structure

Key Lessons Learned

  1. Understand the Platform: Docusaurus handles API routes differently than frameworks like Next.js
  2. Dual Server Architecture: Running separate servers for frontend and API in development provides flexibility
  3. Error Logging: Comprehensive logging helps identify issues quickly
  4. Environment Consistency: Ensuring development and production environments are configured similarly prevents deployment surprises
  5. API Forwarding: Creating a forwarding structure ensures the same code works in both environments

Conclusion

Building a PDF download component with Notion integration in Docusaurus requires understanding both the limitations of the platform and how to work around them. By setting up a custom plugin, implementing proper API handlers, and ensuring consistent environment configuration, we successfully created a solution that works both locally and in production.

This approach can be extended to other external integrations beyond Notion, making it a valuable pattern for any Docusaurus project that needs to handle form submissions or other API interactions.

Learn more Building a PDF Download Form with Notion Integration in Docusaurus: A Complete Guide

Leave a Reply