Caine Nielsen • Building and Deploying the Math Worksheet Generator
Introduction
As a developer and parent, I've always been passionate about creating tools that make learning more effective. The Math Worksheet Generator project was born out of a personal need: my 6-year-old son was struggling with math, and I wanted to help him by creating customizable practice worksheets that we could print off at home. What started as a tool to support my son's learning journey evolved into something I thought could benefit other parents and educators as well. In this blog post, I'll take you through the journey of developing and deploying this application, sharing the technical challenges, interesting features, and lessons learned along the way.
The Vision
The goal was straightforward: create a web application that allows teachers, parents, and educators to generate customizable math worksheets with various operations (addition, subtraction, multiplication, and division). The application needed to be:
Technical Architecture
After evaluating different technologies, I settled on a modern stack that balanced performance, developer experience, and deployment simplicity:
Backend
Frontend
Deployment
Interesting Features and Implementation Details
Shared Browser Instance
One of the most interesting technical challenges was optimizing the worksheet generation process. Initially, each request would launch a new Puppeteer browser instance, which was resource-intensive and slow. I implemented a shared browser instance pattern that maintains a single browser across multiple requests:
1// Global browser instance2let browserInstance: Browser | null = null;
34// Get the browser instance, initializing it if necessary5export async function getBrowser(): Promise<Browser> {
6 if (!browserInstance || !browserInstance.isConnected()) {
7 await initBrowser();
8 }910 if (!browserInstance) {
11 throw new Error("Failed to initialize browser");
12 }1314 return browserInstance;
15}This significantly improved performance, reducing the time to generate worksheets by approximately 70%.
Smart Problem Generation
Generating appropriate math problems was trickier than I initially thought. For example, division problems needed to have clean results (no remainders), and subtraction problems for younger students shouldn't result in negative numbers. I implemented smart problem generation logic to handle these cases:
1case 'division': {
2 // For division, ensure we have clean division (no remainders)3 // First, choose a divisor between 1 and 20 (to keep problems reasonable)4 num2 = getRandomNumber(1, Math.min(maxNumber, 20));
56 // Then, choose a multiple of that divisor within the range7 // Calculate how many multiples of num2 fit within the range8 const minMultiple = Math.ceil(Math.max(minNumber, 1) / num2);
9 const maxMultiple = Math.floor(maxNumber / num2);
1011 // If we can't find a valid multiple in the range, adjust12 if (minMultiple > maxMultiple) {
13 // Fallback to a simpler division problem14 num2 = getRandomNumber(1, 10);
15 num1 = num2 * getRandomNumber(1, 10);
16 } else {
17 // Get a random multiple18 const multiple = getRandomNumber(minMultiple, maxMultiple);
19 num1 = num2 * multiple;
20 }21 break;
22}This ensures that students receive appropriate problems for their skill level.
Base64 Data Transfer
To securely pass worksheet data between the client and server, I used base64 encoding:
1// Encode the worksheet data to a base64 encoded string2const base64Data = encodeJsonToBase64(enrichedWorksheet);
34// Navigate to the worksheet page with the data as a query parameter5await page.goto(`${BASE_URL}/render-worksheet?data=${base64Data}`, {
6 waitUntil: "networkidle0",
7});
This approach allowed me to pass complex data structures through URL parameters without worrying about special characters or URL length limitations.
Precise Paper Size Rendering
One of the critical challenges was ensuring worksheets would print correctly on standard paper. I implemented a precise paper size rendering system that accurately maps pixel dimensions to physical paper:
1// Default options for worksheet processing2const DEFAULT_OPTIONS: WorksheetProcessingOptions = {
3 viewport: {
4 width: 816, // 8.5 inches at 96 DPI
5 height: 1056, // 11 inches at 96 DPI
6 deviceScaleFactor: 1,
7 },
8};
910export async function processWorksheets<T>(
11 worksheets: WorksheetData[],
12 processor: (page: Page, worksheets: WorksheetData[]) => Promise<T>,
13 options: WorksheetProcessingOptions = DEFAULT_OPTIONS
14): Promise<T> {
15 // Get the shared browser instance16 const browser = await getBrowser();
1718 // Create a new page19 const page = await browser.newPage();
2021 try {
22 // Set the page size to 8.5" x 11" (US Letter) or custom viewport23 if (options.viewport) {
24 await page.setViewport(options.viewport);
25 }2627 // Process the worksheets with the provided processor function28 return await processor(page, worksheets);
29 } finally {
30 // Close the page, not the browser31 await page.close();
32 }33}This approach ensures that:
The precise sizing was particularly important for younger students, ensuring that number spacing and problem layouts were appropriate for their developmental stage and motor skills.
Deployment Challenges and Solutions
Docker Configuration for Puppeteer
One of the biggest deployment challenges was getting Puppeteer to work correctly in a containerized environment. Chrome requires specific libraries and configurations to run headless in Docker:
1# Install Google Chrome Stable and fonts
2RUN apt-get update && apt-get install curl gnupg -y \\
3 && curl --location --silent <https://dl-ssl.google.com/linux/linux_signing_key.pub> | apt-key add - \\
4 && sh -c 'echo "deb [arch=amd64] <http://dl.google.com/linux/chrome/deb/> stable main" >> /etc/apt/sources.list.d/google.list' \\
5 && apt-get update \\
6 && apt-get install google-chrome-stable -y --no-install-recommends \\
7 && rm -rf /var/lib/apt/lists/*
Additionally, Puppeteer needed specific launch arguments to work in a containerized environment:
1browserInstance = await puppeteer.launch({
2 executablePath,3 args: [
4 "--no-sandbox",
5 "--disable-setuid-sandbox",
6 "--disable-dev-shm-usage",
7 // Other arguments...8 ],
9 headless: true,
10});
Secure Authentication with Workload Identity Federation
Rather than using service account keys (which can be a security risk), I implemented Workload Identity Federation for secure authentication between GitHub Actions and Google Cloud:
1- id: 'auth'
2 name: 'Authenticate to Google Cloud'
3 uses: 'google-github-actions/auth@v2'
4 with:
5 workload_identity_provider: 'projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
6 service_account: 'github-actions-sa@cn-math-worksheet-generator.iam.gserviceaccount.com'
This approach eliminated the need to store service account keys as GitHub secrets, significantly improving security.
Resource Allocation for PDF Generation
PDF generation is resource-intensive, especially when creating multiple worksheets. I had to carefully tune the Cloud Run resource allocation to ensure reliable performance:
1resources:
2 limits:
3 memory: 1Gi
4 cpu: 1000m
Through testing, I found that 1GB of memory and 1 CPU core provided the optimal balance between cost and performance for this application.
User Experience Considerations
Progressive Enhancement
I designed the application with progressive enhancement in mind, ensuring it works even if JavaScript is disabled or fails to load:
Responsive Design
The application needed to work well on various devices, from desktop computers to tablets and phones:
1@media (max-width: 768px) {
2 .app-main {
3 flex-direction: column;
4 }56 .settings-panel, .preview-panel {
7 width: 100%;
8 }9}This responsive approach ensures teachers can create worksheets from any device, whether they're at their desk or moving around the classroom.
Accessibility
Accessibility was a priority from the beginning:
1<div2 class="checkbox-group"
3 role="group"
4 aria-labelledby="operations-heading"
5>6 <label class="checkbox-container">
7 <input type="checkbox" id="addition" checked>
8 <span class="checkbox-label">Addition</span>
9 </label>
10 <!-- Other checkboxes... -->
11</div>
I used semantic HTML, ARIA attributes, and proper focus management to ensure the application is usable by everyone, including those using screen readers or keyboard navigation.
Lessons Learned
1. Start with a Clear User Story
Before writing any code, I spent time understanding exactly what teachers and parents needed in a worksheet generator. This user-focused approach helped me prioritize features and create an intuitive interface.
2. Choose the Right Tools for the Job
While I initially considered using a full-stack framework like Next.js, I ultimately chose a simpler approach with Deno and vanilla JavaScript. This decision paid off in terms of performance and deployment simplicity.
3. Test in Production-Like Environments Early
I encountered several issues with Puppeteer in Docker that weren't apparent in local development. Setting up a production-like environment early in the development process would have saved time troubleshooting these issues.
4. Security Should Be a Priority from Day One
Implementing Workload Identity Federation was more complex than using service account keys, but the security benefits were worth the effort. Building security into the application from the beginning is always easier than adding it later.
5. Performance Optimization Is an Ongoing Process
The shared browser instance pattern was a significant performance improvement, but it came after initial deployment. Continuous monitoring and optimization are essential for maintaining a fast, responsive application.
Future Enhancements
While the current version of the Math Worksheet Generator meets the initial requirements, there are several enhancements I'm considering for future updates:
Conclusion
Building the Math Worksheet Generator was a rewarding experience that combined my passion for education with my love of software development. The application has already helped numerous teachers save time creating math practice materials, and I'm excited to continue improving it based on user feedback.
The project demonstrates that even relatively simple applications can benefit from modern development practices like containerization, CI/CD automation, and serverless deployment. By focusing on user needs and leveraging the right technologies, it's possible to create valuable tools that make a real difference in people's daily work.
If you're interested in exploring the code or contributing to the project, check out the GitHub repository or try the live application.
Happy teaching and coding!
As always, thank you for reading. I really appreciate it. 💖