Photobook

Every format, every margin, every page a book can contain, every switch the composer offers, and the rules that choose a shape when nobody has chosen one. Drawn at one pixel per millimetre from the constants the renderer prints from — a book that looks wrong here is wrong on paper, which is very much cheaper to notice.

  • bleedartwork runs to here and is cut away
  • trimthe finished edge
  • contentinside the safe margin and the gutter
  • spinewhat the shelf sees
  • jointthe hinge either side of a hardcover spine

Four sizes, to scale

4 trim sizes · soft and hard covers · one pixel per millimetre

lib/photobook/spec.ts — BOOK_SIZES, sizesFor, productUidFor

Drawn against each other rather than tabulated, because the proportions are the decision: a pocket square and a portrait page are not the same object at different magnifications. The dashed inner rectangle is the content box of a right-hand page — the same margins in millimetres at every size, which is a very different share of the paper on a 140 mm book than on a 280 mm one.

Pocket square 140 × 140 mmsoftcover · gutter 11.4% of the width
Square 200 × 200 mmsoftcover · hardcover · gutter 8.0% of the width
Portrait 210 × 280 mmsoftcover · hardcover · gutter 7.6% of the width
Large square 280 × 280 mmhardcover · gutter 5.7% of the width
SizeTrimMade inFull-bleed photo needs
Pocket square 140 × 140 mmpocket140 × 140 mm✓ softcover— no hardcover1725 px across at 300 dpi
Square 200 × 200 mmsquare200 × 200 mm✓ softcover✓ hardcover2434 px across at 300 dpi
Portrait 210 × 280 mmportrait210 × 280 mm✓ softcover✓ hardcover2552 px across at 300 dpi
Large square 280 × 280 mmlarge-square280 × 280 mm— no softcover✓ hardcover3378 px across at 300 dpi

A size with no entry for a cover type is not offered at all: Gelato binds no 280 mm softcover and no 140 mm board, and productUidFor answers null rather than falling back to another product. Three sizes in each cover, which is why the composer asks for the cover first — the size grid then shows a full set either way.

The Gelato product uid for every size and cover (6 of them)
Pocket square 140 × 140 mm · softcover
photobooks-softcover_pf_140x140-mm-5_5x5_5-inch_pt_170-gsm-65lb-coated-silk_cl_4-4_ccl_4-4_bt_glued-left_ct_matt-lamination_prt_1-0_cpt_250-gsm-100-lb-cover-coated-silk_ver
Square 200 × 200 mm · softcover
photobooks-softcover_pf_200x200-mm-8x8-inch_pt_170-gsm-65lb-coated-silk_cl_4-4_ccl_4-4_bt_glued-left_ct_matt-lamination_prt_1-0_cpt_250-gsm-100-lb-cover-coated-silk_ver
Portrait 210 × 280 mm · softcover
photobooks-softcover_pf_210x280-mm-8x11-inch_pt_170-gsm-65lb-coated-silk_cl_4-4_ccl_4-4_bt_glued-left_ct_matt-lamination_prt_1-0_cpt_250-gsm-100-lb-cover-coated-silk_ver
Square 200 × 200 mm · hardcover
photobooks-hardcover_pf_200x200-mm-8x8-inch_pt_170-gsm-65lb-coated-silk_cl_4-4_ccl_4-4_bt_glued-left_ct_matt-lamination_prt_1-0_cpt_130-gsm-65-lb-cover-coated-silk_ver
Portrait 210 × 280 mm · hardcover
photobooks-hardcover_pf_210x280-mm-8x11-inch_pt_170-gsm-65lb-coated-silk_cl_4-4_ccl_4-4_bt_glued-left_ct_matt-lamination_prt_1-0_cpt_130-gsm-65-lb-cover-coated-silk_ver
Large square 280 × 280 mm · hardcover
photobooks-hardcover_pf_280x280-mm-11x11-inch_pt_170-gsm-65lb-coated-silk_cl_4-4_ccl_4-4_bt_glued-left_ct_matt-lamination_prt_1-0_cpt_130-gsm-65-lb-cover-coated-silk_ver

