<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hannah Whitfield</title>
    <description>The latest articles on DEV Community by Hannah Whitfield (@hannahjwhitfield).</description>
    <link>https://dev.to/hannahjwhitfield</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3995024%2F9cdfc171-bc98-4b42-9824-3a620c954a24.jpg</url>
      <title>DEV Community: Hannah Whitfield</title>
      <link>https://dev.to/hannahjwhitfield</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hannahjwhitfield"/>
    <language>en</language>
    <item>
      <title>Automatic Page Layout Is a Search Problem</title>
      <dc:creator>Hannah Whitfield</dc:creator>
      <pubDate>Sat, 25 Jul 2026 08:11:24 +0000</pubDate>
      <link>https://dev.to/hannahjwhitfield/automatic-page-layout-is-a-search-problem-10c9</link>
      <guid>https://dev.to/hannahjwhitfield/automatic-page-layout-is-a-search-problem-10c9</guid>
      <description>&lt;p&gt;Every recipe layout system I've seen starts the same way. A designer makes a beautiful two-page spread — photo left, recipe right — then someone tries it with a recipe that has 22 ingredients and 14 steps. The text overflows, and now you have that decision to make automatically, on every page, for a book with sixty of them.&lt;/p&gt;

&lt;p&gt;That decision problem, not PDF rendering, is where print automation lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fitting Is a Search Problem, Not a CSS Problem
&lt;/h2&gt;

&lt;p&gt;The naive approach is to lay out the content and let it flow. For a bound book with facing pages that's exactly wrong: a recipe spilling three lines onto a second page wastes a full leaf and pushes every subsequent spread out of parity, so your left-hand photos end up on the right.&lt;/p&gt;

&lt;p&gt;Treat each recipe instead as a fitting problem with a small set of levers, and search over them. Ours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ingredient list column count (1 or 2)&lt;/li&gt;
&lt;li&gt;body type size, quantized (9.5 / 10 / 10.5 pt — never continuous)&lt;/li&gt;
&lt;li&gt;leading, as a multiple of the baseline grid&lt;/li&gt;
&lt;li&gt;photo size tier (full-bleed / half / thumbnail / none)&lt;/li&gt;
&lt;li&gt;an optional, droppable tip box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then score candidates and take the best fit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fit_recipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;candidate_configs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;     &lt;span class="c1"&gt;# a few dozen combos, cheap
&lt;/span&gt;        &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# height in points
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                          &lt;span class="c1"&gt;# overflow: reject
&lt;/span&gt;        &lt;span class="n"&gt;slack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;
        &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;W_SLACK&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;slack&lt;/span&gt;                        &lt;span class="c1"&gt;# prefer full pages
&lt;/span&gt;              &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;W_SIZE&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;IDEAL_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;W_DENSITY&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;density_penalty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;W_PHOTO&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;photo_penalty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Candidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;fit_recipe_two_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is quantization. Let type size vary continuously and adjacent recipes land at 9.7pt and 10.3pt — the book looks subtly broken in a way readers feel but can't name. Three or four discrete sizes, applied per &lt;em&gt;section&lt;/em&gt; rather than per page, keeps it coherent.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;measure()&lt;/code&gt; is the part everyone underestimates. You cannot approximate text height with &lt;code&gt;characters / chars_per_line&lt;/code&gt;. You need real font metrics and real line breaking, because one long ingredient name can wrap unexpectedly and change block height by a full line. We measure by running the actual layout engine and reading back the height — headless, no rendering, just the box model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Baseline Grid Is What Makes It Look Professional
&lt;/h2&gt;

&lt;p&gt;Amateur book layout has text on facing pages that doesn't line up. Professional layout puts every line of body text on a shared invisible grid, so with the book open the lines on page 40 align with those on page 41 — and text doesn't show through from the reverse side.&lt;/p&gt;

&lt;p&gt;That means every vertical measurement is a whole multiple of the baseline unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--baseline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--baseline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--baseline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--baseline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--baseline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c"&gt;/* total stays whole */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;figure&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;margin-block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--baseline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap is that images and boxes are arbitrary heights and knock everything off grid. Every non-text block needs its height snapped up to the next multiple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also: &lt;code&gt;line-height&lt;/code&gt; positions the em box, not the baseline, so a font with unusual ascent/descent metrics sits off grid even at a correct line-height. Mixing a display face with a body face needs a per-font baseline offset derived from the font's ascender and &lt;code&gt;unitsPerEm&lt;/code&gt;. Invisible when correct, ugly when wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break Control Is Where the Rules Get Domain-Specific
&lt;/h2&gt;

&lt;p&gt;Generic widow and orphan control is table stakes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;p&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;widows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;h3&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;break-after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;avoid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;orphans: 3&lt;/code&gt; means three lines must remain at the bottom of a page before a break; &lt;code&gt;widows: 3&lt;/code&gt; means three must carry over. Two is the common default and too permissive for a cookbook, where one stranded line next to a photo reads as a mistake.&lt;/p&gt;

