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.
| Size | Trim | Made in | Full-bleed photo needs |
|---|---|---|---|
| Pocket square 140 × 140 mmpocket | 140 × 140 mm | ✓ softcover— no hardcover | 1725 px across at 300 dpi |
| Square 200 × 200 mmsquare | 200 × 200 mm | ✓ softcover✓ hardcover | 2434 px across at 300 dpi |
| Portrait 210 × 280 mmportrait | 210 × 280 mm | ✓ softcover✓ hardcover | 2552 px across at 300 dpi |
| Large square 280 × 280 mmlarge-square | 280 × 280 mm | — no softcover✓ hardcover | 3378 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
| Bleed | 3 mm on all four edges | artwork runs past the trim and is cut away; TrimBox and BleedBox go on every page |
|---|---|---|
| Safe margin | 10 mm inside the trim, on the outer three edges | nothing that matters goes outside it |
| Gutter | 16 mm at the spine | wider 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 |
| Handedness | page 1 is a right, page 2 a left | the gutter alternates from there, and a layout has to know which hand it is on |
| Resolution | 300 dpi target | what a photograph is measured against once it has a slot |
| Full-page floor | 200 dpi | below 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 count | 28 to 200, in multiples of 2 | one 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
Square 200 × 200 mm · content box 174 × 180 mm
Portrait 210 × 280 mm · content box 184 × 260 mm
Large square 280 × 280 mm · content box 254 × 260 mm
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
hardcover
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.
| Pages | softcover spine | hardcover spine | Sheet, softcover |
|---|---|---|---|
| 28 | 2.72 mm | 6.00 mm | 408.72 × 206.00 mm |
| 52 | 4.58 mm | 6.00 mm | 410.58 × 206.00 mm |
| 76 | 6.44 mm | 11.00 mm | 412.44 × 206.00 mm |
| 100 | 8.30 mm | 11.00 mm | 414.30 × 206.00 mm |
| 132 | 10.78 mm | 15.00 mm | 416.78 × 206.00 mm |
| 164 | 13.26 mm | 17.00 mm | 419.26 × 206.00 mm |
| 200 | 16.05 mm | 19.00 mm | 422.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
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.
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
| size | square — one of pocket, square, portrait, large-square | checked against the catalogue; an unrecognised size is refused, never assumed |
|---|---|---|
| coverType | soft — soft or hard | asked before the size, because the two do not offer the same sizes. Not the cover photograph, which is `cover` |
| locale | en | what the book's own words are printed in — headings, labels, the colophon. Choosing a language never invokes a translation service |
| cover | absent | the front photograph, by gallery src. Absent means the planner picks the first photograph of the first chapter |
| spineText | absent — at most 60 characters | absent 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
| includeText | on by default | The 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. |
|---|---|---|
| includeMap | on by default | The route map: one page, or a two-page spread when the route's shape wants it. |
| includeChapters | on by default | The chapter divider that opens each country. |
| includeNames | on by default | Who travelled, on the title page and in the colophon. |
| includeFollowers | on by default | Who 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. |
| includeTransport | on by default | The transport page, when the trip recorded how it moved. Its own switch since B2048; the vehicles drawn on it are includeVehicles. |
| includeCosts | on by default | The cost summary page. The order page turns this on for a first visit exactly when the trip has a budget to show. |
| includeCharts | on by default | The 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. |
| includeFigureMarks | off by default | The party drawn small at the foot of every chapter divider. Draws nothing when nobody has been described or there are no chapter pages. |
| includeVehicles | off by default | The way each leg was travelled, drawn on the transport page with the site's own vehicles. Adds no page of its own. |
| mapPlaces | off by default | Town 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. |
| mapTerrain | off by default | Named 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
| photos | absent | Which 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. |
|---|---|---|
| keep | absent | Photographs 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. |
| layout | absent | The shape of the day — one of the layouts above. |
| hero | absent | Which 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. |
| runOn | absent | Whether 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. |
| textForm | absent | A 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). |
| excluded | absent | Leave 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. |
| text | absent | Leave 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
| Aspect | width ÷ height | the only thing about a photograph's shape the planner knows |
|---|---|---|
| Portrait | aspect below 0.870 | found by bisecting groupPhotos, not read off a constant |
| Panorama | aspect at or above 1.900 | takes a page of its own before any other rule is consulted |
| Landscape vs square | measured, never acted on | orientation() 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 page | 1575 px across at 200 mm | the 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 : 1 | panorama × 1 | a photograph this wide takes a page of its own, whatever else is waiting |
|---|---|---|
| three upright photographs | trio-portrait × 3 | three upright photographs in a row beats a pair plus a straggler (B641) |
| two upright photographs | pair-portrait × 2 | two upright photographs side by side |
| four landscape photographs | quad × 4 | four that neither stand up nor run wide, to a page |
| one upright and one landscape | feature × 1 + feature × 1 | a lone photograph with the pixels to run to the outer edge |
| one landscape photograph, 1575 px wide | feature × 1 | a lone photograph with the pixels to run to the outer edge |
| the same photograph at 1418 px | single × 1 | a 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:
| Route | square | portrait | large-square | |
|---|---|---|---|---|
| compact, north–southfour stops inside half a degree of longitude | one page | one page | one page | one page |
| wide, east–westforty degrees of longitude, three of latitude | spread | spread | spread | spread |
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
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