Copied verbatim from Gelato’s catalogue rather than built by concatenation. The uid this file used to construct was never a real product.

The margins, and the binder's rules

the same for every size · 3 mm bleed · 10 mm safe · 16 mm gutter · 300 dpi

lib/photobook/spec.ts — defaultSpec, GELATO_PAGE_RULE, HERO_FLOOR_DPI

Bleed3 mm on all four edgesartwork runs past the trim and is cut away; TrimBox and BleedBox go on every page
Safe margin10 mm inside the trim, on the outer three edgesnothing that matters goes outside it
Gutter16 mm at the spinewider than the outer margin: a perfect-bound book does not open flat, and the first few millimetres next to the spine curve away from the reader
Handednesspage 1 is a right, page 2 a leftthe gutter alternates from there, and a layout has to know which hand it is on
Resolution300 dpi targetwhat a photograph is measured against once it has a slot
Full-page floor200 dpibelow this a photograph is not offered a full page at all, however well its shape fits — it goes to a grid slot, where the same pixels reach further (B502)
Page count28 to 200, in multiples of 2one rule for every photobook product, soft or hard, square or portrait — measured against the live catalogue. 27 pages rounds up to 28; a book past 200 becomes several volumes, split at a chapter and never mid-day

A spread, at every size

verso and recto, the gutter between them

lib/photobook/spec.ts — contentBoxMm, sideOf

The inner margin is wider than the outer, and that asymmetry is the whole point of drawing both pages: the gutter is where the binding swallows the paper, so a face centred on the page is a face half in the spine. Drawn at all four sizes because the millimetres are identical and the proportions are not — which is the only thing a reader of a drawing can see.

Pocket square 140 × 140 mm · content box 114 × 120 mm

left
right

Square 200 × 200 mm · content box 174 × 180 mm

left
right

Portrait 210 × 280 mm · content box 184 × 260 mm

left
right

Large square 280 × 280 mm · content box 254 × 260 mm

left
right

The cover: back, spine, front

Square 200 × 200 mm · spine grows with the page count · title band 46 mm deep

lib/photobook/coverGeometry.ts — computeCoverGeometry, COVER_BAND_MM

One file, laid out back-to-front across the sheet. A softcover is flat paper wrapped round the block; a hardcover case wraps around boards, with a joint hinging either side of the spine and a wrap folded over the edges. The dashed rectangle is the TrimBox: for a softcover it is the bleed inset, and for a hardcover it is the fold line — not wrap + bleed, which is what it used to be and put every hardcover preview shrunk into a corner.

softcover

back
front
28 pages · sheet 408.72 × 206.00 mm · spine 2.72 mm · bleed 3 mm · wrap 0 mm · trim inset 3 mm · computed
back
front
100 pages · sheet 414.30 × 206.00 mm · spine 8.30 mm · bleed 3 mm · wrap 0 mm · trim inset 3 mm · computed
back
front
200 pages · sheet 422.05 × 206.00 mm · spine 16.05 mm · bleed 3 mm · wrap 0 mm · trim inset 3 mm · computed

hardcover

back
front
28 pages · sheet 458.00 × 246.00 mm · spine 6.00 mm · bleed 3 mm · wrap 17 mm · trim inset 17 mm · computed
back
front
100 pages · sheet 463.00 × 246.00 mm · spine 11.00 mm · bleed 3 mm · wrap 17 mm · trim inset 17 mm · computed
back
front
200 pages · sheet 471.00 × 246.00 mm · spine 19.00 mm · bleed 3 mm · wrap 17 mm · trim inset 17 mm · computed

What the spine actually measures

The one number in this whole pipeline that has been wrong. B884 found it computed about 40% narrow — a 52-page book at 3.22 mm against a real 4.58 mm — and a spine 2 mm narrow wraps the front image round onto the spine, which is invisible until the book is in your hands. B885 replaced the formula: a softcover spine is fitted to every measured row, a hardcover’s is a measured table interpolated and rounded up, because too wide wastes a millimetre of board and too narrow ruins the cover. Both add 4 pages first, which is what Gelato does before it answers. Every figure below is computeCoverGeometry being called, not a table on this page.

