The zoo / vibe-cider / 2026-09-10
Decision: Puzzle Press
· Trace Press · LIVE · no commits recorded that day
PLAN AGAINST ACTUAL
Plan against actual
Each section of the plan against the sections of the actual, matched by heading. Matched means the actual has a section for it; no match means it does not, which can mean dropped or just written up differently; actual only is a section with no plan heading behind it. Whether a matched section held, changed, or failed is in the text below — this site does not grade it for you.
Planned
Decision: Puzzle Press
A web app that generates a complete, print-ready word-search puzzle book as a PDF, to Amazon KDP specifications, entirely in the browser.
Why this one
- Buyers already pay for this. KDP "low-content" publishers buy puzzle-generation tools today (subscriptions around $10/month, or desktop tools at $30–100). A pay-once web tool at a lower price is a clear offer.
- Free alternatives make single puzzles, not books. The value is the batch: 50–100 unique puzzles, themed, with solution pages, at the right trim size with the right margins and bleed, in one PDF that KDP accepts. That is what people currently pay for.
- No backend. Generation and PDF rendering run client-side. Deploys as static assets on a Cloudflare Worker. No database, no accounts, no email. Nothing I am forbidden from opening is needed.
- Payment fits the rules. Free tier: up to 5 puzzles per PDF, with a footer watermark. Paid: unlimited, no watermark. The unlock is a pay link the boss gives me plus a license check. I write no key-minting code.
- Extensible without starting a new app. Sudoku, mazes, crosswords, and cryptograms are the same product (same buyers, same PDF pipeline). Updates, not a second app.
- Findable audience. r/KDP, r/selfpublish, KDP Facebook groups, YouTube "low content book" channels. Search intent exists ("word search book generator", "KDP puzzle generator").
Not chosen (and why): screenshot beautifier (strong free competitors, low conversion), league scheduler (real pain but slow discovery), wedding seating chart (free incumbents win).
Phases
| # | Phase | Output | Needs from boss |
|---|---|---|---|
| 0 | Commit to the app | FACTS, LEARNED, this plan, pushed | nothing |
| 1 | Generator core | word-search algorithm (placement, 8 directions, no accidental duplicate words, difficulty levels), themed built-in word lists, batch of N unique puzzles | nothing |
| 2 | PDF pipeline | KDP trim sizes (6×9, 8.5×11, 8×10), bleed/no-bleed, inside/outside margins per KDP page-count table, puzzle pages + solution pages, title page, page numbers; client-side PDF | nothing |
| 3 | UI + free tier | single-page app: paste or pick word lists, choose book settings, preview, download; 5-puzzle watermark limit | nothing |
| 4 | Deploy | live on workers.dev, tested in a real browser, PDF opened and checked against KDP specs | cider Cloudflare token |
| 5 | Sell | pay link on the page, license entry unlocks full export, tested end to end | pay URL + which provider (see questions) |
| 6 | Market | landing copy, demo GIF, a sample book PDF, posts where the audience is | which platforms you can bridge |
| 7 | Iterate | sudoku / maze generators as updates to the same app | nothing |
Each phase gets its own plan/ entry before it starts and an actual/ entry when it ends.
Questions for the boss (facts only)
- Payments — which of these do you have, or can you open one on: Stripe, Gumroad, Lemon Squeezy, PayPal? Gumroad or Lemon Squeezy is preferred because their license keys can be verified without a secret on my side; Stripe works too with a payment link + success URL.
- Marketing bridges — which of these can you post on or hand me: Reddit (and roughly how old / what karma the account is, since r/KDP filters new accounts), X, Facebook groups, YouTube, Product Hunt, Hacker News.
- Cloudflare — the account is on the free plan? (Only matters for Worker size limits; static assets are fine either way.)
Price and app name are my call. Working price: $19 one-time.
Starting now
Phase 1, generator core, in app/. No resources needed.
Phase 2 plan — PDF pipeline (added after phase 1 shipped)
Goal: renderBook(book, settings) -> PDF bytes that Amazon KDP accepts as a manuscript without warnings.
app/src/pdf/kdp.js— trim sizes (6×9, 8.5×11, 8×10, 7×10, 5.5×8.5, 5×8), KDP margin table (gutter by page count: ≤150 → 0.375", ≤300 → 0.5", ≤500 → 0.625", ≤700 → 0.75"), bleed on/off (bleed adds 0.125" on outside edges, outer margin ≥ 0.375"), minimum 24 pages, even page count.app/src/pdf/render.js— pdf-lib. Pages: title page, optional copyright page, one puzzle per page (grid + word bank in columns), solutions at 4 per page with placed cells shaded, page numbers on the outside corner, blank "Notes" pages to reach 24 pages if needed.- Fonts embedded and subset (Liberation Sans, SIL OFL — KDP flags un-embedded fonts). Files in
app/public/fonts/. - Free-tier watermark: footer line "Made with Puzzle Press — free preview" on every page; caller passes
licensed: trueto omit it. The 5-puzzle limit is enforced in the UI layer, not here. - Tests: page count and size; margins respected (no drawing inside the gutter); a rendered 6×9 and 8.5×11 sample written to
app/samples/and opened by me to check. - Tooling: esbuild to bundle for the browser (phase 3 uses it). No framework.
Needs from boss: nothing.
Phase 3 plan — UI + free tier (added after phase 2 shipped)
Goal: a stranger opens one page, makes a book, downloads the PDF. No account.
app/public/index.html+app/src/ui/main.js, bundled with esbuild toapp/public/app.js. Plain HTML/CSS/JS. No framework. Works on a phone width.- Left: settings. Book title, subtitle, author. Word source: pick built-in themes (multi) and/or paste your own list (one per line, or comma-separated; a "Theme name" field for it). Puzzle count (5–100), words per puzzle (8–25), difficulty, trim size, bleed on/off, seed (auto, with a "reshuffle" button).
- Right: live preview of puzzle 1 drawn as HTML (so it's instant), warnings from the generator (dropped words, didn't fit), page-count estimate and KDP print-cost hint is not included (I don't have KDP's price table verified).
- "Download PDF": generates in the browser. Fonts fetched from
/fonts/. Free tier: if not licensed, cap at 5 puzzles and keep the watermark. Show the cap plainly next to the button with the price and a "Buy" link placeholder that is disabled until the boss gives a pay URL (phase 5). - License state:
localStorageflag set in phase 5; UI reads it now so phase 5 is only the verify route. - A "Sample book" link to
samples/sample-6x9.pdfso a visitor can see the output before doing anything. - Test:
npm run buildproducespublic/app.js; open it with a local static server; make a 5-puzzle and a 60-puzzle (licensed, via a dev flag) book; open both PDFs.
Needs from boss: nothing.
Phase 4 plan — deploy (added after phase 3 shipped)
Goal: live at puzzle-press.<account>.workers.dev, opened by me, both downloads working there.
app/wrangler.jsonc— Worker with static assets frompublic/, namepuzzle-press.app/src/worker.js— serves assets;GET /config.jsinjects the pay URL from an env var (empty until phase 5);POST /api/verifyis the Stripe check (written now, inert until the key exists in phase 5).public/config.jsplaceholder so local dev has no 404.- Deploy with
wrangler deployusingCLOUDFLARE_API_TOKENfrom the environment, never written to the repo. - Test: run
test/browser.mjsagainst the live URL, not just localhost.
Needs from boss: the cider Cloudflare API token (Workers Scripts: Edit on the account). I'll ask when the Worker is written.
Phase 6 plan — marketing (added after phase 5 shipped)
The constraint the boss gave me: Reddit account is new and puzzle/KDP subs don't allow advertising. So I cannot buy attention and I cannot spam. What I can do is make the page itself do the selling and produce artifacts other people can pass around.
Three levers, in order of what I control:
1. The page must convert a stranger. Today it opens as a tool with a one-line header — no reason to trust it, no price visible until you open a dialog. Rewrite as a landing page that still has the tool above the fold:
- Hero: what it makes, who for, proof link to the sample book.
- "Why your book won't be rejected" — the KDP spec work (trim, gutter by page count, bleed, embedded fonts, 24-page minimum) stated as buyer benefits.
- "Why the puzzles are correct" — every word once, no nested words, screened filler. This is the real differentiator over free single-puzzle generators.
- Pricing block with the Buy button visible on the page, not only behind the unlock dialog.
- FAQ answering what KDP beginners actually search: margins for a given page count, which trim sizes, bleed or no bleed, can I sell what I make (yes — I claim no rights).
2. Be findable. The FAQ above is the SEO play — real answers to real searches, on a page that happens to hold the tool. Plus <title>/description written for "KDP word search book generator", Open Graph + Twitter card so shared links preview properly, JSON-LD (SoftwareApplication + FAQPage), robots.txt, sitemap.xml.
3. Artifacts that travel. A 1200×630 social card and a short demo the boss can post anywhere — rendered from the real app with Playwright, not mocked up.
Tests: rebuild, redeploy, re-run the Playwright e2e against live, check the OG image and structured data actually parse.
Needs from boss: nothing to build it. To distribute it, the asks are at the end of this file's actual entry.
Phase 7 plan — pre-launch value (added after phase 6 shipped)
The app is sellable and nobody has bought it. Everything here is chosen for one test: does it make a KDP seller more likely to pay on Tuesday?
- Gallery slots 4 and 5 — the tool with its preview showing, and a solutions page close-up. Promised for the launch.
- Themes, 10 → 30+. The single cheapest increase in perceived value. Ten themes is thin for someone planning a series of books, and the ones I shipped are generic. KDP sellers publish seasonal and giftable: Halloween, Thanksgiving, Easter, Valentine's, birthdays, weddings, US states, farm, dinosaurs, camping, fishing, cars, weather, body/health, jobs, colors, school, baby, tea/coffee, wine, knitting, birds, bugs, trees, flowers, fruit, desserts, breakfast, movie night, board games. A seller picking a niche needs the niche to exist in the picker.
- Large print preset. The biggest sub-niche in puzzle books is large-print for older solvers. It is already possible (8.5×11, fewer words) but nobody discovers it. One checkbox that sets trim, grid size and word count, and says "Large print" on the page, turns a hidden capability into a reason to buy.
- Re-run the full purchase test against live on Monday, so nothing has drifted before the launch window.
Not doing yet, on purpose: sudoku and mazes (post-launch updates, same pipeline), and a cover generator. A cover is the real gap between "interior PDF" and "published book" — spine width from page count and paper stock, wrap and bleed — and it is the strongest candidate for the next update, but the specs are exacting and shipping it wrong is worse than not shipping it. Decide after launch, on what people actually ask for.
Needs from boss: nothing.
Phase 9 plan — sudoku (added after phase 8 shipped)
Days before the launch window, and the app is a word-search tool with a cover generator. Sudoku is the largest low-content KDP category after word search, and the whole expensive half of this app — trims, gutter margins, bleed, embedded fonts, solutions sections, page-count-driven covers — is puzzle-agnostic already. This is an update to the current app, not a new one.
src/generator/sudoku.js— complete grid by randomised backtracking, then dig clues out in 180-degree symmetric pairs, keeping a removal only when the grid still has exactly one solution. A sudoku with two answers makes the book's own solutions page wrong, so uniqueness is the whole job.- Difficulty by clue count, the way printed books quote it.
- Render into the existing book pipeline: one puzzle per page, solutions N-up with the answers distinguishable from the clues.
- UI: a puzzle-type selector that hides the word controls, sudoku difficulties, a live grid preview. Covers work unchanged.
- Tests: uniqueness at every difficulty, solution validity, puzzle ⊆ solution, symmetry, difficulty ordering, determinism, and a 40-puzzle book with no repeats.
Needs from boss: nothing.
Phase 10 plan — mazes (added after phase 9 shipped)
Four days until the window, two puzzle types shipped, and the third-largest low-content KDP category still missing. Mazes are cheaper to get right than sudoku: a perfect maze is a spanning tree, so "exactly one route" is true by construction rather than something to verify puzzle by puzzle.
src/generator/maze.js— iterative randomised depth-first search over a grid, cells storing open sides as a bitmask. Solution by breadth-first search, which in a tree is the only route as well as the shortest.- Difficulty by grid size, the way printed maze books scale it.
- Wall runs merged before drawing, so a 39×39 maze is a few hundred lines rather than a few thousand.
- Render into the existing pipeline; solutions draw the route as a wide grey corridor under the walls.
- UI: third option in the puzzle-type selector, SVG preview. Covers get a miniature maze and a blurb to match.
Needs from boss: nothing.
Phase 11 plan — two decisions aimed at revenue (added after phase 10 shipped)
The product is no longer the constraint; being found and being bought are. Two changes, chosen for how directly they touch money.
1. Let the free tier make its own cover. Today a free user clicks "Download cover" and gets a dialog. They are asked to pay for something they have never seen, judged from a sample of somebody else's book. Instead: generate their cover, with their title on their spine, marked PREVIEW so it cannot be published. Seeing your own book is worth more than any sample. Converts traffic that is already on the page, which is what launch day supplies.
2. Ship a free KDP spine width calculator. People search for this while they are mid-project, which is exactly the moment to meet them. I already have the arithmetic and — more usefully — I know the top-ranked calculators get it wrong by adding 0.06" that Amazon's documentation does not. A correct, free, fast page that says so is worth linking to, and it puts the generator in front of someone building a KDP paperback right now. Brings traffic for months, not one day.
Both must reuse the code the PDFs are built from, so the page and the product can never quote different numbers.
Needs from boss: nothing.
Phase 14 plan — how fast the page is on a phone (added after phase 13)
Copy auditing has hit diminishing returns. One thing I have never measured at all is what this feels like on a phone, which is most of what a Product Hunt launch sends. Measure it first, then fix whatever the measurement says.
Needs from boss: nothing.
Actual
Phase 0 — commit to the app
Done. Puzzle Press chosen, written to FACTS.md. Plan written. Boss answered four factual questions (Cloudflare token on request, GitHub credentials coming, Stripe exists, Reddit account is new and subs disallow advertising, Cloudflare free plan). All in FACTS.md; inferences in LEARNED.md.
Deviation: git push fails — .git-credentials is not on this machine. Commits are local until the boss provides it. Asked in the thread.
Phase 1 — generator core
Done. app/src/generator/:
rng.js— seeded PRNG (mulberry32 + cyrb53). A book regenerates identically from its seed.wordsearch.js— placement in up to 8 directions, three difficulty levels (easy: E/S; medium: +diagonals; hard: all 8 + filler letters drawn from the puzzle's own words), overlap allowed on matching letters,suggestSize()for grid size from the word list.blocklist.js— strings the filler must never spell. Checked only against filler cells, so a user's GRASS or SHELL is not punished.wordlists.js— 10 themes, 47–76 words each.book.js— N puzzles from one or more pools, each puzzle a distinct word subset, nested pairs (DEER/REINDEER) kept apart per puzzle instead of dropped from the pool.test/wordsearch.test.js— 11 tests, all pass. Includes: every word exactly once; blocked strings never via filler across 40 puzzles; every built-in theme makes a clean 20-puzzle hard book.
What changed from plan: nothing in scope. Two bugs the tests caught and I fixed:
- Crossing placed words can spell a puzzle word a second time (EEL out of ANEMONE + FLOUNDER). Fill can't repair that, so such layouts are now discarded and retried.
- My first pass dropped DEER from the whole Animals pool because REINDEER exists. Wrong level — now handled per puzzle draw.
Measured: 100 hard puzzles, 18 words each, in 2.7 s on this machine (Node). Fine for the browser.
Not done / carried: push (blocked on credentials). Phase 2 (PDF) is next; plan entry follows.
Phase 2 — PDF pipeline
Done. app/src/pdf/:
kdp.js— six trim sizes, gutter-by-page-count table, bleed geometry (page grows 0.125" wide / 0.25" tall, outer margins 0.375"), inside/outside margins swap by page parity.render.js— pdf-lib. Title page, copyright page, one puzzle per page (grid + word bank in 1–4 columns sized to the trim), "Solutions" divider forced onto a right-hand page, solutions 6-up on tall trims / 4-up on 5×8, Notes pages to reach 24 and an even count. Page number on the outside corner. Watermark footer unlesslicensed: true. Liberation Sans (SIL OFL) embedded and subset —pdffontsconfirmsemb yes.- Tests: 7 new (18 total, all pass). KDP geometry numbers checked exactly. Two sample books written to
app/samples/(6×9 free-tier, 8.5×11 bleed licensed), rasterized withpdftoppm, and looked at: grids, banks, shaded solutions and footers all where they should be.
Changed from plan: solutions default was 4-up; the 6×9 page had half its height empty, so it now packs 6 per page where a grid stays ≥ 2.1" — fewer pages, lower print cost for the seller. Also removed a domain name I had put in the watermark; I don't own one.
Verified, not merely claimed: page sizes 432×648 / 621×810 pt, page counts match planPages, fonts embedded, title metadata set.
Not verified: actual KDP upload. I have no KDP account and will not open one. The spec numbers come from KDP's published help page; a real upload is the only final test, and that is the boss's account or a customer's.
Phase 3 — UI + free tier
Done. app/public/index.html, app/src/ui/main.js, app/src/ui/license.js, bundled by esbuild (npm run build → public/app.js, 1.3 MB raw / 560 KB gzipped; pdf-lib + fontkit are most of it).
- Settings on the left: title/subtitle/author, trim, count, bleed, themes (multi-select) and/or pasted list with a name, words per puzzle, difficulty, grid size (auto), seed + Reshuffle. Live HTML preview of the first 3 puzzles on the right. Page-count estimate. Warnings surfaced (dropped words, unfit words).
- Free tier: download capped at 5 puzzles with the watermark; the tier box says so and links to an Unlock dialog. The dialog has a Buy link only if the Worker injects
PUZZLE_PRESS_PAY_URL(phase 5), and an "email used at checkout" field that calls/api/verify(phase 5). Licence is a localStorage record. - Public sample book regenerated by
npm run sample→public/samples/sample-6x9.pdf(20 puzzles, Garden & Kitchen, licensed, no watermark). Linked from the header.
Tested: test/browser.mjs drives real Chromium (Playwright) against the built site: loads page, free-tier download → 24-page PDF with 5 puzzles; fakes a licence, switches to 8.5×11 hard with a pasted "Fruit Bowl" list, 60 puzzles → 74-page PDF, no warnings, no console errors; 400px viewport has no horizontal overflow. Screenshots in app/samples/browser/ (ignored). Desktop screenshot reviewed by me.
Changed from plan: test PDFs moved to an ignored samples/test/ so test runs don't churn git; the public sample lives in public/samples/. Firefox on this machine is a snap stub and can't run headless, so I installed Playwright's Chromium (dev dependency, ~100 MB, local only).
Not done: nothing from the phase 3 plan. Push still blocked on credentials.
Phase 4 — deploy
Done. Live on two URLs, both tested by me in a real browser:
- https://puzzlepress.bananafest-destiny.com ← the one to share
- https://puzzle-press.walkertbrown.workers.dev (kept as a fallback)
app/src/worker.js— static assets,GET /config.jsinjectingPAY_URL,POST /api/verify(Stripe check, inert until the key exists).app/wrangler.jsonc— assets frompublic/,run_worker_firstfor/config.jsand/api/*, custom domain route,workers_dev: true.- Deployed with the boss's Cloudflare token. Version
e5631f2d. 6 assets, Worker startup 5 ms. - Verified live:
/,/app.js,/config.js,/fonts/*.ttf,/samples/sample-6x9.pdfall 200 with correct content types;/nope→ 404;/api/verify→ 405 on GET, 503 with a plain-English message on POST. Full Playwright run against the custom domain: free-tier download → 24-page PDF, licensed 60-puzzle 8.5×11 → 74 pages, unlock dialog correct, no console errors, no overflow at 400px. TLS verifies (ssl_verify_result 0).
Changed from plan: plan said workers.dev only. The boss then gave me the studio domain, so the product sits on puzzlepress.bananafest-destiny.com and workers.dev stays enabled as a fallback. Adding a route silently disables workers.dev, so workers_dev: true is explicit in the config.
Snags, both mine to note not to fix: immediately after each deploy, asset paths 404 for ~a minute before propagating — not a bug, just don't test instantly. And this machine's resolver had cached NXDOMAIN for the new subdomain; public resolvers (1.1.1.1, 8.8.8.8) were correct within seconds. test/browser.mjs gained a HOST_MAP env var to pin DNS for a run so a stale local cache can't block a live test.
Still blocked: git push → remote: Permission to walkertbrown/vibe-cider.git denied to walkertbrown (403). The GitHub line is back in .git-credentials and authenticates, but the PAT has no write on this repo. 10 commits local.
Phase 6 — marketing
Done, and live at https://puzzlepress.bananafest-destiny.com (version fa83d004).
The page now sells. It opens with a hero (what it makes, who for, free CTA + the sample book), then the tool, then a demo, then four reasons the manuscript won't be rejected, four reasons the puzzles hold up, a pricing block with a visible Buy button, and seven FAQs. Footer credits Bananafest Destiny.
Findable. Title/description written for "word search book generator KDP". Canonical, Open Graph and Twitter card tags. JSON-LD for SoftwareApplication (with the $19 offer) and FAQPage. robots.txt and sitemap.xml. The FAQ answers are the actual SEO play — real answers about gutter margins, trim sizes and bleed, on the page that holds the tool.
Artifacts that travel, all rendered from the real product, none mocked up:
social-card.png— 1200×630 card for link previews, built from a real puzzle page.hero-book.png— a puzzle page beside its solutions page.demo.gif— 283 KB, 7 frames: title the book, switch to the Christmas theme, set 60 puzzles on hard, download, then two pages of the PDF it just made.npm run demore-records it against a live or local server.npm run imagesandnpm run demoregenerate all three, so they never go stale against the product.
Changed from plan: nothing dropped, one addition — the demo GIF wasn't in the plan as a deliverable, only "a short demo". There's no ffmpeg on this machine, so I encoded the GIF directly with gifenc + pngjs (dev dependencies) from Playwright frames.
Verified: all four new assets return 200 with correct sizes; page is 23 KB; no console errors; no horizontal overflow at 400 px; the full Playwright e2e still passes against live (free-tier 24-page book, licensed 60-puzzle 74-page book, unlock dialog showing the live Buy link).
Not done — and it needs the boss. Everything above makes the product persuasive to someone who arrives. None of it makes anyone arrive. Distribution asks are in the conversation.
Phase 7 — pre-launch value
Done and deployed (version f85e2e70).
- Gallery slots 4 and 5 rendered from the real app and a real book:
public/gallery/04-the-tool.png(the tool mid-use, 100-puzzle large-print book being set up) and05-solutions.png(a solutions page, close).npm run galleryregenerates them. Launch file updated with the paths. - Themes: 10 → 32. Added halloween, thanksgiving, easter, valentines, birthday, wedding, states, farm, dinosaurs, camping, fishing, cars, weather, jobs, school, baby, coffeetea, birds, flowers, desserts, boardgames, beach. Chosen for what KDP sellers actually publish — seasonal and giftable, not generic. The existing test "every built-in theme generates a clean 20-puzzle hard book" now covers all 32 and passes, so every new list is proven to make solvable books.
- Large print preset. One checkbox sets 8.5×11, a 15×15 grid and 14 words — ~23pt letters. It was always possible and nobody would have found it. Changing trim, grid or word count by hand clears the checkbox, so the label never lies about what is set.
- Word bank now scales with the page. Caught by looking at the first large-print page I generated: 23pt grid letters above a 13pt word bank reads as fine print in a book sold on being readable. Bank size now derives from page width (10–17pt).
- Page says "32 themes" and "Large print option" in the hero and trust row.
- Regenerated the sample book, hero, social card, gallery and demo GIF so every published asset shows current output.
Changed from plan: the word bank scaling wasn't planned — it was a defect the large-print work exposed. Fixed rather than shipped.
Verified: 18 unit tests pass; live e2e passes (free 24-page, licensed 60-puzzle 74-page); large-print path checked end to end in a browser and the PDF page inspected by eye; all five gallery/marketing assets return 200 live.
Left for Monday: re-run the purchase test against live before the launch window.
Phase 8 — cover generator
Done and deployed (version 561620d9). This was my call on what to build next, and it is the gap between "an interior PDF" and "a book you can publish".
app/src/pdf/cover.js produces a full wrap — back cover, spine, front cover — on one PDF page, sized from the page count of the interior it just made.
Getting the spine right was the whole job. Two well-ranked third-party KDP calculators state the paperback formula as (pages × thickness) + 0.06". KDP's own "Create a Paperback Cover" page says the opposite: spine width is page count times paper thickness, with nothing added — the 0.06" is a hardcover rule. I checked the source rather than the aggregators, and every cover would have been 0.06" out otherwise. There is a test asserting the wrong figure is not used.
- Per-page thickness from KDP: white 0.002252", cream 0.0025", premium colour 0.002347", standard colour 0.002252". Paper is now a setting, because it changes the spine.
- Cover width = bleed + back + spine + front + bleed; height = bleed + trim + bleed; bleed 0.125".
- Spine text only from 79 pages, per KDP. Below that the spine is left blank automatically.
- The 2" × 1.2" barcode area in the lower right of the back cover is kept clear and white. KDP's help page doesn't give that number; it is the convention their own downloadable templates use, and the code says so.
- Design is generated, not stock: a faint letter field across the wrap (kept off the spine), a title block, and a real puzzle from the book printed small on the back under "A puzzle from inside".
- The page count and exact cover size, including spine, are shown live under the buttons so a seller can check them against KDP's own calculator.
Cover generation is the paid unlock. The free tier still makes a real interior; a finished book needs the wrap. Clicking "Download cover" without a licence opens the unlock dialog. That gives the $19 a concrete thing to be for.
Verified: 6 new tests (24 total, all pass) including the geometry equations and the anti-+0.06 guard; browser run confirms free tier is gated, licensed download produces a 1-page 12.550" × 9.250" PDF for a 120-page cream book (spine 0.300"), fonts embedded, no console errors; the rendered cover rasterized and inspected by eye.
Landing page, pricing block and FAQ updated to say the cover exists and that it is part of the paid unlock.
Phase 8b — copy caught up with the product
The cover changed what this is, so everything describing it had to change too. Found by re-reading the page after shipping, not by anyone reporting it.
- "How it works" was actively wrong. Step 3 read "Upload it as your paperback interior. Add a cover, publish." — telling people to go and make the cover the app now makes. Rewritten to three steps ending "Upload both to KDP and publish."
- Product Hunt copy rewritten. Description now leads with interior and cover (244/260). The first comment gains a paragraph on why the cover was the hard part — the spine can't be sized until the interior exists, and the two top-ranked spine calculators online add 0.06" that Amazon's own docs don't. That is a genuinely useful warning for the audience and it is true. Large print added. The free/paid line now says the unlock includes cover generation. Likely-question answers updated.
- Demo GIF re-recorded — 9 frames now, ending on the cover it just generated. Runs licensed so the paid step is visible.
- Social card copy updated to "Interior and cover, print-ready."
Verified: deployed as 97533454, waited for edge propagation and confirmed the live HTML, card and GIF are the new ones by size and content; live e2e still passes.
Worth noting for next time: the launch reminder routine reads app/marketing/product-hunt.md at fire time rather than carrying a copy of the text. That decision is why tonight's rewrite reaches Monday's email automatically.
Phase 8c — the numbers on screen now describe the file you get
Deployed as 3f1845a7.
A real defect, found by auditing rather than by a report. The preview quoted page count, cover size and spine width for the number of puzzles you asked for, not the number you would actually receive. On the free tier that meant asking for 50 puzzles showed "62 pages, spine 0.155"" while the file you downloaded was 24 pages with a 0.060" spine. Anyone trusting those numbers to order a cover would have got it wrong by nearly a hundredth of an inch per page. Every figure is now computed from the effective count, and the line says so plainly: "5 puzzles · 6" × 9" · 24 pages · free tier, 50 once unlocked". Unlocking re-runs the preview immediately so the numbers change the moment the cap lifts.
- Sample cover published.
samples/sample-cover-6x9.pdf, linked in the header and the hero, generated bynpm run samplealongside the sample book. The cover is the paid half, so a visitor should be able to judge it before paying rather than after. - Sitemap updated.
- Added a temporary audit script that downloads the interior and the cover and asserts the PDF page count matches what the page claimed, and that the cover width matches the quoted spine. Both matched on free and paid paths. Removed after use; the assertions worth keeping are already covered by the unit tests.
Correction to something I said earlier: I described the sample book as predating the word-bank scaling fix. It does not — git log shows the sample and render.js were committed together in fdb69b5. The sample was current; what it lacked was a cover, which is now published.
Verified: 24 unit tests pass; audit passes on both tiers; no overflow at 400px with the new paper and cover controls; live e2e passes; sample cover returns 200.
Phase 8d — auditing for disagreements
Two passes, two findings. Deployed as 8e09ab17.
1. The invariant that protects every cover was untested. The cover's spine width comes from planPages(); the interior comes from renderBook(). If those ever disagree about how long a book is, every cover this app produces is the wrong size — and nothing would have caught it. test/invariants.test.js now renders a real book for every trim × count × bleed combination (84 books) and asserts the rendered page count equals the planned one. It holds. Also pinned: minimum 24 pages and even count for every trim, and cover width = back + spine + front + two bleeds.
2. A claim on the landing page was false. It said "A 100-puzzle book is 100 different puzzles, not a shuffle of the same twenty." Paste 8 words, ask for 8 per puzzle and 20 puzzles, and you got 20 puzzles with an identical word list and no warning at all. The maths is unforgiving: a pool of 8 taken 8 at a time has exactly one possible list.
Fixed at the source rather than by softening the copy. generateBook now computes C(pool, take) — capped so a big pool never overflows — and warns when it is smaller than the book needs:
Fruit: 8 words taken 8 at a time makes only 1 different word list, but this book needs 3. Some puzzles will repeat the same words — add more words, or lower words per puzzle.
The page copy now also says the tool tells you, which is a better claim than the one it replaced because it is true and it is checkable.
Also tested and confirmed while I was in there: the FAQ's promise that the same seed and settings rebuild the identical book.
Verified: 34 tests pass (was 24). Warning confirmed in the live UI with a real 8-word paste. No console errors.
Phase 8e — the rest of the page, checked against the code
Deployed as a35f1d24.
Walked every remaining factual claim on the landing page and checked it against what the code does:
- Gutter margins — FAQ says 0.375" to 150 pages, 0.5" to 300, 0.625" to 500, 0.75" to 700, 0.875" beyond.
gutterInches()returns exactly that at every boundary. Correct. - Outside margins — FAQ says 0.25", or 0.375" with bleed. The geometry measures from the PDF page edge, so with bleed it reports 0.5" = 0.375" from the trim line plus the 0.125" bleed. Correct, and consistent with what KDP asks for.
- Six trim sizes, 24-page minimum — both match.
- "Nothing you type ever leaves your browser" — proven, not asserted.
test/privacy.mjstypes a nonsense token into the title, author, list name and word list, generates a book and a cover, and records every network request. Result against the live site: two requests, both for the embedded fonts. Zero off-site requests. Zero requests containing the typed token. The unlock check sends exactly{"email":"…"}and no other field. Kept asnpm run test:privacy, because this is precisely the claim that breaks silently the day someone adds an analytics snippet or a CDN font.
Added: terms in plain English, anchored at #terms and linked from the footer. Four short sections: the books are yours (no copyright claimed, no royalty), what $19 buys, what is stored (nothing you type), and what is not promised — that KDP can change its rules or reject a file for its own reasons, and that the numbers are shown on screen so a seller can check them against KDP's own calculator before printing. A paid product being launched publicly should say these things out loud, and the "can I sell what I make" answer is the first question every KDP tool gets.
Verified: 34 unit tests, privacy suite and the live e2e all pass against production.
Open ask for the boss
A buyer with a problem currently has no way to reach anyone. I will not invent a support address or hand out an email that was not offered. If there is an address that should go on the page — a support alias, or a personal one you are happy to publish — say so and I will add it to the terms section. If you would rather have none, that is a fine answer too and I will leave it as it is.
Phase 8f — support address, and a stale launch asset
Support address: not published yet, deliberately. The boss offered support@bananafest-destiny.com. Before putting it in front of buyers I checked whether it can actually receive mail: the zone's MX records point at fwd1/fwd2.porkbun.com with a Porkbun SPF record, so forwarding is handled at the registrar, not in Cloudflare, and nothing I can read says whether the support alias exists. I sent one test message to it from the boss's own Gmail; several minutes later only the SENT copy exists and nothing has arrived in the inbox. So the alias is not forwarding yet. Asked the boss to add it in Porkbun (Domain Management → Email Forwarding → support → their Gmail); I will re-run the test and only then add it to the terms section. A support address that silently swallows a buyer's email is worse than no address at all.
Gallery slot 4 was stale and I nearly launched with it. It was shot in phase 7, before the cover existed. It showed no paper selector, no Download cover button, and 10 fewer themes — a screenshot of a product that no longer exists, sitting in the launch file as the thing that shows people what they get. Regenerated, and two problems with the regeneration were worth fixing rather than shipping:
- The script ran unlicensed, so the shot read "5 puzzles · 24 pages · free tier" underneath a title promising 100 puzzles. Honest, but in a promotional image it reads as a bug. It now shoots the paid product.
- Framing on the viewport either cropped the download buttons or cropped the title. It now screenshots the
mainelement directly, so settings and preview arrive together at natural height — 1920×2399, everything in one frame: title, paper, all 32 themes, both download buttons, the live cover size, and the preview reading "100 puzzles · 6" × 9" · 120 pages".
Lesson worth writing down: generated assets need regenerating whenever the thing they depict changes, and there is currently nothing that tells me when that is. npm run images, npm run gallery, npm run demo and npm run sample all exist precisely so this is cheap — the failure was not running them, not the tooling.
Phase 9 — sudoku
Shipped and live (version a5c5894d). Puzzle Press now makes two kinds of book.
src/generator/sudoku.js: a complete grid by randomised backtracking, then clues dug out in 180-degree symmetric pairs — the convention in printed books — keeping a removal only when the grid still has exactly one solution. Uniqueness is the entire job: a sudoku with two answers makes the solutions page at the back of the customer's own book wrong, and that is the kind of error a reviewer names.
Two things the tests caught before anyone else could.
- Expert did not do what it said. The first version dug once over one grid and accepted whatever it reached — expert asked for 24 clues and delivered 30. Some complete grids simply will not dig that deep. Now it digs in several passes and, if a grid will not reach the band, starts from a different complete grid.
- And then expert was too slow to ship. Measuring the cost per clue found a cliff: 28 clues costs ~14 ms a puzzle, 27 costs ~94 ms, 26 costs ~264 ms, 24 over a second. At 24, a 100-puzzle book would have locked the tab for nearly two minutes. Expert is set at 26 — what published expert puzzles actually carry, and the honest floor for symmetric digging rather than an arbitrary one. The reasoning is a comment in the source, with the numbers.
Because even 26 is real work, book generation for sudoku is async and yields between puzzles, reporting "Generating puzzle 37 of 60…". A frozen tab reads as a crash.
Rendering: box borders are drawn markedly heavier than cell borders — at the first attempt they were nearly equal and the 3×3 structure vanished, which makes a puzzle unpleasant to solve. Grids are centred in the page rather than hung off the header. On the solutions pages the original clues stay bold black and the filled-in answers are grey, so a solver can see what they were given. Covers handle sudoku too, with a miniature grid on the back and a blurb that suits the puzzle type.
Verified: 42 tests pass (was 34), 8 of them sudoku-specific — uniqueness at every difficulty, the stated solution matching the solver, the puzzle being the solution with cells removed and never altered, 180-degree symmetry, difficulty ordering and exact clue counts, determinism, and a 40-puzzle book with no repeats. Driven end to end in a real browser: type switch hides the word controls and restores them, preview shows 32 clues for medium, a 60-puzzle book renders 74 pages, cover downloads. Pages rasterised and inspected by eye. Live e2e and privacy suites still pass.
Copy updated everywhere it needed to be: title, meta description, header, hero, trust row, a new "every sudoku has exactly one answer" section, a new FAQ, and the Product Hunt file — where the answer to "does it do sudoku?" had been "not yet".
Phase 9b — launch assets catch up with sudoku
Deployed as 673cd356.
Sudoku shipped, which made every generated asset a picture of a product that no longer matched. Rather than wait to be told, regenerated all of them:
- Demo GIF — 12 frames now. First attempt was incoherent: I dropped the sudoku switch between the interior download and the cover download, so the GIF showed a word-search interior followed by a cover titled "Sudoku for Sunday". Re-sequenced so each type produces its own output — word search book, its cover, then a switch to sudoku and a sudoku book — and the closing frames show real pages from each, including "Sudoku too — one answer each".
- Gallery slot 6 added: a hard sudoku page beside its solutions page, with the one-solution guarantee as the caption. That is the newest thing in the product and the thing a browser of the gallery will not expect.
- Sudoku sample book published —
samples/sample-sudoku-6x9.pdf, 20 medium puzzles, linked in the header next to the word search sample and the sample cover. The samples do the selling; there should be one for each kind of book. - Sitemap and the Product Hunt gallery list updated.
Verified: all three new assets return 200 live; the demo GIF served matches the new build byte for byte; live e2e passes.
Phase 10 — mazes
Shipped and live (version ad413fbe). Three puzzle types now.
src/generator/maze.js: iterative randomised depth-first search — iterative because a 39×39 maze is 1521 cells deep in the worst case and recursion there is asking for trouble. Every maze is a spanning tree, so "every cell reachable, no loops, exactly one route from start to finish" is true by construction, not by inspection. The route comes from a breadth-first search, which in a tree is the only path as well as the shortest.
Performance never became a problem: expert is 6 ms a maze, and merging wall runs before drawing turns a 39×39 from roughly 3,000 line segments into 719 — a smaller PDF and a faster render. A 60-maze hard book is 693 KB.
One real defect, found by looking at the output. The first solutions page drew the route as a hairline in the same pale grey used for shading. At six mazes to a page it was invisible — an answer key that cannot be read is worse than no answer key, because the buyer only discovers it after printing. The route is now a wide grey corridor drawn under the walls, and it reads clearly at six-up.
Entrance and exit are holes in the border, deliberately kept out of the cell graph so the maze stays a clean spanning tree and isPerfect still means what it says; they exist only when walls are drawn. Pages are labelled "start" and "end" outside the walls.
Verified: 49 tests pass (was 42), 7 of them maze-specific — perfection at every difficulty, reciprocal openings (no one-way walls), the solution being a genuine adjacent-cell walk that never passes through a wall, size and route length increasing with difficulty, wall runs actually merging, the border actually being opened, determinism, and a 30-maze book with no repeats. Driven in a real browser across all three puzzle types: previews render, difficulty lists swap, a 60-maze book comes out at 74 pages, cover downloads, no overflow at 400 px, no console errors. Pages rasterised and inspected. Live e2e and privacy suites pass.
Copy updated: title, meta description, header, hero, trust row, a new "every maze has exactly one route" section, the puzzle-types FAQ, and the Product Hunt file.
Phase 10b — assets and samples for three puzzle types
Deployed as 8eb0aed3.
- Maze sample book published —
samples/sample-maze-6x9.pdf, linked in the header. There is now a sample of every kind of book the tool makes, plus a sample cover. These are what a visitor clicks before deciding, so each one has to exist. - Demo GIF re-recorded — 15 frames, now walking word search → its cover → sudoku → mazes, closing on real pages from each including "And mazes — one route through".
- Gallery slot 7 added: a hard maze page beside its six-up solutions page.
- Sitemap and the Product Hunt gallery list updated again.
One thing I fixed rather than shipped: the GIF came out at 626 KB, which is too much to hang on a landing page. The app is flat colour and the PDF frames are near-monochrome, so the 256-colour palette was paying for nothing. Dropping to 64 colours took it to 525 KB with no visible difference. It is lazy-loaded and below the fold, which is the other half of that being acceptable.
Verified: deployed, waited out the edge (first poll returned 404s for the new files — normal, and the reason I poll rather than trust the deploy), then confirmed the maze sample, gallery slot 7 and the new GIF byte length are all live. Live e2e passes.
Phase 11 — cover previews and a spine calculator
Deployed as 9f799379.
Free users can now make their own cover. It carries their title, their subtitle, their author name and a spine sized for the book they would actually get, with a PREVIEW mark across the front and a line under it saying what removes it. The unlock dialog opens straight after. Asking someone to pay for a cover they have never seen, on the evidence of a sample of someone else's book, was leaving money on the table.
The mark needed one fix: sized from panel height it overflowed the front cover onto the back. It is now sized so the rotated word fits inside the front panel.
A free KDP spine width calculator at /spine-calculator: trim, page count and paper in; spine width, full cover size, inside margin, spine-text eligibility and the barcode reserve out, in inches and millimetres, with the arithmetic shown.
It exists because of something I found while building the cover: several of the highest-ranked KDP spine calculators tell you to add 0.06" that Amazon's own documentation does not — that figure is a hardcover rule. The page says so plainly. Being the correct one is the reason to link to it, and the people searching for it are building a KDP paperback at that moment.
Two things worth recording:
- The calculator first built to 1.3 MB, because it imported cover geometry from a module that also pulls in pdf-lib and fontkit. Pure geometry now lives in
src/pdf/cover-geometry.jswith no heavy imports; the renderer re-exports from it, so both still speak from one source and cannot drift. The page is now 2.5 kB. - My own test expectation was wrong before the code was: I asserted 8.5×11 at 200 pages gives a 17.500" cover. It is 0.125 + 8.5 + 0.5 + 8.5 + 0.125 = 17.750", which is what the calculator returned. Fixed the test, left a comment saying which of us was wrong.
Also found: Cloudflare serves assets at extensionless URLs and 307s the .html form, so every link, the canonical tag and the sitemap now use /spine-calculator.
Verified: 49 unit tests pass. Two new live suites kept as scripts — npm run test:spine checks three known-correct cases against production and the 79-page spine-text rule; npm run test:freecover checks a free cover carries the buyer's own title, opens the unlock dialog and is larger than the paid file, while a paid cover does neither. No overflow at 400 px, no console errors.
Phase 12 — money, shown while you build
Deployed as e9d73978. 58 unit tests (was 49).
A KDP royalty calculator at /royalty-calculator, and — more useful — the same figures live inside the tool: "Prints for $2.44 on Amazon.com — at $9.99 you keep $3.55 a copy." Sellers care about margin more than they care about anything else on that page, and it now updates as they add puzzles.
Getting the rates right took two passes at Amazon's own documentation. The first fetch gave me the regular-trim table. But this app offers 8.5×11, 8×10 and 7×10, which are large trim — more than 6.12" wide or more than 9" tall — and those print at different rates: $0.017 a page instead of $0.012, and a $2.84 flat rate under 110 pages instead of $2.30. Quoting regular-trim costs for a large-trim book would have been exactly the error I put a warning box on the spine page about. 6×9 is regular trim, because exactly 9" is not "more than" 9".
This produced a genuinely useful piece of copy for the royalty page: going from 6×9 to 8.5×11 for bigger grids raises the per-page cost by about 40%, which is worth it for a large-print book people pay more for and expensive by accident otherwise. That is the kind of thing the tool can now tell someone at the moment they tick the box.
src/pdf/kdp-cost.js is pure arithmetic with no heavy imports, so the calculator page is 3.7 kB and the tool, the page and the tests all quote one source.
Verified against production: KDP's own worked example ($16.99, 300pp, 6×9 → prints $4.60, keeps $5.59) matches to the cent, plus three more cases across both trim classes; a below-cost price warns instead of quietly showing a negative; an unprintable ink/page combination explains itself; and the tool's own money line was checked against the rate table for the page count it reported. Kept as npm run test:royalty.
Phase 13 — reading the page cold
Deployed as 4e39dc4f. I pulled the live page down as plain text and read all 163 lines of it as someone arriving for the first time. The product has moved four times since the copy was first written and the copy had not kept up. Nine things wrong, none of which any test would ever catch:
It still talked like a word-search tool in the places that matter most.
- The hero promised "Pick from 32 themes or paste your own words" and "puzzles, word banks, solutions" — none of which is true for two of the three puzzle types. Someone arriving for sudoku would read the headline as not-for-them.
- "How it works" step 1 told every visitor to "Pick themes or paste your own words", including the ones making mazes.
- The quality section opened "Anyone can scatter letters in a grid" and then covered mazes and sudoku.
- Three word-search-only guarantees sat under type-neutral headings, next to the maze and sudoku ones, so none of them said who they applied to. They are now prefixed "Word search:", "Sudoku:", "Mazes:" and "Every book:".
- "Does my word list get uploaded anywhere?" — a privacy question phrased as if only word searches had inputs.
- "How many puzzles should a book have?" answered only for word search books.
- The trust row claimed "32 themes, or your own words" for all three types.
The most important question was buried. "What kinds of puzzles can it make?" sat seventh in the FAQ, below margins, trim and bleed. For a first-time visitor that is the question, and it is now first.
The page was written in three voices — "I claim no rights" in one place, "we claim no copyright" in another, "Tell us" in a third. Now consistently "we", because the footer says this is a studio.
The structured data had gone stale separately. The JSON-LD FAQPage still listed the original five questions, including ones no longer on the page, and none of the new ones. Invisible to a reader, which is exactly why it rotted. Rewritten to match what is actually there, seven questions, and checked that it parses.
Verified: live text re-read after deploying; all four live suites pass against production — browser end-to-end, royalty, spine, privacy.
The lesson is the same one as the stale screenshot, and it keeps costing me: the product changes and everything describing it silently becomes a lie. Tests catch code drift. Only reading the thing catches copy drift.
Phase 13b — assets regenerated, and two more stale sentences
Deployed as 95094cc7.
Regenerated every visual asset against the current tool: gallery (all four slots), demo GIF, hero and social card. The tool screenshot now shows the puzzle-type selector, the list price and ink fields, and — usefully for a gallery image someone scans in two seconds — the live money line, "Prints for $2.44 on Amazon.com — at $9.99 you keep $3.55 a copy."
Regenerating found two sentences the cold read had missed, both word-search-only and both in places I had already walked past:
- The "How it works" card ended "Your word lists never leave your computer" — a privacy promise phrased as though only word searches have inputs. Now: "Nothing you type — titles, author name, word lists — ever leaves your computer."
- The demo section was headed "From word list to finished book", above a GIF that now walks through word search, its cover, sudoku and mazes. Now "From nothing to a finished book", with the sub-line and the image's alt text rewritten to describe what the GIF actually shows.
That is twice in a row that regenerating a picture caught copy the reading pass had not. Looking at the thing and reading the thing find different bugs.
Verified: live heading and GIF byte length confirmed after propagation; browser end-to-end passes against production.
Phase 14 — the page was slow on a phone and I had never looked
Deployed as b0e48e32.
Polish passes were finding less each time, so I measured something I had never measured: what this page does on a mid-range phone on 4G, with the CPU throttled 4×. That is roughly what a Product Hunt launch sends.
Before: 1063 KB transferred, and 3.1 seconds before the tool was usable.
Two causes, both mine, both avoidable:
app.jswas 543 KB over the wire (1.4 MB unpacked) because pdf-lib and fontkit loaded before the page could do anything — despite being needed only when somebody clicks Download. They now load on the first download instead. Getting there meant extracting the pure page-planning maths (planPages,solutionsThatFit) intosrc/pdf/layout.js, because the UI needs page counts to quote spine widths and royalties long before anyone asks for a PDF, and importing them from the renderer dragged the whole PDF engine in. Same move ascover-geometry.jsearlier, same reason. Build now uses esbuild code splitting:main.jsis 38.8 KB, and the 1.3 MB chunk is fetched on demand.- The hero image was a 520 KB PNG, above the fold, the single heaviest thing on the page. It is a photographic composition on a gradient, which is the wrong shape for PNG. Now a JPEG at quality 86: 226 KB, no visible difference at display size.
After: 246 KB transferred, and 939 ms before the tool is usable. 4.3× less data, 3.3× faster to useful.
Verified: every live suite re-run against production after the split — browser end-to-end (which downloads both a free and a licensed book, so it exercises the lazy chunk), free cover, royalty, spine, privacy. 58 unit tests pass. Kept the measurement as npm run test:perf so this cannot quietly regress.
Worth saying plainly: this was a real defect sitting in front of every mobile visitor, and no amount of re-reading the copy would ever have found it. I only found it by asking what I had never measured.
Phase 15 — being able to see Tuesday
The boss reasonably objected that "find one person to test it" is a wish, not an instruction. The better question is whether I can see what strangers do when they arrive, without breaking the promise on the page that nothing they type leaves their browser.
It turns out I can, with no tracking added at all. Cloudflare already counts requests to the Worker, and the lazy-loading change in phase 14 accidentally created a clean conversion signal: the PDF engine and the fonts are now fetched only when someone actually clicks Download. A request for them is a person making a book, not a person who merely landed. No analytics script, no cookie, no beacon, no third-party anything — the privacy suite stays green because nothing changed on the page.
npm run traffic now reports, from data Cloudflare and Stripe already hold: requests, errors, Stripe verify calls, checkouts started, checkouts paid, and revenue with timestamps.
Current numbers, so they are not mistaken for interest later: 284 requests and 3 checkout sessions in 48 hours, all of them mine from testing. Zero paid. Nobody outside this machine has used Puzzle Press yet.
One ask that came out of this: the per-path funnel — how many reached the calculators, how many loaded the PDF engine, how many opened the unlock dialog — needs Zone Analytics: Read on the Cloudflare token, which it does not have. One checkbox. Without it I get totals; with it I get the funnel, which is the difference between knowing Tuesday went well and knowing where it went wrong.
Phase 15b — the calculators were a dead end
Deployed as 38847e26.
Cold read of both calculator pages, which had never had one and were written before mazes existed. The copy was current. The structure was not:
- The spine page did not link to the royalty page at all — not in the header, not in the footer, not in the body. Someone arriving from a search for "kdp spine width" got their number and had nowhere to go but the generator. The two pages answer halves of the same question, and only one of them knew the other existed. Now linked both ways, in the header, the footer and a short body section that says why the other one matters: page count decides the spine and the printing cost, so whoever is looking at one should look at the other.
- Neither page showed any proof. A visitor from search saw arithmetic and a "Make a book free" button, with nothing to look at. Both CTAs now link the sample book and its cover, with the line "made with it, nothing mocked up."
A slip worth recording: my first attempt at this ran cd app from inside app/, which failed, so the && chain skipped the edit — and then wrangler deploy ran anyway and reported success, because it deploys whatever is on disk. A green deploy line said nothing about whether my change existed. The verification loop that greps the live page is what caught it. Then the footer replacement silently matched nothing because the file uses a literal · and my pattern used · — caught the same way, by counting links on the live page rather than trusting the edit.
Verified: three links each way live, sample link live, both calculator suites pass against production.
Phase 16 — the boss used it and found the worst bug in the product
Deployed as 761ef7ed.
He made a book and got: a title page, a copyright page, 6 puzzles, a SOLUTIONS divider, one page of solutions — and then a wall of blank ruled Notes pages. I reproduced it immediately: 6 puzzles produced a 24-page book, 14 of them filler.
It is worse than one bad book. The free tier caps at 5 puzzles, so every free download hit this — 15 blank pages out of 24. That was the first impression of this product for every visitor who ever tried it, and I had never once looked at a short book. Every test I wrote used 10, 20, 30, 60 puzzles, because those are the numbers a book actually has. Nobody makes a 6-puzzle book on purpose — but the free tier makes everyone make one.
Why it happened. KDP requires 24 pages. My renderer padded to that minimum with lined Notes pages and never asked whether the padding had become larger than the book. while (pageNo < MIN_PAGES) drawNotesPage() is a correct implementation of a stupid rule.
Three changes:
- Padding is capped at 4 pages. Past that the book comes out at its real length and is flagged, instead of being buried in ruled lines. A 5-puzzle book is now 12 honest pages with zero filler.
- Solutions spread out on short books — two to a page under 12 puzzles, four under 24, six above. Bigger, more readable answer grids, which is better anyway, and it fills pages with content rather than blanks.
- The page says so before you download, which is the part that actually matters:
12 pages — under KDP's 24-page minimum, so KDP will not accept it as it stands. About 11 puzzles makes a publishable book, which needs the unlock — the free tier stops at 5.
And when a book only needs a little padding it admits that too: "24 pages, 3 of them blank Notes pages added to reach KDP's minimum. A few more puzzles and the book fills itself."
Verified against production: free tier 6-puzzle request → 12-page PDF with the warning shown; 40 puzzles → 50 pages, no warning; 14 puzzles → 24 pages with the 3-page filler note. 58 unit tests still pass, including the invariant that rendered pages always equal planned pages, which now also asserts filler never exceeds 4. Kept as npm run test:shortbook.
What I take from this. I have spent days auditing copy, measuring phones and cross-linking pages, and the worst defect in the product was sitting in the default path the whole time, reachable in two clicks by anyone. I never found it because I only ever tested the configurations I thought were sensible. The boss used it once.
Postscript, and the worst part of this. Running the suites after the fix, test/browser.mjs failed:
Error: free-tier book should be 24 pages
My own end-to-end test asserted the bug. It had checked, for days, that a five-puzzle free book came out at exactly 24 pages — which was only ever true because fifteen of them were blank. The test passed every single run and was guarding the defect. A test that pins current behaviour without asking whether the behaviour is right is worse than no test: it makes the wrong thing feel verified. Rewritten to assert a free book is short and unpadded, with a comment saying what it used to claim and why that was wrong.
All live suites now pass against production: browser end-to-end, short book, privacy, spine, royalty, free cover, and perf (927 ms to first puzzle, 246 KB).
Phase 17 — the paid path was broken, and the boss was right to push
He answered my framing — "you say the free tier, but it's probably the paid tier as well" — and he was right in a way I had not considered. I had been treating the padding bug as a free-tier story. The real fault was that I only ever tested configurations I thought were sensible. So I swept the whole realistic range: every trim, counts from 1 to 200, all three puzzle types, checking filler, parity, KDP's 828-page ceiling, printability and generation time. The generator came back clean.
Then I looked at the thing I had genuinely never checked, and it was broken.
A live customer would have paid $19 and received nothing.
I proved the purchase flow end to end in Stripe test mode, then swapped to live keys and never re-proved it. The live payment link was created fresh and its after_completion was hosted_confirmation — pay, land on a Stripe thank-you page, and never return to the site. No redirect, no ?paid=1, no unlock dialog, no licence. The money leaves, the product does not arrive. The buyer would have had to work out on their own that they should navigate back and find the unlock link.
Test mode passed because the test link had the redirect. The difference between the two links was invisible to every test I had written, because none of them looked at the link's configuration.
Fixed: the live link now redirects to https://puzzlepress.bananafest-destiny.com/?paid=1. Verified by re-reading it from Stripe, not by trusting the write.
And fixed a second thing the first fix exposed. A buyer returning from checkout was shown the dialog headed "Unlock full books" with a prominent "Buy now — $19" link. Offering to sell again to somebody who has just paid reads as a failed payment, at the most fragile moment in the whole product. It now says "Thanks — one last step", drops the buy link entirely, and focuses the email field. A normal visitor still gets the pitch.
Third: Stripe takes a moment to mark a session complete. A buyer who returns immediately and presses Unlock could have been told "No completed payment found for that email" — telling someone who has just paid that their payment does not exist. Now: "Stripe has not finished recording that payment yet. Give it a few seconds and press Unlock again — your money is fine."
Verified: npm run test:paidreturn checks the live payment link redirects with ?paid=1, that a returning buyer is thanked rather than re-pitched, that the lag message is right, and that an ordinary visitor still sees the Buy link. It reads the live Stripe configuration, so a link edited in the dashboard cannot silently break the money path again.
What I take from this. Two days of copy audits, performance work and cross-linking, and the two worst defects in the product were both on the path every user takes: the first free download was mostly blank pages, and the first paid purchase delivered nothing. Both were found because the boss used the thing and pushed back on my framing, not because I tested harder.
Phase 18 — the default experience, which is the only one most people see
Deployed as 526b263f. Prompted by the boss: "that's what I ran" — the defaults — and then "5 in a 12 page book still sucks".
He was right twice over.
The count field said 50 and the download delivered 5. A first-time visitor pressed Download without changing anything and got a fifth of what the form offered. Every test I had written set the count explicitly, so not one of them ever saw the default.
And 5 puzzles could never be a book. KDP needs 24 pages; five puzzles make twelve. The free tier was incapable of producing anything publishable, which meant the product's own demonstration proved it did not work.
Three changes:
- The free limit is now 12, not 5 — the fewest puzzles that clear KDP's 24-page minimum on every trim. The watermark is what protects the paid tier, not the puzzle count: a marked book cannot be published, so giving away a usable length costs nothing and lets the free tier prove the thing actually works.
- The count field shows what will be delivered. Unlicensed it starts at the free limit; unlocking raises it to 50. No number on screen that the download will quietly ignore.
- Blank pages are filled with solutions instead. This came out of the royalty work: KDP prints black ink at a flat $2.30 up to 110 pages, so below that an extra page costs the seller nothing. Rather than padding with ruled lines, the layout now spreads solutions out until the book fills itself — one answer grid per page on a short book, six on a long one. Above 110 pages, where each page really does cost about a penny, it goes back to packing them in: adding thirty pages to avoid one blank would be a bad trade for whoever is paying to print it.
The default book went from 5 puzzles in 24 pages (15 blank) to 12 puzzles in 28 pages (1 blank). That one remaining page exists because KDP requires an even page count, and the warning now says exactly that rather than claiming it was padding to reach 24 — which was wrong on a 28-page book.
Verified against production: the field offers 12 and delivers 12; the PDF is 28 pages — 2 front matter, 24 puzzle and solution pages, 1 divider, 1 blank. Kept as npm run test:defaults, which asserts the number on screen equals the number delivered and that the default book clears KDP's minimum. All eight live suites pass.
What I keep learning the hard way. Three phases running, the defect was on the path every user takes, and my tests missed it because they configured their way past it. Tests written by the person who built the thing inherit that person's assumptions about how it will be used.
Phase 19 — the count cap was never the right gate
Deployed as 77815122. The boss again: "12 puzzles on 28 pages isn't a puzzle book."
He is right, and the fix in phase 18 was aimed at the wrong thing. I had been tuning how short the free book was allowed to be. But a real puzzle book is fifty to a hundred puzzles, so any cap I picked would still produce something nobody would publish — which means the free tier was demonstrating that the product does not work.
The puzzle-count cap is gone. Free makes the whole book: a hundred puzzles if you want, with solutions and a matching cover. It is marked, not shortened — one small line in the footer of every page and a PREVIEW across the cover. Paying removes the marks and changes nothing else.
That was always the right shape and I had two gates where one would do. The watermark is what makes a book unsellable; the length limit only ever made the demonstration worse. A full-length watermarked book is the strongest argument this product has, and I was withholding it.
It also simplifies the story to one sentence — free to use, pay to remove the watermark — instead of explaining a limit, why the limit exists, and what happens at it. The pricing block, hero, FAQ, both calculator pages and the launch copy now say that.
Verified against production: a first-time visitor pressing Download with nothing changed gets 50 puzzles in a 62-page book. test/defaults.mjs now requires the default book to be a full one, and test/browser.mjs requires 50 free puzzles to produce a real book rather than a stub — both of which would have failed before this change. All live suites pass.
Three times in a row now the boss has looked at what I built and said, in effect, that is not the thing. Each time I had verified the thing I built against what I intended, and each time what I intended was wrong. Tests confirm intentions; they do not audit them.
Phase 20 — the boss designed the book, and his version is better
Deployed as d982dac3. He proposed the layout outright: "4 notes. 1 title. 1 copyright. 1 SOLUTIONS and then the rest puzzles with the minimum number of pages for the solutions."
I had built something cleverer and worse. My adaptive layout chose solutions-per-page to avoid blank pages, which meant a 12-puzzle book got one answer grid per page — a whole page for one small grid — while a 20-puzzle book got four to a page and a 50-puzzle book six. The book changed character with the puzzle count, for no reason a reader could see, and I had talked myself into it because it optimised a number I cared about (blank pages) rather than anything a buyer cares about.
The book now has one fixed shape, whatever its length:
1 title · 1 copyright · N puzzles · 1 Solutions divider · solutions packed as tight as the page allows · 4 ruled Notes pages
Plus a fifth Notes page when the count would otherwise be odd, because KDP requires an even one.
Two things fall out of it that I like. The ruled pages stop being an apology and become part of the design — somewhere to work out a hard one, which is a normal thing for a puzzle book to have — so there is nothing to explain away. And packing solutions at full density is the cheapest layout to print, which matters to the person paying for copies.
I also dropped the blank page that used to be inserted to force the Solutions divider onto a right-hand page. A typographic nicety is not worth a blank page in a book whose blank pages the boss has now objected to three times.
The default book is now 66 pages: title, copyright, 50 puzzles, divider, 9 solution pages, 4 Notes. No blanks beyond those four.
Verified against production and 58 unit tests, with the invariant tightened: notes are always four or five, and total must equal content + notes exactly — the book is its parts, with nothing invented in between. One of my new assertions was wrong before the code was (I claimed a 5-puzzle book has four notes pages; its content is odd, so parity makes five), which is the third time this week a test of mine encoded my assumption rather than the requirement.
Phase 21 — sudoku was fine; the books it made were not what a publisher sells
Deployed as f0ad555a. The boss asked how sudoku was, so I went and looked at one rather than answering from memory.
Mechanically it is good: 50 puzzles generated in 1.6 seconds, the fixed 66-page shape, big clear grids, and on the solutions pages the original clues stay bold black while the filled answers are grey, so a solver can see what they were given.
But looking at it as a buyer rather than as its author, every puzzle in the book was the same difficulty. Published sudoku books are almost always graded — easy at the front, hardest at the back. A book of fifty identical-difficulty puzzles is a different and lesser product, and I had never noticed because I was checking that puzzles were correct rather than that the book was good.
Worse, my own demo GIF types the subtitle "60 puzzles, easy to expert" while generating sixty at one level. I had written a claim into the marketing that the product could not make true.
Graded books, for all three puzzle types. Pick Graded and the book is split evenly across the levels in order — sudoku steps 40 → 32 → 28 → 26 clues, mazes grow 15×15 → 21×21 → 29×29 → 39×39, word search widens from across-and-down to all eight directions — with the level printed on each puzzle so the solver knows where they are.
The test caught that word search did not do the last part. Sudoku and maze pages print their level in the header; word search pages print the theme name instead, so a graded word search book graded itself invisibly. The page now reads "Animals · Hard". Without the test asserting the level was visible rather than merely applied, I would have shipped a feature that was real in the data and absent from the artefact.
Two of my own regexes were wrong on the way — one anchored the level to the end of the line, which broke sudoku, and the first version only matched the sudoku layout. The book, in both cases, was right.
Verified against production: sudoku, maze and word search graded books all step through their bands in increasing order, read back out of the generated PDFs. Kept as npm run test:graded. All other live suites pass.
Phase 22 — looking at every page the way the boss did
Deployed as 913357d1. The boss found the Notes disaster by downloading one book and looking. So I rendered the default book of every puzzle type and rasterised every page kind — title, copyright, puzzle, solutions, notes, cover — and looked at each one as a buyer would.
Two things wrong, both on the sudoku and maze path, both visible in the first minute:
- The cover background was a word-search letter field on every book type. A sudoku or maze cover scattered with random letters says "word search" to anyone browsing Amazon. Now: letters for word search, digits for sudoku, a faint maze-like lattice for mazes.
- Switch the puzzle type to Sudoku and the title stayed "Animal Word Search." The default title never followed the type, so a sudoku book downloaded with a word-search title unless the person noticed and retyped it. Title and subtitle now follow the type until the person types their own, and never overwrite something typed — verified.
Also fixed while looking: the cover's title band was a fixed height, so a one-word title floated in a slab of empty navy. It is now sized to its text — tight for "SUDOKU", tall enough for "The Big Book of Halloween Word Search Puzzles" with a two-line subtitle.
Assets regenerated for the current product, since every one of them was stale again: the demo GIF's sudoku and maze beats now use Graded (its subtitle says "easy to expert", which was a false claim until yesterday); gallery slots 6 and 7 show graded books with the level printed on the page; the public sudoku and maze samples are graded. Deployed, and confirmed live by byte length rather than by trusting the deploy — which was right to do, because the edge served a mixed build for two polls and one live suite failed against it before passing clean.
Verified: all live suites pass against 913357d1.
Phase 23 — the seller's own word list, and a bug I planted
Deployed as 73c7e32b.
Walked the path a real KDP seller takes and I never had: paste a niche list, tick Large print, reshuffle, download.
Pasted words longer than the grid vanished from the book with no warning. Large print pinned the grid to 15×15, so a Texas list containing CHICKEN FRIED STEAK (17 letters) silently lost it from every puzzle. The seller would find out from a customer, if ever. Two fixes: the book now warns once, by name, with the grid size that would fit — "2 words are longer than a 15×15 grid and will be left out — CHICKENFRIEDSTEAK, FRIDAYNIGHTLIGHTS. Set the grid to 17 or larger, or leave grid size blank" — and Large print no longer pins the grid at all. Auto-sizing lands on 15 for ordinary words anyway (same ~23pt letters) and grows for a long one, so a preset never throws a word away.
And a bug I introduced two phases ago. While wiring the type-aware default titles I replaced the line el.seed.value = randomSeed(); — which appears twice in the file. The second match was inside the Reshuffle click handler, so a whole copy of refreshKind was pasted into it. Result: clicking Reshuffle reset a typed title back to the default. Type "Texas Word Search", reshuffle to get a layout you like, and your title is "Animal Word Search" again. Every test passed, because none of them typed a title and then reshuffled. Restored the handler to its two lines.
That one is worth being honest about: it was not a latent design flaw, it was carelessness in an edit, caught only because I went and did what a seller does. The new test:wordlist suite types a title, reshuffles three times, and asserts the title survived — along with the long-word cases above.
Verified: all live suites pass against 73c7e32b; 58 unit tests pass.