&lt;p&gt;But recipes have rules generic typography doesn't know about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The ingredient list must never split.&lt;/strong&gt; A cook needs all of it at once. &lt;code&gt;break-inside: avoid&lt;/code&gt; on the whole list — and if it doesn't fit, escalate to two pages rather than break it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A numbered step must not split mid-step.&lt;/strong&gt; After step 4 is fine; inside step 4 is not. So &lt;code&gt;li { break-inside: avoid }&lt;/code&gt; but explicitly &lt;em&gt;not&lt;/em&gt; on the &lt;code&gt;&amp;lt;ol&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yield, time and temperature are atomic.&lt;/strong&gt; Splitting "Bake at 425°F" from "for 20 minutes" across a page turn is a usability failure, not an aesthetic one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A step referencing a photo belongs on that photo's spread.&lt;/strong&gt; Not expressible in CSS at all — it's a hard filter on candidate configurations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more engine behavior: &lt;code&gt;break-inside: avoid&lt;/code&gt; on a block taller than the page silently does nothing, and you get an arbitrary break. Guard it with a measurement check. Wiring these constraints into the fitting search and running it across a full book, so page parity holds end to end, is the layout core of &lt;a href="https://getcookpress.com" rel="noopener noreferrer"&gt;CookPress&lt;/a&gt; — though the constraint that took longest was the boring one below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Incremental Layout Problem
&lt;/h2&gt;

&lt;p&gt;Once a book is composed, editing recipe 12 must not reflow recipes 13 through 60. Otherwise every edit invalidates every proof the author already approved, and review never converges.&lt;/p&gt;

&lt;p&gt;The fix is to anchor. Each recipe gets a fixed page allocation once accepted, and re-fitting happens &lt;em&gt;within&lt;/em&gt; that allocation using the same search, just a tighter frame. If an edit genuinely cannot fit, surface it as a conflict for the author rather than silently repaginating.&lt;/p&gt;

&lt;p&gt;Treating page allocation as immutable state rather than derived output is what made the system usable. It feels wrong — you're giving up global optimality — but a book you can review incrementally beats a tighter book you must re-proof from page one after every typo fix.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>architecture</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Generating Print-Ready PDFs in a Browser Is Harder Than It Looks</title>
      <dc:creator>Hannah Whitfield</dc:creator>
      <pubDate>Fri, 24 Jul 2026 19:21:41 +0000</pubDate>
      <link>https://dev.to/hannahjwhitfield/generating-print-ready-pdfs-in-a-browser-is-harder-than-it-looks-25g3</link>
      <guid>https://dev.to/hannahjwhitfield/generating-print-ready-pdfs-in-a-browser-is-harder-than-it-looks-25g3</guid>
      <description>&lt;p&gt;"Export to PDF" sounds like a solved problem until the output has to go to an actual printer and come back as a physical book. Screen PDFs and print PDFs are different artefacts with different requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  RGB is not CMYK
&lt;/h2&gt;

&lt;p&gt;Browsers work in RGB. Printers work in CMYK. The conversion is lossy and not symmetric — saturated RGB colours, particularly bright blues and greens, have no CMYK equivalent and come back visibly duller.&lt;/p&gt;

&lt;p&gt;Nothing in the browser warns you. The proof looks right on screen and wrong on paper, which is an expensive way to discover the problem.&lt;/p&gt;

&lt;p&gt;Practical mitigation: design within a CMYK-safe palette from the start rather than converting at the end. Muted colours survive the round trip; neon does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bleed and trim
&lt;/h2&gt;

&lt;p&gt;Print requires content to extend past the trim line — typically 3mm on each edge — so that cutting tolerance does not leave white slivers. A "210×297mm" PDF is wrong for print; you need 216×303mm with trim marks.&lt;/p&gt;

&lt;p&gt;CSS gets you part of the way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@page&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;216mm&lt;/span&gt; &lt;span class="m"&gt;303mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* A4 + 3mm bleed each side */&lt;/span&gt;
  &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;@page&lt;/code&gt; support is uneven and bleed marks generally are not something the browser will draw for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image resolution is the most common rejection
&lt;/h2&gt;

&lt;p&gt;Screen images are typically 72-96 DPI. Print needs 300 DPI. An image looking crisp in the browser is roughly a quarter of the resolution a printer wants, and print services reject or flag these.&lt;/p&gt;

&lt;p&gt;Worth validating at upload — compute effective DPI from the intrinsic pixel dimensions against the placed physical size and warn immediately, rather than at export when the user has already done the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination is the genuinely hard part
&lt;/h2&gt;

&lt;p&gt;Reflowable HTML has no concept of a page. Getting a recipe to not break across a page boundary means measuring rendered content and making layout decisions, which the browser will not do for you.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;break-inside: avoid&lt;/code&gt; helps for simple blocks and is not sufficient for real layout control. What actually worked: render content off-screen, measure real heights, then assign blocks to pages in code — essentially reimplementing pagination rather than asking CSS to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fonts must be embedded
&lt;/h2&gt;

&lt;p&gt;An unembedded font substitutes at the print shop and your careful typography becomes Helvetica. Whatever generates the PDF must embed subsets — and the font licence has to permit embedding, which not all do.&lt;/p&gt;

&lt;p&gt;I deal with all of this at &lt;a href="https://getcookpress.com" rel="noopener noreferrer"&gt;Cook Press&lt;/a&gt; — recipes in, Etsy-ready hardcover out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveat
&lt;/h2&gt;

&lt;p&gt;If the output is only ever going to be read on screen, ignore most of this. These constraints exist because a physical printing press is on the other end, and they are pure overhead otherwise.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