Pagessoftcover spinehardcover spineSheet, softcover
282.72 mm6.00 mm408.72 × 206.00 mm
524.58 mm6.00 mm410.58 × 206.00 mm
766.44 mm11.00 mm412.44 × 206.00 mm
1008.30 mm11.00 mm414.30 × 206.00 mm
13210.78 mm15.00 mm416.78 × 206.00 mm
16413.26 mm17.00 mm419.26 × 206.00 mm
20016.05 mm19.00 mm422.05 × 206.00 mm

These are the offline fallback and they are not the answer. fetchCoverGeometry asks Gelato’s own cover-dimensions endpoint, and lib/photobook/build.ts calls it before anything is rendered for an order. This table is what a checkout with no API key draws with, so the pipeline stays developable without an account — every geometry here carries source: “computed” to say exactly that.

The pages a book can contain

12 kinds

lib/photobook/plan.ts — BookPage

title
The trip's title, its dates, the byline, and the party drawn walking above it. Always a recto, and the book starts on its back — no blank leaf after it, which is the owner's call against the printing convention.
intro
The trip's own introduction, set as prose. Only when the trip wrote one and the book is printing text.
route
The journey drawn on the world, one page or a two-page spread depending on the shape of the route. Every stop, with the fold slid off the line where it can be.
chapter
A country opens. Not weeks and not highlights — a border is the thing a reader already has a mental model for. Optionally the party drawn small at the foot.
day
A date, a place, what happened, and a photograph filling the foot of the page. Every day gets one. Its words can run onto a second day page when the owner asked for that.
photos
Photographs and nothing else, in one of the grids the planner chose or the owner named.
followers
Who came along, by name. Names only — a book gets handed around and eventually given away.
transport
How the trip moved: the modes counted, optionally with the site's own vehicles drawn beside the words.
costs
What it cost, by category and by country. Only when the trip recorded costs and the switch is on.
analytics
A chart page — spend against budget, or the trip's weather. One topic per page, and only the ones the trip actually recorded.
colophon
The last page: how the book was made, who is in it, and the party drawn once more.
blank
Paper, inserted so the next page falls on the hand it has to fall on, or so the book reaches the binder's minimum.

The pages, as the renderer draws them

the real shape lists, walked by the preview's own SVG writer — invented numbers, no content

lib/photobook/charts.ts · lib/photobook/routeMap.ts · lib/photobook/preview.ts — svgMarks

H O W W E G O T A B O U T7days driving3days by train1day flying8 legs written down, from Basel to Budapest.
transport — the opener template — charts.ts · transportShapes, with the vehicles switched on. The block sits on the opener baseline at 0.62 of the content box and rises only when its rows need more, never above the head; each bar is that mode's share of the trip's days.
W H A T I T C O S TCHF 924total, everything includedBefore we leftCHF 120On the roadCHF 804Per day on the roadCHF 103W H E R E I T W E N TaccommodationCHF 229otherCHF 216flightsCHF 177transportCHF 152foodCHF 130activitiesCHF 20B U D G E T A N D W H A T H A P P E N E DBudgetedCHF 1,000SpentCHF 924
costs — the stat sheet — charts.ts · costsShapes. Panels are chosen before any is drawn, so the page never finishes with both a hole and a panel dropped; the six tints stop half way to paper so the smallest category is still ink.
W H E R E T H E M O N E Y W E N TS P E N T , D A Y B Y D A YCHF 92312 July 202620 July 2026the budgetW H A T E A C H D A Y C O S TCHF 40012 July 202620 July 2026average CHF 103
spend — two plots on one axis — charts.ts · spendPageShapes. The budget is a plan and is drawn dashed; the average is a measurement and is drawn solid. Both are named in the key under the plot they belong to.
T H E W E A T H E R W E H A DD A I L Y H I G H A N D L O W35°15°12 July 202620 July 2026average high 30.4°average low 20.6°R A I N15 mm2 days have no reading, and are left blank.Measured by Open-Meteo.
weather — the reserved note band — charts.ts · weatherPageShapes. The note and the archive's credit are taken out of the page before a plot is laid out; two days carry no reading at all and draw nothing, which is what the note counts.

