I got tired of graph-paper sites that print at the wrong scale, hide the PDF behind a signup, or slap a watermark on it. So I built one that's free, has no login, and prints true-to-scale.
The interesting part for me was the architecture: a single pure function buildPaper(config) emits geometric primitives (lines / circles / text), and everything is derived from it - the on-screen SVG preview, the in-browser PDF, and the build-time PDF and OG-image generation. No drawing logic is duplicated across output formats, so adding a new paper style is one renderer function plus about five one-line registrations, and it automatically gets a live preview, a downloadable PDF, an OG image, and SEO pages across every paper size.
Everything is true-to-scale: the SVG viewBox and the PDF are both in millimetres, so a 5 mm grid measures exactly 5 mm on paper when you print at 100%.
It's fully static - no backend, no database, no runtime. About 1,900 pages are prerendered to HTML, ~1,700 PDFs are pre-generated at build time (reusing the exact same draw logic as the browser), and it all deploys to a CDN as plain files.
Tech stack:
- Nuxt 4 / Vue 3 (script setup, TypeScript), Nitro static generation (nuxt generate)
- Tailwind CSS v4 plus scoped CSS with a small design-token system
- Paper engine: one pure buildPaper() -> primitives; one renderer per style. Single source of truth for preview + PDF + images
- Preview: pure renderPaperSvg() -> inline SVG baked into the prerendered HTML, so previews render with zero client JS
- PDF: jsPDF (dynamic import) for arbitrary custom configs in the browser; a Bun script reuses the same primitives to pre-build the static PDFs
- Social images: SVG -> PNG at build time with @resvg/resvg-js (1200x630 OG per style + full A4 sheets)
- Data-driven pSEO: ~40 styles across the full ISO A/B/C and US/Imperial sizes -> ~1,900 pages generated from one data table; JSON-LD (WebSite/HowTo/FAQ/Breadcrumb); selective noindex on low-demand size combos so thin pages don't dilute the site
- Tooling: Bun (package manager + native-TS runner for all the generators)
- Hosting: Cloudflare Pages, pure static (no worker)
There's also a custom editor (tweak spacing, color and margins, then export) and a virtual graph-paper canvas you can draw on, where the logical drawing space is decoupled from the pixel buffer so it stays crisp on any DPR.
One war story: because Cloudflare Pages serves pages at trailing-slash URLs, the client router saw /foo/ while my lookup table keyed on /foo - so pages rendered correctly server-side, then hydrated straight into a 404. Classic "works with JS off, breaks with JS on."
Happy to answer anything about the rendering pipeline or the static-generation setup.
Bug in the "Draw on Graph Paper"/virtual graph paper? Change the grid to hexagon and make sure "snap to grid" is checked. Still snaps to the original square grid.
I have checked your app. The idea is great, but I feel the log-log plots and some scientific and engineering sheets will be helpful. By the way great work.
Although I don't personally use graph paper much, this project looks great. I find many of the example pattern previews very hard to see, though. I'd darken and/or thicken the lines. The descriptions underneath are good, but there are a lot of patterns that are unfamiliar to me that I'd like to be able to preview better, like Cross Stitch and Knitting.
16 comments
[ 4.4 ms ] story [ 51.5 ms ] threadThe interesting part for me was the architecture: a single pure function buildPaper(config) emits geometric primitives (lines / circles / text), and everything is derived from it - the on-screen SVG preview, the in-browser PDF, and the build-time PDF and OG-image generation. No drawing logic is duplicated across output formats, so adding a new paper style is one renderer function plus about five one-line registrations, and it automatically gets a live preview, a downloadable PDF, an OG image, and SEO pages across every paper size.
Everything is true-to-scale: the SVG viewBox and the PDF are both in millimetres, so a 5 mm grid measures exactly 5 mm on paper when you print at 100%.
It's fully static - no backend, no database, no runtime. About 1,900 pages are prerendered to HTML, ~1,700 PDFs are pre-generated at build time (reusing the exact same draw logic as the browser), and it all deploys to a CDN as plain files.
Tech stack:
- Nuxt 4 / Vue 3 (script setup, TypeScript), Nitro static generation (nuxt generate) - Tailwind CSS v4 plus scoped CSS with a small design-token system - Paper engine: one pure buildPaper() -> primitives; one renderer per style. Single source of truth for preview + PDF + images - Preview: pure renderPaperSvg() -> inline SVG baked into the prerendered HTML, so previews render with zero client JS - PDF: jsPDF (dynamic import) for arbitrary custom configs in the browser; a Bun script reuses the same primitives to pre-build the static PDFs - Social images: SVG -> PNG at build time with @resvg/resvg-js (1200x630 OG per style + full A4 sheets) - Data-driven pSEO: ~40 styles across the full ISO A/B/C and US/Imperial sizes -> ~1,900 pages generated from one data table; JSON-LD (WebSite/HowTo/FAQ/Breadcrumb); selective noindex on low-demand size combos so thin pages don't dilute the site - Tooling: Bun (package manager + native-TS runner for all the generators) - Hosting: Cloudflare Pages, pure static (no worker)
There's also a custom editor (tweak spacing, color and margins, then export) and a virtual graph-paper canvas you can draw on, where the logical drawing space is decoupled from the pixel buffer so it stays crisp on any DPR.
One war story: because Cloudflare Pages serves pages at trailing-slash URLs, the client router saw /foo/ while my lookup table keyed on /foo - so pages rendered correctly server-side, then hydrated straight into a 404. Classic "works with JS off, breaks with JS on."
Happy to answer anything about the rendering pipeline or the static-generation setup.
However, on Safari, in the Mac, I don't see any graph on the first two (standard North-South/East-West graph).
I do see the isometric and hexagonal ones, though. Maybe the lines are too thin/light.