From 244cad2bf1dc3824217831dd73793bcebcc94c95 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Mon, 5 Oct 2026 18:17:30 +0000 Subject: [PATCH 1/4] Split the PDF build into draft, print and digital scripts build-pdf-print is the KDP interior (final, no cover, no links), build-pdf-digital the finished book for screens (final, cover and links), and build-pdf-draft an advance copy that now requires --reviewer, which replaces REVIEWER_NAME. Each writes its own file in build/, so the print interior and each reviewer's copy no longer overwrite the digital edition. resize-cover reads its page count from the print interior, the file actually uploaded; preview reads the digital edition, which is now the pre-push build. Claude-Session: https://claude.ai/code/session_01KtTPqhYezQFvpkDqQFUUVT --- CLAUDE.md | 6 +-- PRINT-SPECIFICATION.md | 24 +++++----- README.md | 8 ++-- WRITING-PLAN.md | 2 +- package.json | 4 +- scripts/build-pdf.mjs | 95 ++++++++++++++++++++++++++-------------- scripts/preview-page.mjs | 2 +- scripts/resize-cover.mjs | 11 ++--- 8 files changed, 90 insertions(+), 62 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index db441cb5..0b7e6581 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ Three commands, all of which must pass: ``` npm run lint # banned phrases, heading rules, figure and chapter-reference rules node scripts/check-figure-geometry.mjs # renders every SVG in Chromium: text overlap, arrowheads, coin marks -npm run build-pdf # the whole book; prints only the output path on success +npm run build-pdf-digital # the whole book; prints only the output path on success ``` The first two also run in GitHub Actions on every pull request and every push to `main` @@ -16,8 +16,8 @@ does not run there, so run it yourself. The build reads the finished PDF back an quote printed outside the code face, which is the one check on the typographic quotes that does not trust the build's own conversion. -`npm run preview -- ""` renders a single chapter around a phrase, which is faster than -building the book when checking one edit. +`npm run preview -- ""` renders the page of the digital edition carrying a phrase, +which is faster than paging through the book when checking one edit. `node scripts/check-figure-bands.mjs` reports horizontal dead space in the figures — the hole a deletion leaves when the columns around it stay put. It is advisory, not a gate: it over-reports on diff --git a/PRINT-SPECIFICATION.md b/PRINT-SPECIFICATION.md index 6e70af11..a940c56c 100644 --- a/PRINT-SPECIFICATION.md +++ b/PRINT-SPECIFICATION.md @@ -66,7 +66,7 @@ Figure captions and table cells are ragged right for the same reason: they are s ## Running Heads and Folios -The header carries two items: the book's title on the left, the current chapter's title on the right, both 8pt Gelasio italic in grey. That is the conventional pair, and it is all that fits. At this measure a third box wraps both of them onto two lines, which is why the prerelease marker sits in the footer at bottom left instead, with the folio centered. A build carrying `REVIEWER_NAME` fills the third footer box, bottom right, with that name and the short hash of the commit it was built from, so a single leaf out of an advance copy still says whose it is and which revision of the text it shows. The hash carries a `-dirty` suffix when the working tree had uncommitted changes, because that text matches no commit exactly. Unstamped builds leave that box empty. +The header carries two items: the book's title on the left, the current chapter's title on the right, both 8pt Gelasio italic in grey. That is the conventional pair, and it is all that fits. At this measure a third box wraps both of them onto two lines, which is why the prerelease marker sits in the footer at bottom left instead, with the folio centered. A draft, which always carries `--reviewer`, fills the third footer box, bottom right, with that name and the short hash of the commit it was built from, so a single leaf out of an advance copy still says whose it is and which revision of the text it shows. The hash carries a `-dirty` suffix when the working tree had uncommitted changes, because that text matches no commit exactly. Final builds leave that box empty. Headers and folios are suppressed on the cover, the title page, chapter openers (headers only), part titles, and the blank versos paged.js inserts. The reviewer stamp follows the folio: present wherever the folio is, absent from the cover and the blanks. @@ -92,7 +92,7 @@ The drawing is vector throughout and its type is outlined, so the file embeds no **The press cover is a separate export, sized by `npm run resize-cover`.** A file going to KDP wraps the whole book: 0.125in bleed + 7.44in back cover + spine + 7.44in front cover + 0.125in bleed wide, and 9.69 + 0.25in tall. The spine is the interior page count times KDP's per-page thickness (0.002252in for white paper, 0.0025in for cream), so at 308 interior pages on white (309 with the cover) the target is 15.824 x 9.94in with a 0.694in spine. None of that belongs in `assets/cover.pdf`, which is the front cover at trim and nothing else. -`npm run resize-cover` rescales the Figma export of the full wrap, kept at `assets/cover-wrap.pdf` (another path can be passed as an argument, `--pages N` and `--paper white|cream` as options), to that exact size, filling the target and cropping overflow symmetrically into the bleed. Page count is read from the built book unless `--pages` is passed; pass the count of the file actually uploaded as the interior (a `NO_COVER=1` interior is one page shorter than the built book). The script also measures the spine the art was drawn for, by aligning trim heights and taking the width left over after two covers and two bleeds, and warns when that disagrees with the real spine: the art then bleeds onto or short of the true spine edges by half the difference per side, and spine text rides off-center by the same amount. A warning means the rescale is good enough for a proof and the production cover should be re-exported from a Figma frame drawn for the frozen page count (1 px = 1 pt: 15.824 x 9.94in is a 1139 x 716 px frame). +`npm run resize-cover` rescales the Figma export of the full wrap, kept at `assets/cover-wrap.pdf` (another path can be passed as an argument, `--pages N` and `--paper white|cream` as options), to that exact size, filling the target and cropping overflow symmetrically into the bleed. Page count is read from the print interior (`npm run build-pdf-print`), the file actually uploaded, unless `--pages` is passed. The script also measures the spine the art was drawn for, by aligning trim heights and taking the width left over after two covers and two bleeds, and warns when that disagrees with the real spine: the art then bleeds onto or short of the true spine edges by half the difference per side, and spine text rides off-center by the same amount. A warning means the rescale is good enough for a proof and the production cover should be re-exported from a Figma frame drawn for the frozen page count (1 px = 1 pt: 15.824 x 9.94in is a 1139 x 716 px frame). ## US Letter @@ -104,20 +104,18 @@ It is not a book size and is not what gets printed. Letter's margins leave a 6.3 ``` npm install -npm run build-pdf # print size, into build/ -npm run build-pdf -- --size letter # US Letter -NO_COVER=1 npm run build-pdf # interior only -FINAL=1 NO_COVER=1 NO_LINKS=1 npm run build-pdf # the interior uploaded to KDP -FINAL=1 npm run build-pdf # no "prerelease draft" note on the title page or in the footers -REVIEWER_NAME="..." npm run build-pdf # advance copy: name and commit hash on the title page and every folio -npm run build-pdf -- out.pdf # explicit output path -npm run build-pdf -- --help # the switches, from the script itself -npm run preview -- "" # rasterize one page to check it +npm run build-pdf-digital # the finished book for screens: cover and links, no draft markings +npm run build-pdf-print # the interior uploaded to KDP: no cover, no links, no draft markings +npm run build-pdf-draft -- --reviewer "..." # advance copy: name and commit hash on the title page and every folio +npm run build-pdf-draft -- --reviewer "..." --size letter # the same at US Letter +npm run build-pdf-digital -- out.pdf # explicit output path +npm run build-pdf-digital -- --help # the switches, from the script itself +npm run preview -- "" # rasterize one page of the digital edition to check it ``` -An unrecognized switch, a `--size` with no value or an unknown one, and more than one output path all print the usage and exit nonzero, rather than taking the stray argument for a filename. +An unrecognized switch, a `--size` or `--reviewer` with no value or an unknown size, and more than one output path all print the usage and exit nonzero, rather than taking the stray argument for a filename. So does a draft with no `--reviewer`, since a review copy nobody can trace is the one the stamp exists to prevent, and a final build with one, since a review copy is not final. -The switches compose: `NO_COVER=1 npm run build-pdf -- --size letter` is the cover-less Letter build. +Each build writes its own file in `build/`: the digital edition is `Building Financial Software on Solana.pdf`, and every other build adds a label, `(print interior)`, `(draft, )` or `(letter)`, so one build never overwrites another. The three scripts set `FINAL=1`, `NO_COVER=1` and `NO_LINKS=1` for `scripts/build-pdf.mjs`, and those still compose by hand for an unusual combination: `FINAL=1 NO_COVER=1 node scripts/build-pdf.mjs --size letter` is a cover-less final Letter build. **The interior uploaded to KDP carries no links.** A link in a PDF is an annotation laid over the page rather than ink on it, and KDP removes every one it finds, then reports each page it removed one from as "non-printable markup" to be checked: every table of contents page, and every page with a cross-reference or an address. Nothing printed changes when they go, because every address is printed in full after its link text, but the report cannot say so. `NO_LINKS=1` builds the file without them, and without the named destinations the internal links pointed at, so KDP has nothing to remove. Every other build keeps them, for reading on screen. diff --git a/README.md b/README.md index 29a91be2..9624f523 100644 --- a/README.md +++ b/README.md @@ -22,24 +22,24 @@ The book's home is [solanabook.org](https://solanabook.org), which hosts the pub ## Building the PDF -`npm install` then `npm run build-pdf` renders the whole manuscript, in table-of-contents order, as a single-column textbook PDF (serif type, running headers, page numbers, a contents page with page numbers) into `build/`. The page is 7.44 x 9.69in, the KDP standard trim closest in proportion to the 7 x 9.19in size technical books are usually printed at; `npm run build-pdf -- --size letter` builds the same manuscript at US Letter for screen reading and office printers. `npm run resize-cover` sizes the full-wrap cover export (`assets/cover-wrap.pdf`) to KDP's exact requirement, spine width included. [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) records the trim, margins, type, cover math, and the rest of the physical book. Requires a Chromium binary; the script uses `/opt/pw-browsers/chromium` or the `CHROMIUM_PATH` environment variable. +`npm install` then `npm run build-pdf-digital` renders the whole manuscript, in table-of-contents order, as a single-column textbook PDF (serif type, running headers, page numbers, a contents page with page numbers) into `build/`. The page is 7.44 x 9.69in, the KDP standard trim closest in proportion to the 7 x 9.19in size technical books are usually printed at. Three builds share it: `build-pdf-digital` is the finished book for reading on screen, `build-pdf-print` the interior uploaded to KDP, and `build-pdf-draft -- --reviewer ""` an advance copy stamped for one reviewer, which takes `--size letter` for a reviewer printing on an office printer. `npm run resize-cover` sizes the full-wrap cover export (`assets/cover-wrap.pdf`) to KDP's exact requirement, spine width included. [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) records the trim, margins, type, cover math, and the rest of the physical book. Requires a Chromium binary; the script uses `/opt/pw-browsers/chromium` or the `CHROMIUM_PATH` environment variable. ## Preparing for press 1. Build the interior without the cover page, without the clickable links that KDP would otherwise strip as non-printable markup, and without the prerelease draft markings: ``` - FINAL=1 NO_COVER=1 NO_LINKS=1 npm run build-pdf + npm run build-pdf-print ``` 2. Export the full-wrap cover (back, spine and front, with bleed) from Figma as a PDF to `assets/cover-wrap.pdf`. -3. Resize the cover to KDP's exact size. Run this after step 1, because the spine width comes from the page count of the book in `build/`: +3. Resize the cover to KDP's exact size. Run this after step 1, because the spine width comes from the page count of the print interior in `build/`: ``` npm run resize-cover ``` -4. Upload both to Amazon KDP: `build/Building Financial Software on Solana.pdf` as the manuscript, and `build/cover-kdp-pp-white.pdf` as the cover. +4. Upload both to Amazon KDP: `build/Building Financial Software on Solana (print interior).pdf` as the manuscript, and `build/cover-kdp-pp-white.pdf` as the cover. ## Status diff --git a/WRITING-PLAN.md b/WRITING-PLAN.md index bb84132e..2d52dc32 100644 --- a/WRITING-PLAN.md +++ b/WRITING-PLAN.md @@ -11,7 +11,7 @@ How the manuscript gets produced, verified, reviewed, and kept alive. The table - Say what a feed carries. Every feed in this book is a price feed, so the prose says "price feed", never a bare "feed": "three of the four count slots because their feeds stamp each update" made the reader stop to work out what kind of feed it meant. A price feed network is "a price feed network" too. Code identifiers keep their own names (`mock-price-feed`, `InvalidPriceFeed`). `scripts/lint-manuscript.mjs` rejects a bare "feed". - A key pair is two keys, and only its public key is an address: prose never calls a key pair an address or an account, never puts one on or off the curve, and never says a PDA has one. In the figures, an address made from a key pair is an unlabeled `r="6"` dot on the curve with its account's box beside it; it is never an offcurve dot with a caption, and no caption names a key pair. `scripts/lint-manuscript.mjs` fails the prose and the captions, and `scripts/check-figure-geometry.mjs` fails a public-key dot off the curve, a derived-address dot on it, and any dot more than 20 units from an account box. Seven order-book figures once drew the order book and its vaults as offcurve dots captioned `A CLIENT KEYPAIR`. The vaults are now PDAs of the market; the book, which the client must create, has its box above the curve at the top right, beside its dot. - No sentence runs past forty-five words, enforced by `scripts/lint-manuscript.mjs` over prose, list items, and boxouts. The ceiling is deliberately loose, because it is catching a specific failure rather than policing rhythm: every sentence it first caught was a list that had been glued into a paragraph, or two separate claims joined by a colon, and the fix each time was to split at the punctuation already sitting there. The same pass bans a short list of words that have a plain equivalent and carry no meaning of their own here: "utilize", "commence", "prior to", "subsequent to", "in order to", "in the event that", "due to the fact that", "whilst", "amongst", "thus", "hence", "therefore", "approximately", "numerous", and "sufficient". Latin phrases are banned on the same grounds, because each has an English equivalent a reader does not have to translate: "pro rata" (say "in proportion to" or "based on the size of"), "per se", "de facto", "ad hoc", "vice versa", "a priori", "inter alia", "ceteris paribus", "per annum", and the abbreviations "e.g.", "i.e.", "etc.", "viz." and "cf.". Words that only *look* bureaucratic stay, because they are exact in this domain: a transaction **executes**, a supplier earns **yield**, a maker **provides** liquidity, a program signs by **presenting** seeds, and a firm **operates** a venue. "Confirm" is banned as a loose synonym for "check", because `confirmed` is a Solana commitment level and the book teaches the two that remain under Alpenglow. This is the one rule taken from ASD-STE100 Simplified Technical English; the rest of that standard is written for aircraft maintenance procedures, and its bans on direct address, rhetorical questions, and metaphor would flatten a book that teaches with a story. -- An address is written as a markdown link, `[host.tld](https://host.tld)`, never as loose prose, because the PDF build only reaches links. It prints in italic, which is what technical publishing does with an address (O'Reilly's house style and No Starch's) and needs no machine face to tell a reader it is copied exactly; the monospace face stays with `program_id`, where it separates code from the prose around it. Never underlined: underlining is a screen convention and collides with descenders in a serif face. The scheme prints, because it is part of the address and because technical publishing prints one: O'Reilly sets a link as "words (https://host/path)". A leading `www.` does not, being a subdomain nobody types and every browser supplies. The link keeps whatever it was written with, and the link is what a click follows, so write the link text as the host and path alone and let the markdown target carry the full address. A link whose text is words rather than an address gets that address printed after it in parentheses, since nobody can click a printed page. Both are done by `scripts/build-pdf.mjs`; the lint catches an address written as prose. A long address may break across lines, at a slash; what may not break is where it points, so `npm run build-pdf` reads the link annotations back out of the finished PDF and fails unless every line of a broken address carries the whole destination. But an address does not belong in running prose at all: it is a string of punctuation inside a justified measure, and a reader holding the book cannot act on it while reading the sentence anyway. It goes in a footnote, which names the page it points at, so the sentence keeps its words and the address waits at the foot of the page for a reader who wants it. Two things stay inline. `solanabook.org` is short and is the thing its sentence is naming, and the Glossary is reference matter already, whose entries would read oddly with notes hanging off them. +- An address is written as a markdown link, `[host.tld](https://host.tld)`, never as loose prose, because the PDF build only reaches links. It prints in italic, which is what technical publishing does with an address (O'Reilly's house style and No Starch's) and needs no machine face to tell a reader it is copied exactly; the monospace face stays with `program_id`, where it separates code from the prose around it. Never underlined: underlining is a screen convention and collides with descenders in a serif face. The scheme prints, because it is part of the address and because technical publishing prints one: O'Reilly sets a link as "words (https://host/path)". A leading `www.` does not, being a subdomain nobody types and every browser supplies. The link keeps whatever it was written with, and the link is what a click follows, so write the link text as the host and path alone and let the markdown target carry the full address. A link whose text is words rather than an address gets that address printed after it in parentheses, since nobody can click a printed page. Both are done by `scripts/build-pdf.mjs`; the lint catches an address written as prose. A long address may break across lines, at a slash; what may not break is where it points, so the PDF build reads the link annotations back out of the finished PDF and fails unless every line of a broken address carries the whole destination. But an address does not belong in running prose at all: it is a string of punctuation inside a justified measure, and a reader holding the book cannot act on it while reading the sentence anyway. It goes in a footnote, which names the page it points at, so the sentence keeps its words and the address waits at the foot of the page for a reader who wants it. Two things stay inline. `solanabook.org` is short and is the thing its sentence is naming, and the Glossary is reference matter already, whose entries would read oddly with notes hanging off them. - American spelling throughout, which the lint checks in two batches: the "-our" and "-ise" endings, and the doubled-l forms and noun/verb splits British English keeps ("canceling", "modeling", "cancelable", "center", "license"). Identifiers keep whatever the companion repository shipped, so the `Cancelled` enum variant is written capitalized and left alone. Typography in the source is one apostrophe and one set of quotes, both straight, and one italic marker, the asterisk. The figures are typed the same way. The build sets the quotes the way a typeset book carries them, ’ and “ ” in prose, headings, and figure labels, and straight inside code, which is O'Reilly's convention and every other technical publisher's; the lint rejects a curly quote, as a character or an entity, in a chapter or a figure, so that grep and the build only ever meet one form. A single quote is always an apostrophe, never a quotation inside a quotation, and the lint holds the manuscript to that too. Underscore italics are not a second house style: an underscore is a word character, so `\b` never fires against the start of `_Arithmetic of Money_`, and a chapter reference written that way reads clean to the cross-reference check. The lint bans them for that reason. - The publisher is **Quicknode**, one word, that capitalization. The legal entity is **Quiknode, Inc.**, and it appears only where the entity is what is meant: the copyright holder, the liability disclaimer, the trademark owner, and the exclusive right to sell editions. There is no "Quicknode Press" imprint. `scripts/lint-manuscript.mjs` checks the spellings across the manuscript; `LICENSE.md` and `README.md` sit outside its globs and are checked by eye. - Numbers from outside the companion repository (fees, incident losses, market sizes) name their source in the text, link the primary publisher, and say where and when they apply. A figure that contradicts a reader's own experience is the normal case rather than the exception, because a cost that is typical in one country is unheard of in another: state the scope, so a reader whose transfers have all been domestic can check the claim in one click instead of concluding the book made it up. diff --git a/package.json b/package.json index 302e5351..7fcb2989 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "1.0.0", "description": "Manuscript and build tooling for Building Financial Software on Solana", "scripts": { - "build-pdf": "node scripts/build-pdf.mjs", + "build-pdf-digital": "FINAL=1 node scripts/build-pdf.mjs", + "build-pdf-draft": "node scripts/build-pdf.mjs", + "build-pdf-print": "FINAL=1 NO_COVER=1 NO_LINKS=1 node scripts/build-pdf.mjs", "lint": "node scripts/lint-manuscript.mjs", "measure-slot-drift": "node scripts/measure-slot-drift.mjs", "preview": "node scripts/preview-page.mjs", diff --git a/scripts/build-pdf.mjs b/scripts/build-pdf.mjs index 02bd41b9..56b8417f 100644 --- a/scripts/build-pdf.mjs +++ b/scripts/build-pdf.mjs @@ -2,12 +2,14 @@ // Build a technical-book style PDF of the manuscript. // // Reads TABLE-OF-CONTENTS.md to get the chapters in order, converts each -// markdown file to HTML, lays the result out as 6x9in book pages (serif +// markdown file to HTML, lays the result out as book pages at the print trim (serif // type, running headers, page numbers, chapters on fresh pages, a table of // contents with page numbers) using Paged.js, and prints to PDF with the // system Chromium via playwright-core. // -// Usage: node scripts/build-pdf.mjs [output.pdf] +// Usage: npm run build-pdf-draft -- --reviewer "" +// npm run build-pdf-print +// npm run build-pdf-digital import { readFileSync, writeFileSync, mkdtempSync, mkdirSync, existsSync } from "node:fs"; import { execFileSync } from "node:child_process"; @@ -39,21 +41,29 @@ const PAGE_SIZES = { letter: { widthIn: 8.5, heightIn: 11, sideMarginIn: 1.1, topMarginIn: 0.9, bottomMarginIn: 0.95 }, }; -const USAGE = `Usage: npm run build-pdf -- [--size ${Object.keys(PAGE_SIZES).join("|")}] [output.pdf] - - --size Page size; defaults to "print". See PRINT-SPECIFICATION.md. - --help Print this and exit. - -Environment: - NO_COVER=1 Build the interior without the cover page. - NO_LINKS=1 Leave out the clickable links, which paper cannot carry and - KDP strips from an interior as non-printable markup. - FINAL=1 Leave out the prerelease markings: the draft note on the - title page and the marker in every page's footer. - REVIEWER_NAME Stamp as an advance copy for that reviewer: named on the - title page, and in the footer of every page carrying a folio. - The commit the copy was built from is stamped beside the - name, so feedback can be matched to the text it was about.`; +const USAGE = `Usage: npm run build-pdf-draft -- --reviewer "" [--size ${Object.keys(PAGE_SIZES).join("|")}] [output.pdf] + npm run build-pdf-print [-- output.pdf] + npm run build-pdf-digital [-- output.pdf] + + build-pdf-draft An advance review copy, with the prerelease markings. + build-pdf-print The interior uploaded to KDP: final, no cover, no links. + build-pdf-digital The finished book for reading on screen: final, with the + cover and links. + + --reviewer Stamp as an advance copy for that reviewer: named on the + title page, and in the footer of every page carrying a + folio. The commit the copy was built from is stamped + beside the name, so feedback can be matched to the text it + was about. Every draft needs one; a final build refuses it. + --size Page size; defaults to "print". See PRINT-SPECIFICATION.md. + --help Print this and exit. + +Environment, set by the scripts above: + NO_COVER=1 Build the interior without the cover page. + NO_LINKS=1 Leave out the clickable links, which paper cannot carry + and KDP strips from an interior as non-printable markup. + FINAL=1 Leave out the prerelease markings: the draft note on the + title page and the marker in every page's footer.`; const die = (message) => { console.error(`${message}\n\n${USAGE}`); @@ -65,6 +75,7 @@ const die = (message) => { // dropped rather than taken for an output path. const argv = process.argv.slice(2).filter((arg) => arg !== "--"); let sizeName = "print"; +let reviewerName = ""; const positional = []; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; @@ -76,6 +87,11 @@ for (let i = 0; i < argv.length; i++) { } else if (arg === "--size") { if (i + 1 >= argv.length) die("--size needs a value."); sizeName = argv[++i]; + } else if (arg.startsWith("--reviewer=")) { + reviewerName = arg.slice("--reviewer=".length).trim(); + } else if (arg === "--reviewer") { + if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) die("--reviewer needs a name."); + reviewerName = argv[++i].trim(); } else if (arg.startsWith("-")) { // Without this an unrecognized flag becomes the output path, and the build // succeeds having written a file named "--size". @@ -95,9 +111,33 @@ const TOP_MARGIN_IN = PAGE_SIZES[sizeName].topMarginIn; const BOTTOM_MARGIN_IN = PAGE_SIZES[sizeName].bottomMarginIn; const TEXT_WIDTH_IN = +(PAGE_WIDTH_IN - 2 * SIDE_MARGIN_IN).toFixed(2); -// A size the book is not printed at is labelled in the filename, so two builds -// can sit in build/ without one quietly overwriting the other. -const defaultName = sizeName === "print" ? `${TITLE}.pdf` : `${TITLE} (${sizeName}).pdf`; +const isFinal = process.env.FINAL === "1"; +// A reviewer's copy exists to be traced back to its reviewer, so a final build +// carrying one is a contradiction rather than a combination, and a draft +// without one is a copy nobody can trace. +if (isFinal && reviewerName) die("A final build cannot carry --reviewer: a review copy is not final."); +if (!isFinal && !reviewerName) die("A draft is an advance review copy and needs --reviewer \"\"."); + +// Reading and review copies leave the cover off: the art is not what is being +// read, and the interior's page count is what a printer quotes against. +const includeCover = process.env.NO_COVER !== "1"; +// Read here rather than where the links are removed, because it names the file. +const stripLinks = process.env.NO_LINKS === "1"; + +// Every build other than the digital edition at the print trim is labelled in +// its filename, so the print interior, the digital edition and each +// reviewer's copy can sit in build/ without one quietly overwriting another. +// scripts/preview-page.mjs reads the digital edition and +// scripts/resize-cover.mjs the print interior, by these names. +const labels = []; +if (!isFinal) labels.push(`draft, ${reviewerName.replace(/[\\/:*?"<>|]/g, "-")}`); +if (!includeCover && stripLinks) labels.push("print interior"); +else { + if (!includeCover) labels.push("no cover"); + if (stripLinks) labels.push("no links"); +} +if (sizeName !== "print") labels.push(sizeName); +const defaultName = labels.length ? `${TITLE} (${labels.join(", ")}).pdf` : `${TITLE}.pdf`; const outputPath = resolve(positional[0] ?? join(repoRoot, "build", defaultName)); mkdirSync(dirname(outputPath), { recursive: true }); @@ -110,12 +150,6 @@ const escapeHtml = (text) => const escapeCssString = (text) => text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\r\n]+/g, " "); -// Advance review copies are stamped per reviewer: REVIEWER_NAME=... npm run build-pdf -const reviewerName = (process.env.REVIEWER_NAME ?? "").trim(); -const isFinal = process.env.FINAL === "1"; -// A reviewer's copy exists to be traced back to its reviewer, so a final build -// carrying one is a contradiction rather than a combination. -if (isFinal && reviewerName) die("FINAL=1 and REVIEWER_NAME cannot be combined: a review copy is not final."); const buildDate = new Date().toISOString().slice(0, 10); // A reviewer's copy also carries the commit it was built from. The manuscript @@ -140,9 +174,7 @@ const commit = reviewerName ? describeCommit() : ""; const advanceNote = reviewerName ? `Advance review copy for ${escapeHtml(reviewerName)}. Please don't redistribute. Built ${buildDate}${commit ? ` from commit ${escapeHtml(commit)}` : ""}.` - : isFinal - ? "" - : `Prerelease draft, built ${buildDate}.`; + : ""; const draftFooter = isFinal ? "" : `@bottom-left { content: "PRERELEASE DRAFT (${buildDate})\\A NOT FOR DISTRIBUTION"; white-space: pre; font-family: Gelasio, Georgia, serif; font-size: 8pt; letter-spacing: 0.02em; color: #999; }`; @@ -538,10 +570,6 @@ async function rasterizeCover(coverPdfPath) { return dataUrl; } -// Reading and review copies leave the cover off: the art is not what is being -// read, and the interior's page count is what a printer quotes against. -// NO_COVER=1 npm run build-pdf -const includeCover = process.env.NO_COVER !== "1"; const coverPdfPath = join(repoRoot, "assets", "cover.pdf"); let coverContent = " "; if (!includeCover) { @@ -1072,7 +1100,6 @@ for (const pdfPage of pdf.getPages()) { // them out, and the named destinations the internal links pointed at with // them, so there is nothing for KDP to remove. The printed text is the same // either way: an address is printed in full after its link text. -const stripLinks = process.env.NO_LINKS === "1"; if (stripLinks) { for (const pdfPage of pdf.getPages()) pdfPage.node.delete(PDFName.of("Annots")); pdf.catalog.delete(PDFName.of("Dests")); diff --git a/scripts/preview-page.mjs b/scripts/preview-page.mjs index fd5791c2..443ab155 100644 --- a/scripts/preview-page.mjs +++ b/scripts/preview-page.mjs @@ -1,4 +1,4 @@ -// Rasterize a page of the built PDF to PNG, for eyeballing typography and +// Rasterize a page of the digital edition (npm run build-pdf-digital) to PNG, for eyeballing typography and // figure placement. Finds the page by a phrase on it, so page-number drift // between builds never previews the wrong page. // Usage: npm run preview -- "" diff --git a/scripts/resize-cover.mjs b/scripts/resize-cover.mjs index 8a006369..a6f23622 100644 --- a/scripts/resize-cover.mjs +++ b/scripts/resize-cover.mjs @@ -7,7 +7,7 @@ // page count carries a spine of the wrong width even after a perfect rescale. // // Usage: -// npm run resize-cover # assets/cover-wrap.pdf, count from the built book +// npm run resize-cover # assets/cover-wrap.pdf, count from the print interior // npm run resize-cover -- path/to/export.pdf [--pages 257] [--paper white|cream] // // The input is the full wraparound export (back cover, spine, front cover, @@ -52,14 +52,15 @@ if (!existsSync(input)) { process.exit(1); } if (!pages) { - const bookPath = join(repoRoot, "build", "Building Financial Software on Solana.pdf"); + // The interior uploaded to KDP is what the spine wraps, so the count comes + // from the print build rather than the digital one, which carries the cover. + const bookPath = join(repoRoot, "build", "Building Financial Software on Solana (print interior).pdf"); if (!existsSync(bookPath)) { - console.error("Pass --pages N, or build the book first so the count can be read from it."); + console.error("Pass --pages N, or run npm run build-pdf-print first so the count can be read from it."); process.exit(1); } pages = (await PDFDocument.load(readFileSync(bookPath))).getPageCount(); - console.log(`Page count ${pages}, read from the built book. Pass --pages to override,`); - console.log(`and count the file you will actually upload as the interior.`); + console.log(`Page count ${pages}, read from the print interior. Pass --pages to override.`); } const spine = pages * PAGE_THICKNESS[paper]; From f8fdb257757c0fc30b7ab0a9c0f73752b3a72b88 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Mon, 5 Oct 2026 18:24:00 +0000 Subject: [PATCH 2/4] Rename the print build to paperback and the print size to trim "Print" read as both the book Amazon prints and a page run off an office printer. The KDP build is now build-pdf-paperback, writing the paperback interior, and the page size it is laid out at is --size trim. Claude-Session: https://claude.ai/code/session_01KtTPqhYezQFvpkDqQFUUVT --- PRINT-SPECIFICATION.md | 8 ++++---- README.md | 8 ++++---- package.json | 2 +- scripts/build-pdf.mjs | 30 +++++++++++++++--------------- scripts/resize-cover.mjs | 8 ++++---- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/PRINT-SPECIFICATION.md b/PRINT-SPECIFICATION.md index a940c56c..52366cf9 100644 --- a/PRINT-SPECIFICATION.md +++ b/PRINT-SPECIFICATION.md @@ -92,11 +92,11 @@ The drawing is vector throughout and its type is outlined, so the file embeds no **The press cover is a separate export, sized by `npm run resize-cover`.** A file going to KDP wraps the whole book: 0.125in bleed + 7.44in back cover + spine + 7.44in front cover + 0.125in bleed wide, and 9.69 + 0.25in tall. The spine is the interior page count times KDP's per-page thickness (0.002252in for white paper, 0.0025in for cream), so at 308 interior pages on white (309 with the cover) the target is 15.824 x 9.94in with a 0.694in spine. None of that belongs in `assets/cover.pdf`, which is the front cover at trim and nothing else. -`npm run resize-cover` rescales the Figma export of the full wrap, kept at `assets/cover-wrap.pdf` (another path can be passed as an argument, `--pages N` and `--paper white|cream` as options), to that exact size, filling the target and cropping overflow symmetrically into the bleed. Page count is read from the print interior (`npm run build-pdf-print`), the file actually uploaded, unless `--pages` is passed. The script also measures the spine the art was drawn for, by aligning trim heights and taking the width left over after two covers and two bleeds, and warns when that disagrees with the real spine: the art then bleeds onto or short of the true spine edges by half the difference per side, and spine text rides off-center by the same amount. A warning means the rescale is good enough for a proof and the production cover should be re-exported from a Figma frame drawn for the frozen page count (1 px = 1 pt: 15.824 x 9.94in is a 1139 x 716 px frame). +`npm run resize-cover` rescales the Figma export of the full wrap, kept at `assets/cover-wrap.pdf` (another path can be passed as an argument, `--pages N` and `--paper white|cream` as options), to that exact size, filling the target and cropping overflow symmetrically into the bleed. Page count is read from the paperback interior (`npm run build-pdf-paperback`), the file actually uploaded, unless `--pages` is passed. The script also measures the spine the art was drawn for, by aligning trim heights and taking the width left over after two covers and two bleeds, and warns when that disagrees with the real spine: the art then bleeds onto or short of the true spine edges by half the difference per side, and spine text rides off-center by the same amount. A warning means the rescale is good enough for a proof and the production cover should be re-exported from a Figma frame drawn for the frozen page count (1 px = 1 pt: 15.824 x 9.94in is a 1139 x 716 px frame). ## US Letter -`--size letter` builds the same manuscript at 8.5 x 11in with 1.1in side margins, and writes it to a filename carrying the size so it cannot overwrite the print build. It is for reading on screen and for running a draft off an office printer, where Letter is the paper in the tray. +`--size letter` builds the same manuscript at 8.5 x 11in with 1.1in side margins, and writes it to a filename carrying the size so it cannot overwrite a build at the trim. It is for reading on screen and for running a draft off an office printer, where Letter is the paper in the tray. It is not a book size and is not what gets printed. Letter's margins leave a 6.3in measure, about 100 characters of 10pt type, which is longer than a reader tracks comfortably, and print on demand costs more per unit at that trim. The page count differs too: 267 pages against 309, because the larger page carries more. @@ -105,7 +105,7 @@ It is not a book size and is not what gets printed. Letter's margins leave a 6.3 ``` npm install npm run build-pdf-digital # the finished book for screens: cover and links, no draft markings -npm run build-pdf-print # the interior uploaded to KDP: no cover, no links, no draft markings +npm run build-pdf-paperback # the interior uploaded to KDP: no cover, no links, no draft markings npm run build-pdf-draft -- --reviewer "..." # advance copy: name and commit hash on the title page and every folio npm run build-pdf-draft -- --reviewer "..." --size letter # the same at US Letter npm run build-pdf-digital -- out.pdf # explicit output path @@ -115,7 +115,7 @@ npm run preview -- "" # rasterize one page of the digital e An unrecognized switch, a `--size` or `--reviewer` with no value or an unknown size, and more than one output path all print the usage and exit nonzero, rather than taking the stray argument for a filename. So does a draft with no `--reviewer`, since a review copy nobody can trace is the one the stamp exists to prevent, and a final build with one, since a review copy is not final. -Each build writes its own file in `build/`: the digital edition is `Building Financial Software on Solana.pdf`, and every other build adds a label, `(print interior)`, `(draft, )` or `(letter)`, so one build never overwrites another. The three scripts set `FINAL=1`, `NO_COVER=1` and `NO_LINKS=1` for `scripts/build-pdf.mjs`, and those still compose by hand for an unusual combination: `FINAL=1 NO_COVER=1 node scripts/build-pdf.mjs --size letter` is a cover-less final Letter build. +Each build writes its own file in `build/`: the digital edition is `Building Financial Software on Solana.pdf`, and every other build adds a label, `(paperback interior)`, `(draft, )` or `(letter)`, so one build never overwrites another. The three scripts set `FINAL=1`, `NO_COVER=1` and `NO_LINKS=1` for `scripts/build-pdf.mjs`, and those still compose by hand for an unusual combination: `FINAL=1 NO_COVER=1 node scripts/build-pdf.mjs --size letter` is a cover-less final Letter build. **The interior uploaded to KDP carries no links.** A link in a PDF is an annotation laid over the page rather than ink on it, and KDP removes every one it finds, then reports each page it removed one from as "non-printable markup" to be checked: every table of contents page, and every page with a cross-reference or an address. Nothing printed changes when they go, because every address is printed in full after its link text, but the report cannot say so. `NO_LINKS=1` builds the file without them, and without the named destinations the internal links pointed at, so KDP has nothing to remove. Every other build keeps them, for reading on screen. diff --git a/README.md b/README.md index 9624f523..e579f893 100644 --- a/README.md +++ b/README.md @@ -22,24 +22,24 @@ The book's home is [solanabook.org](https://solanabook.org), which hosts the pub ## Building the PDF -`npm install` then `npm run build-pdf-digital` renders the whole manuscript, in table-of-contents order, as a single-column textbook PDF (serif type, running headers, page numbers, a contents page with page numbers) into `build/`. The page is 7.44 x 9.69in, the KDP standard trim closest in proportion to the 7 x 9.19in size technical books are usually printed at. Three builds share it: `build-pdf-digital` is the finished book for reading on screen, `build-pdf-print` the interior uploaded to KDP, and `build-pdf-draft -- --reviewer ""` an advance copy stamped for one reviewer, which takes `--size letter` for a reviewer printing on an office printer. `npm run resize-cover` sizes the full-wrap cover export (`assets/cover-wrap.pdf`) to KDP's exact requirement, spine width included. [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) records the trim, margins, type, cover math, and the rest of the physical book. Requires a Chromium binary; the script uses `/opt/pw-browsers/chromium` or the `CHROMIUM_PATH` environment variable. +`npm install` then `npm run build-pdf-digital` renders the whole manuscript, in table-of-contents order, as a single-column textbook PDF (serif type, running headers, page numbers, a contents page with page numbers) into `build/`. The page is 7.44 x 9.69in, the KDP standard trim closest in proportion to the 7 x 9.19in size technical books are usually printed at. Three builds share it: `build-pdf-digital` is the finished book for reading on screen, `build-pdf-paperback` the interior uploaded to KDP, and `build-pdf-draft -- --reviewer ""` an advance copy stamped for one reviewer, which takes `--size letter` for a reviewer printing on an office printer. `npm run resize-cover` sizes the full-wrap cover export (`assets/cover-wrap.pdf`) to KDP's exact requirement, spine width included. [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) records the trim, margins, type, cover math, and the rest of the physical book. Requires a Chromium binary; the script uses `/opt/pw-browsers/chromium` or the `CHROMIUM_PATH` environment variable. ## Preparing for press 1. Build the interior without the cover page, without the clickable links that KDP would otherwise strip as non-printable markup, and without the prerelease draft markings: ``` - npm run build-pdf-print + npm run build-pdf-paperback ``` 2. Export the full-wrap cover (back, spine and front, with bleed) from Figma as a PDF to `assets/cover-wrap.pdf`. -3. Resize the cover to KDP's exact size. Run this after step 1, because the spine width comes from the page count of the print interior in `build/`: +3. Resize the cover to KDP's exact size. Run this after step 1, because the spine width comes from the page count of the paperback interior in `build/`: ``` npm run resize-cover ``` -4. Upload both to Amazon KDP: `build/Building Financial Software on Solana (print interior).pdf` as the manuscript, and `build/cover-kdp-pp-white.pdf` as the cover. +4. Upload both to Amazon KDP: `build/Building Financial Software on Solana (paperback interior).pdf` as the manuscript, and `build/cover-kdp-pp-white.pdf` as the cover. ## Status diff --git a/package.json b/package.json index 7fcb2989..749fd10b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build-pdf-digital": "FINAL=1 node scripts/build-pdf.mjs", "build-pdf-draft": "node scripts/build-pdf.mjs", - "build-pdf-print": "FINAL=1 NO_COVER=1 NO_LINKS=1 node scripts/build-pdf.mjs", + "build-pdf-paperback": "FINAL=1 NO_COVER=1 NO_LINKS=1 node scripts/build-pdf.mjs", "lint": "node scripts/lint-manuscript.mjs", "measure-slot-drift": "node scripts/measure-slot-drift.mjs", "preview": "node scripts/preview-page.mjs", diff --git a/scripts/build-pdf.mjs b/scripts/build-pdf.mjs index 56b8417f..cf464ce4 100644 --- a/scripts/build-pdf.mjs +++ b/scripts/build-pdf.mjs @@ -8,7 +8,7 @@ // system Chromium via playwright-core. // // Usage: npm run build-pdf-draft -- --reviewer "" -// npm run build-pdf-print +// npm run build-pdf-paperback // npm run build-pdf-digital import { readFileSync, writeFileSync, mkdtempSync, mkdirSync, existsSync } from "node:fs"; @@ -25,7 +25,7 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const TITLE = "Building Financial Software on Solana"; const AUTHOR = "Mike MacCana"; -// Page sizes, in inches. "print" is what the book is printed at: 7.44 x 9.69 +// Page sizes, in inches. "trim" is what the book is printed at: 7.44 x 9.69 // is the KDP standard trim closest in proportion to the 7 x 9.19 technical // books are usually printed at, which KDP does not offer. The margins follow // O'Reilly's: a 1in side margin clears KDP's 0.75in gutter minimum for books @@ -37,25 +37,25 @@ const AUTHOR = "Mike MacCana"; // measure down to 6.3in, about 100 characters, which is longer than a reader // tracks comfortably. Every page dimension is derived from the chosen size. const PAGE_SIZES = { - print: { widthIn: 7.44, heightIn: 9.69, sideMarginIn: 1, topMarginIn: 0.8, bottomMarginIn: 1 }, + trim: { widthIn: 7.44, heightIn: 9.69, sideMarginIn: 1, topMarginIn: 0.8, bottomMarginIn: 1 }, letter: { widthIn: 8.5, heightIn: 11, sideMarginIn: 1.1, topMarginIn: 0.9, bottomMarginIn: 0.95 }, }; const USAGE = `Usage: npm run build-pdf-draft -- --reviewer "" [--size ${Object.keys(PAGE_SIZES).join("|")}] [output.pdf] - npm run build-pdf-print [-- output.pdf] - npm run build-pdf-digital [-- output.pdf] + npm run build-pdf-paperback [-- output.pdf] + npm run build-pdf-digital [-- output.pdf] - build-pdf-draft An advance review copy, with the prerelease markings. - build-pdf-print The interior uploaded to KDP: final, no cover, no links. - build-pdf-digital The finished book for reading on screen: final, with the - cover and links. + build-pdf-draft An advance review copy, with the prerelease markings. + build-pdf-paperback The interior uploaded to KDP: final, no cover, no links. + build-pdf-digital The finished book for reading on screen: final, with the + cover and links. --reviewer Stamp as an advance copy for that reviewer: named on the title page, and in the footer of every page carrying a folio. The commit the copy was built from is stamped beside the name, so feedback can be matched to the text it was about. Every draft needs one; a final build refuses it. - --size Page size; defaults to "print". See PRINT-SPECIFICATION.md. + --size Page size; defaults to "trim". See PRINT-SPECIFICATION.md. --help Print this and exit. Environment, set by the scripts above: @@ -74,7 +74,7 @@ const die = (message) => { // forward the separator itself has changed over the years, so a bare "--" is // dropped rather than taken for an output path. const argv = process.argv.slice(2).filter((arg) => arg !== "--"); -let sizeName = "print"; +let sizeName = "trim"; let reviewerName = ""; const positional = []; for (let i = 0; i < argv.length; i++) { @@ -125,18 +125,18 @@ const includeCover = process.env.NO_COVER !== "1"; const stripLinks = process.env.NO_LINKS === "1"; // Every build other than the digital edition at the print trim is labelled in -// its filename, so the print interior, the digital edition and each +// its filename, so the paperback interior, the digital edition and each // reviewer's copy can sit in build/ without one quietly overwriting another. // scripts/preview-page.mjs reads the digital edition and -// scripts/resize-cover.mjs the print interior, by these names. +// scripts/resize-cover.mjs the paperback interior, by these names. const labels = []; if (!isFinal) labels.push(`draft, ${reviewerName.replace(/[\\/:*?"<>|]/g, "-")}`); -if (!includeCover && stripLinks) labels.push("print interior"); +if (!includeCover && stripLinks) labels.push("paperback interior"); else { if (!includeCover) labels.push("no cover"); if (stripLinks) labels.push("no links"); } -if (sizeName !== "print") labels.push(sizeName); +if (sizeName !== "trim") labels.push(sizeName); const defaultName = labels.length ? `${TITLE} (${labels.join(", ")}).pdf` : `${TITLE}.pdf`; const outputPath = resolve(positional[0] ?? join(repoRoot, "build", defaultName)); mkdirSync(dirname(outputPath), { recursive: true }); diff --git a/scripts/resize-cover.mjs b/scripts/resize-cover.mjs index a6f23622..661022fb 100644 --- a/scripts/resize-cover.mjs +++ b/scripts/resize-cover.mjs @@ -7,7 +7,7 @@ // page count carries a spine of the wrong width even after a perfect rescale. // // Usage: -// npm run resize-cover # assets/cover-wrap.pdf, count from the print interior +// npm run resize-cover # assets/cover-wrap.pdf, count from the paperback interior // npm run resize-cover -- path/to/export.pdf [--pages 257] [--paper white|cream] // // The input is the full wraparound export (back cover, spine, front cover, @@ -54,13 +54,13 @@ if (!existsSync(input)) { if (!pages) { // The interior uploaded to KDP is what the spine wraps, so the count comes // from the print build rather than the digital one, which carries the cover. - const bookPath = join(repoRoot, "build", "Building Financial Software on Solana (print interior).pdf"); + const bookPath = join(repoRoot, "build", "Building Financial Software on Solana (paperback interior).pdf"); if (!existsSync(bookPath)) { - console.error("Pass --pages N, or run npm run build-pdf-print first so the count can be read from it."); + console.error("Pass --pages N, or run npm run build-pdf-paperback first so the count can be read from it."); process.exit(1); } pages = (await PDFDocument.load(readFileSync(bookPath))).getPageCount(); - console.log(`Page count ${pages}, read from the print interior. Pass --pages to override.`); + console.log(`Page count ${pages}, read from the paperback interior. Pass --pages to override.`); } const spine = pages * PAGE_THICKNESS[paper]; From f6adb7b93f1b01c1ef160796a1efcc64752c148b Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Mon, 5 Oct 2026 18:25:03 +0000 Subject: [PATCH 3/4] Size the cover wrap as part of the paperback build build-pdf-paperback now runs resize-cover straight after the interior, so one command produces both files KDP takes and the spine is always sized from the interior just built. resize-cover still runs alone for --paper cream or --pages. Claude-Session: https://claude.ai/code/session_01KtTPqhYezQFvpkDqQFUUVT --- PRINT-SPECIFICATION.md | 4 ++-- README.md | 12 ++++-------- package.json | 2 +- scripts/build-pdf.mjs | 5 ++++- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/PRINT-SPECIFICATION.md b/PRINT-SPECIFICATION.md index 52366cf9..1b4b5ecb 100644 --- a/PRINT-SPECIFICATION.md +++ b/PRINT-SPECIFICATION.md @@ -92,7 +92,7 @@ The drawing is vector throughout and its type is outlined, so the file embeds no **The press cover is a separate export, sized by `npm run resize-cover`.** A file going to KDP wraps the whole book: 0.125in bleed + 7.44in back cover + spine + 7.44in front cover + 0.125in bleed wide, and 9.69 + 0.25in tall. The spine is the interior page count times KDP's per-page thickness (0.002252in for white paper, 0.0025in for cream), so at 308 interior pages on white (309 with the cover) the target is 15.824 x 9.94in with a 0.694in spine. None of that belongs in `assets/cover.pdf`, which is the front cover at trim and nothing else. -`npm run resize-cover` rescales the Figma export of the full wrap, kept at `assets/cover-wrap.pdf` (another path can be passed as an argument, `--pages N` and `--paper white|cream` as options), to that exact size, filling the target and cropping overflow symmetrically into the bleed. Page count is read from the paperback interior (`npm run build-pdf-paperback`), the file actually uploaded, unless `--pages` is passed. The script also measures the spine the art was drawn for, by aligning trim heights and taking the width left over after two covers and two bleeds, and warns when that disagrees with the real spine: the art then bleeds onto or short of the true spine edges by half the difference per side, and spine text rides off-center by the same amount. A warning means the rescale is good enough for a proof and the production cover should be re-exported from a Figma frame drawn for the frozen page count (1 px = 1 pt: 15.824 x 9.94in is a 1139 x 716 px frame). +`npm run resize-cover` rescales the Figma export of the full wrap, kept at `assets/cover-wrap.pdf` (another path can be passed as an argument, `--pages N` and `--paper white|cream` as options), to that exact size, filling the target and cropping overflow symmetrically into the bleed. Page count is read from the paperback interior, the file actually uploaded, unless `--pages` is passed. `npm run build-pdf-paperback` runs it straight after building that interior, so the two always agree; run it alone to pass `--paper cream` or `--pages`. The script also measures the spine the art was drawn for, by aligning trim heights and taking the width left over after two covers and two bleeds, and warns when that disagrees with the real spine: the art then bleeds onto or short of the true spine edges by half the difference per side, and spine text rides off-center by the same amount. A warning means the rescale is good enough for a proof and the production cover should be re-exported from a Figma frame drawn for the frozen page count (1 px = 1 pt: 15.824 x 9.94in is a 1139 x 716 px frame). ## US Letter @@ -105,7 +105,7 @@ It is not a book size and is not what gets printed. Letter's margins leave a 6.3 ``` npm install npm run build-pdf-digital # the finished book for screens: cover and links, no draft markings -npm run build-pdf-paperback # the interior uploaded to KDP: no cover, no links, no draft markings +npm run build-pdf-paperback # the KDP upload: the interior (no cover, no links, no draft markings), then the cover sized to it npm run build-pdf-draft -- --reviewer "..." # advance copy: name and commit hash on the title page and every folio npm run build-pdf-draft -- --reviewer "..." --size letter # the same at US Letter npm run build-pdf-digital -- out.pdf # explicit output path diff --git a/README.md b/README.md index e579f893..060b6ef6 100644 --- a/README.md +++ b/README.md @@ -26,20 +26,16 @@ The book's home is [solanabook.org](https://solanabook.org), which hosts the pub ## Preparing for press -1. Build the interior without the cover page, without the clickable links that KDP would otherwise strip as non-printable markup, and without the prerelease draft markings: +1. Export the full-wrap cover (back, spine and front, with bleed) from Figma as a PDF to `assets/cover-wrap.pdf`. +2. Build the interior without the cover page, without the clickable links that KDP would otherwise strip as non-printable markup, and without the prerelease draft markings, then resize the cover to KDP's exact size, spine width taken from the interior's page count: ``` npm run build-pdf-paperback ``` -2. Export the full-wrap cover (back, spine and front, with bleed) from Figma as a PDF to `assets/cover-wrap.pdf`. -3. Resize the cover to KDP's exact size. Run this after step 1, because the spine width comes from the page count of the paperback interior in `build/`: + `npm run resize-cover` runs the second half alone, for cream paper (`-- --paper cream`) or a different page count (`-- --pages N`). - ``` - npm run resize-cover - ``` - -4. Upload both to Amazon KDP: `build/Building Financial Software on Solana (paperback interior).pdf` as the manuscript, and `build/cover-kdp-pp-white.pdf` as the cover. +3. Upload both to Amazon KDP: `build/Building Financial Software on Solana (paperback interior).pdf` as the manuscript, and `build/cover-kdp-pp-white.pdf` as the cover. ## Status diff --git a/package.json b/package.json index 749fd10b..2856cf05 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build-pdf-digital": "FINAL=1 node scripts/build-pdf.mjs", "build-pdf-draft": "node scripts/build-pdf.mjs", - "build-pdf-paperback": "FINAL=1 NO_COVER=1 NO_LINKS=1 node scripts/build-pdf.mjs", + "build-pdf-paperback": "FINAL=1 NO_COVER=1 NO_LINKS=1 node scripts/build-pdf.mjs && node scripts/resize-cover.mjs", "lint": "node scripts/lint-manuscript.mjs", "measure-slot-drift": "node scripts/measure-slot-drift.mjs", "preview": "node scripts/preview-page.mjs", diff --git a/scripts/build-pdf.mjs b/scripts/build-pdf.mjs index cf464ce4..428a302e 100644 --- a/scripts/build-pdf.mjs +++ b/scripts/build-pdf.mjs @@ -42,11 +42,14 @@ const PAGE_SIZES = { }; const USAGE = `Usage: npm run build-pdf-draft -- --reviewer "" [--size ${Object.keys(PAGE_SIZES).join("|")}] [output.pdf] - npm run build-pdf-paperback [-- output.pdf] + npm run build-pdf-paperback npm run build-pdf-digital [-- output.pdf] build-pdf-draft An advance review copy, with the prerelease markings. build-pdf-paperback The interior uploaded to KDP: final, no cover, no links. + Then sizes the cover wrap to it (npm run resize-cover). + Takes no arguments: the cover reads the interior from + its default path. build-pdf-digital The finished book for reading on screen: final, with the cover and links. From 19673e2287efee8524acf7647176548022505a11 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Mon, 5 Oct 2026 18:33:58 +0000 Subject: [PATCH 4/4] Remove render-page.mjs Nothing ran or documented it. Rendering a page of a built PDF to PNG is what npm run preview does, finding the page by a phrase rather than a number that drifts between builds. Claude-Session: https://claude.ai/code/session_01KtTPqhYezQFvpkDqQFUUVT --- scripts/render-page.mjs | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 scripts/render-page.mjs diff --git a/scripts/render-page.mjs b/scripts/render-page.mjs deleted file mode 100644 index 4d6202ff..00000000 --- a/scripts/render-page.mjs +++ /dev/null @@ -1,35 +0,0 @@ -// Render one page of a PDF to PNG with pdf.js inside Chromium, to check how -// browser-family viewers display it (system rasterizers can hide viewer bugs). -// Usage: node scripts/render-page.mjs -import { chromium } from "playwright-core"; -import { readFileSync, writeFileSync } from "node:fs"; -import { pathToFileURL } from "node:url"; -import { resolve } from "node:path"; - -const [pdfPath, pageNum, outPath] = process.argv.slice(2); -const pdfB64 = readFileSync(resolve(pdfPath)).toString("base64"); -const pdfjsUrl = pathToFileURL(resolve("node_modules/pdfjs-dist/legacy/build/pdf.mjs")).href; -const workerUrl = pathToFileURL(resolve("node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs")).href; - -const browser = await chromium.launch({ executablePath: "/opt/pw-browsers/chromium", args: ["--allow-file-access-from-files"] }); -const page = await browser.newPage(); -page.on("console", (m) => console.log("console:", m.text())); -page.on("pageerror", (e) => console.error("pageerror:", e.message)); -writeFileSync(resolve("node_modules/.render-page.html"), "render"); -await page.goto(pathToFileURL(resolve("node_modules/.render-page.html")).href); -const dataUrl = await page.evaluate(async ({ pdfjsUrl, workerUrl, pdfB64, pageNum }) => { - const pdfjs = await import(pdfjsUrl); - pdfjs.GlobalWorkerOptions.workerSrc = workerUrl; - const bytes = Uint8Array.from(atob(pdfB64), (c) => c.charCodeAt(0)); - const doc = await pdfjs.getDocument({ data: bytes }).promise; - const p = await doc.getPage(Number(pageNum)); - const viewport = p.getViewport({ scale: 612 / p.getViewport({ scale: 1 }).width }); - const canvas = document.createElement("canvas"); - canvas.width = Math.round(viewport.width); - canvas.height = Math.round(viewport.height); - await p.render({ canvasContext: canvas.getContext("2d"), viewport }).promise; - return canvas.toDataURL("image/png"); -}, { pdfjsUrl, workerUrl, pdfB64, pageNum }); -await browser.close(); -writeFileSync(resolve(outPath), Buffer.from(dataUrl.split(",")[1], "base64")); -console.log("wrote", outPath);