The route, in every style the composer offers

Four invented stops, the real projection, the real land and water, and the real label-collision rule — including its fallback to the first and last name when more than a third of the labels cannot sit beside their own dot.

BaselViennaBudapestBalatonfüred4 stops, 12–20 July 2026
closeup
12344 stops, 12–20 July 20261. Basel2. Vienna3. Budapest4. Balatonfüred
numbered
BaselViennaBudapest4 stops, 12–20 July 2026
chapters
SwitzerlandAustriaHungary4 stops, 12–20 July 2026
countries
1. Basel2. Vienna3. Budapest4. Balatonfüred4 stops, 12–20 July 2026
strip

The title page, the chapter dividers, the day pages and the cover are drawn by render.ts and preview.ts from the page’s own fields rather than from a shape list, so they are described above rather than drawn here — and looked at in the composer, where a real book has its photographs.

Every option the composer offers

with the value a fresh book starts from

lib/photobook/options.ts — BookOptions, DEFAULT_OPTIONS, DAY_LAYOUTS

The book

sizesquare — one of pocket, square, portrait, large-squarechecked against the catalogue; an unrecognised size is refused, never assumed
coverTypesoft — soft or hardasked before the size, because the two do not offer the same sizes. Not the cover photograph, which is `cover`
localeenwhat the book's own words are printed in — headings, labels, the colophon. Choosing a language never invokes a translation service
coverabsentthe front photograph, by gallery src. Absent means the planner picks the first photograph of the first chapter
spineTextabsent — at most 60 charactersabsent means derive it. Not a taste limit: the spine runs the height of the book, the smallest of which is 140 mm, and type that outruns it is trimmed away at both ends
excludePhotos[]photographs left out of the book, by gallery src
focalPoints{}where a photograph is cropped from, both axes 0–1. A photograph with no entry crops from its centre
days{}per-day overrides, keyed by date. Absent means the planner decides, which is still the normal case

The switches

includeTexton by defaultThe days' prose and the photo captions. Off gives a photo album with dates — the day pages stay, because a photo album that cannot say when it was is worse than one with a heading.
includeMapon by defaultThe route map: one page, or a two-page spread when the route's shape wants it.
includeChapterson by defaultThe chapter divider that opens each country.
includeNameson by defaultWho travelled, on the title page and in the colophon.
includeFollowerson by defaultWho followed from home: their page when there are seven or more, their two lines in the colophon when there are fewer. Its own switch since B2037.
includeTransporton by defaultThe transport page, when the trip recorded how it moved. Its own switch since B2048; the vehicles drawn on it are includeVehicles.
includeCostson by defaultThe cost summary page. The order page turns this on for a first visit exactly when the trip has a budget to show.
includeChartson by defaultThe chart pages — spend against budget, and the weather. The one switch that adds pages rather than defending ones the book always had; on for a first visit only when there is both a budget and weather to chart.
includeFigureMarksoff by defaultThe party drawn small at the foot of every chapter divider. Draws nothing when nobody has been described or there are no chapter pages.
includeVehiclesoff by defaultThe way each leg was travelled, drawn on the transport page with the site's own vehicles. Adds no page of its own.
mapPlacesoff by defaultTown and park names on the map, checked against the route's own stop labels so a town name never overprints a stop's. Off by default — B2049.
mapTerrainoff by defaultNamed peaks on the map. Relief, rivers and lakes already print on every close-up style; this is the one basemap layer that did not. Off by default — B2049.

A single day, when the owner has said

