They were building the client's deck by hand in a presentation tool every single time
The client sent three sample files as an aside — two documents and a spreadsheet covering one job. They were not a requirement, they were context. Reading them changed the plan for the next two months.
The spreadsheet was an internal cost sheet: a row per hoarding, with the rate built up in columns — base rate, mounting, printing, tax — and a total. The first document was the client-facing proposal: a cover, a page explaining the idea, then a slide per site showing the hoarding with the advert composited onto it, the location, the dimensions and the rate. The second was a proof-of-display deck sent after installation, photographs of the adverts actually up.
None of that existed in the software. Not a stage of it. The system began at the booking, and everything before the booking — choosing sites, building the rate, producing the two documents, sending them, winning or losing — was being done by hand in office software for every pitch. So was the proof deck at the other end.
The documents a business makes by hand are the specification for the part of the product you have not built. Ask for samples early. They describe the workflow more honestly than any interview, because nobody edits a spreadsheet to look better for a consultant.
Generating rather than templating
The build produced both artefacts server-side from the proposal record: a slide deck and a cost sheet workbook, generated on demand, downloadable from the proposal screen.
The cost sheet was the easy half — it matched the client’s own spreadsheet column for column, which was the point. The deck is where the work was, and most of what I learned is in the mechanics.
Composite at generation time, not at upload. The first version warped the client’s supplied artwork onto the hoarding in the photograph when the design was uploaded, and stored the result. That meant a stored derivative for every design and every photograph, going stale whenever either changed. The rewrite stores only the raw design and composites in memory at generation. Nothing derived is persisted, so nothing derived can be wrong.
Read the image dimensions. The deck library sizes a picture to the box you give it, so every photograph came out stretched. There is no way around this other than knowing each image’s true aspect ratio before placing it, which meant a small dependency-free reader for the two image formats in use. Fifty lines that fixed every slide.
Column widths win over the box. A cost table ran off the right edge of the slide. The library sizes a table to the sum of its column widths and ignores the width you set on the table, so a set of columns summing to more than the slide’s usable width silently overflows. Nothing errors; the table simply extends past the edge and the last column is invisible in the exported file.
Give the export slide masters. Three layouts were defined and used throughout, which sounds cosmetic and is not: it means the recipient can open the file, switch to the master view, and rebrand the whole deck. An export that can be edited by the person who received it is worth more than one that is prettier.
The variant that turned out to matter most
Two download buttons, always both: a branded deck and a white-label one in a neutral palette with no mark anywhere.
That came from the business rather than from design. Proposals go to two kinds of recipient — a direct advertiser, who should see the platform’s branding, and an agency, who will forward it to their own client under their own name. Handing an agency a branded deck means they retype it. A white-label export means they send it as it is.
The recommended button is highlighted based on which kind of party the proposal is addressed to, and both are always present, because the recommendation is a hint and not a rule.
The bug that only appeared on real data
Deck generation returned a server error for every proposal on one dataset. The download filename was built from the proposal title, and those titles contained an en dash. Content disposition headers are Latin-1, so a character outside that set throws before a byte of the file is sent.
That has its own article. What belongs here is the shape: the feature had been tested end to end with titles somebody typed, and it broke on titles somebody pasted. Document generation is unusually exposed to this because it takes user text and puts it into headers, filenames, XML and font glyph tables, each with a different idea of what a character is.
What I would carry forward
Generating a document is not a reporting feature. It is the deliverable the business is actually paid to produce, and it is the artefact the client sees. The rate build-up matters because it appears in the cost sheet. The photograph quality matters because it appears on a slide. Building the deck generator forced the data model to hold things it had been getting away without — structured dimensions per site, a per-line rate build-up, a nominated photograph per site on the proposal.
Build the output document early, because it audits your model. Anything the document needs and the records cannot supply is a gap you would otherwise find in front of the client.
The honest limit: the composited mockup was removed later. Clients preferred to choose from the real photographs of the board rather than see a warped approximation of their artwork on it, and the compositing engine now sits unused behind a working feature nobody asked to replace.