photosabsentWhich photographs, in which order. Named by gallery src, so the day is rebuilt from the list rather than filtered by it. A named photograph the day no longer has is dropped, never invented. An empty list is a day emptied on purpose; absent is a day nobody touched.
keepabsentPhotographs that must be printed whatever the budget, named the same way. Much smaller than `photos`: the day stays the planner’s and keeps its quota, the pinned frames simply come out of that quota first and are never dropped as junk or as a near-duplicate. Written by “Keep this” on the receipt’s list of what did not fit.
layoutabsentThe shape of the day — one of the layouts above.
heroabsentWhich photograph runs big, when one is going to. A hint, not a decision: the planner still decides whether this day gets a hero at all.
runOnabsentWhether the words may continue onto further pages rather than being cut short. Absent means yes, since B1815 — a day that overflows gets as many pages as it needs. Only `false` does anything now: it holds this one day to one page and accepts that its ending is not printed.
textFormabsentA named text shape for this day: `runon-auto` (what an untouched day already does), `textpage` (the words take the page and the photograph moves to the facing one, declined below about half a page), `bigshort` (set large for a day of a few words, and only when every printed day is short enough), `twocolumn` (two columns, declined the same way `textpage` is).
excludedabsentLeave the day out of the book entirely. The route map and the trip's own numbers are deliberately unaffected: a day the book leaves out is still a place the trip went.
textabsentLeave this day's prose out while the rest of the book keeps its text. A day may only narrow the book's decision, never widen it.

6 day layouts

auto
The planner decides, as it always has — the rules below.
hero
One photograph filling the paper, then the rest grouped. Overrules the rhythm, not the resolution warning.
single
Every photograph on its own page, running to the outer edge.
pair
Two to a page — side by side if both stand up, stacked otherwise.
grid
Four to a page where the shapes allow it.
text
The day's words and nothing else.

How a shape is chosen when nobody has chosen one

every number below is the planner answering, not a figure typed onto this page

lib/photobook/plan.ts — groupPhotos, chaptersOf, routeFitsOnePage, spineTextFor

What is measured about a photograph

Aspectwidth ÷ heightthe only thing about a photograph's shape the planner knows
Portraitaspect below 0.870found by bisecting groupPhotos, not read off a constant
Panoramaaspect at or above 1.900takes a page of its own before any other rule is consulted
Landscape vs squaremeasured, never acted onorientation() answers three values, and every rule that consults it asks only whether the answer is portrait. A square photograph and a wide one are grouped identically
Pixels for a full page1575 px across at 200 mmthe 200 dpi floor, measured against the trim width rather than the exact slot — so the gate errs toward the grid slot rather than toward a page that turns out soft (B502)

The grouping ladder, run

Photographs are taken in order and the first rule that fits wins. These are real calls to groupPhotos on probe shapes — no photographs, only widths and heights.

one photograph at 2.2 : 1panorama × 1a photograph this wide takes a page of its own, whatever else is waiting
three upright photographstrio-portrait × 3three upright photographs in a row beats a pair plus a straggler (B641)
two upright photographspair-portrait × 2two upright photographs side by side
four landscape photographsquad × 4four that neither stand up nor run wide, to a page
one upright and one landscapefeature × 1 + feature × 1a lone photograph with the pixels to run to the outer edge
one landscape photograph, 1575 px widefeature × 1a lone photograph with the pixels to run to the outer edge
the same photograph at 1418 pxsingle × 1a lone photograph without them: it sits inside the content box instead, a smaller printed width and therefore a lower bar (B502)

The last two rows are the same shape of photograph at two resolutions, and they get different pages. At 300 dpi the planner also warns about any photograph that prints soft in the slot it actually got — 180 dpi across a 200 mm page, in that last case.

Which days run big

A hero is a chapter’s opening day and then roughly every third day after it —dayIndex === 0 || dayIndex % 3 === 0 in draftsForChapter. Every day used to open full-bleed; nineteen days meant nineteen full-bleed pages, and a book where every photograph is the loudest has no loud photographs in it. Three further conditions, all of them in the same function: a day with only one photograph never spends it on a hero, because the alternative is a page of prose with nothing on it facing a page of photograph with nothing to say; the owner naming hero for a day overrules the rhythm but not the resolution; and the automatic rhythm, if its usual pick is too small for a full page, looks for another photograph of that day that qualifies before giving the hero page up.

How much a day may say

dayTextBudget measures the column: the content box, less the heading, less the photograph that shares the page — 52% of the trim height, which is what reads as a picture with a caption above it rather than as a band. fitDayText then wraps the paragraphs at the real type size and counts how many lines fit. Over the line, the page says so and the rest is on the website, unless the owner set runOn for that day — then the day gets a second page, taking a spare photograph with it if it has one. One measurement, two callers: the planner deciding and the renderer drawing use the same helper, because two callers computing this separately is how a book truncates on a page the planner thought was fine.

One page of map, or two

routeFitsOnePage pads the route’s own bounding box by 15% (with a floor for a degenerate span) and asks which of a single page or a two-page spread its shape sits closer to, in log space. A compact north–south journey cannot fill a forced 2:1 spread — the frame has to widen sideways to reach it, and the route ends up a squiggle in a corner. Real calls, on probe coordinates:

Routepocketsquareportraitlarge-square
compact, north–southfour stops inside half a degree of longitudeone pageone pageone pageone page
wide, east–westforty degrees of longitude, three of latitudespreadspreadspreadspread

Where a chapter starts

Consecutive days in the same country. A country revisited later in the trip becomes a second chapter, because that is what actually happened. A day with no country is filed under the caller’s word for elsewhere. chaptersOf run on four probe days:

Portugal, Portugal, Spain, Portugal → Portugal (4)

What goes down the spine

The trip’s title, with its start year appended unless the title already carries it — a shelf of books called “Portugal” is unreadable, and “Algarve 2026 · 2026” was printed down the spine of a book somebody paid for (B1544). The owner’s own spineText wins outright.

  • Alps + 2024 → Alps · 2024
  • Algarve 2026 + 2026 → Algarve 2026
  • Algarve 2026 + 2026 + the owner's own text → Eight days of rain

Photograph categories — designed, not shipped

from the bildwissen branch (B1865, B1866). Not on main, and nothing in the planner reads any of it

lib/ingest/imageFacts.ts · lib/photos/described.ts — on branch bildwissen

Not built. Everything above this heading is running code. Everything below is a design being built on another branch. No layout rule in lib/photobook/ consults a single one of these values today, and this page will import them instead of listing them once that branch lands.

What a model may say about one photograph

Three texts and not one, because they answer three different questions about the same pixels: a caption is what a person would print under the picture, alt text is what somebody who cannot see it needs said, and a long description exists only when there is genuinely more to read out — a map, a menu, a group. Each per journal locale, because a journal in three languages needs the alt text in three languages or it has it in none. Plus a confidence of high, medium or low, and a set of tags.

portrait · group · crowd · landscape · seascape · cityscape · architecture · interior · food · drink · vehicle · animal · plant · sport · celebration · night · detail · document · screenshot · artwork

A vocabulary handed to the model in the prompt, not a frozen enum in a contract: it can grow without a schema version, and anything outside it is dropped after parsing rather than failing the answer.

What only the pixels can say

The planner today measures everything a shape can tell it — aspect, orientation, panorama, the length of a caption. It cannot see how bright a picture is, what colour it mostly is, whether it is sharp, whether it is nearly empty, or which ninth of the frame is quiet enough to carry text. Measured once at ingest and cached beside the photograph:

brightness
mean luminance, 0–1. A caption on a white sky needs dark type; on a night shot it needs light type
dominant
the picture's main colour, for a mount or a page ground that does not fight it
sharpness
higher is crisper. A soft frame is a poor full-bleed
entropy
how much is going on. Near zero is fog, snow or a wall
quietTiles
nine numbers over a 3×3 grid, each the mean per-channel deviation of that ninth. Low means flat, which means a caption can sit there
width, height
the displayed pixels, EXIF rotation already applied — the two the planner already uses