Commit Graph
3 Commits
Author SHA1 Message Date
austin e10da662a0 Three bugs and a lighter page: 367 KB of fonts became 57
THE BUGS.

Tapping any field zoomed the page in on iOS, because Safari does that to anything under 16px and `field`
was 14px. Every enquiry typed on a phone began by throwing the layout sideways. 16px on phones, the denser
size back at sm where no browser does it.

The focus ring vanished on the sage bands: one colour for the whole site, and sage-500 on sage-800 is
about 2.5:1 — under the 3:1 a focus indicator needs. It went missing exactly where the page's main calls
to action live. Cream on those sections now.

THE FONTS. AdBhashitha shipped 1605 glyphs to set 187 codepoints, as .woff, unpreloaded: 306 KB for the
face every heading uses, arriving after first paint so each page opened in a fallback serif and then
reflowed. Subset to Latin and the punctuation it actually has, every layout feature kept, compressed to
woff2: **306 KB → 8 KB**, and a pixel diff of the catering page before and after is exactly zero. The
script wordmark is only recompressed, never subset — its swashes come from alternates and the logo is the
one thing that must not change shape. All three faces are preloaded now and together weigh less than the
display face did alone.

THE REST OF THE LIST. The products filter wrapped to three rows on a phone with "PASTRIES" stranded on the
last; it is a scrolling snap row below sm, wrapping and centred above it, and the negative margin lets a
half-chip show at the edge so it is obviously scrollable. Chips are 44px tall and the gallery arrows 44px
wide — both were under the thumb-sized minimum. The contact form has a honeypot: a real field with a
plausible name, off-page rather than display:none, skipped by the tab key. A filled one gets the same
thank-you a person gets, because telling a bot it failed only teaches it to try again — and nothing is
recorded or sent, which the test proves against the enquiry table.

And the catering small print, which was loose text floating between two rows of cards, is a soft panel
belonging to the table above it.

61 tests. Verified at 390 and 900.
2026-07-26 20:53:06 -05:00
austin 69af6c672e The shop knows when it is open, and says so — to visitors and to Google
Three things a bakery's site should do that this one couldn't, all from the same fact.

THE HOURS WERE MARKUP, so the only thing the site could do with them was print them. A visitor at half
past two on a Sunday had to work out for themselves that the shop was shut. They are data now (Shop.WEEK),
and `Hours` reads them two ways: as the list the footer and the contact page print — consecutive days with
the same times collapsed into one line, the way a sign does it, instead of three hand-written rows — and
as an answer. The header now says "Open until 2:00pm", or "Closed · opens Tuesday at 7:00am", worked out
on the server in the shop's own timezone. Right now, on a Sunday evening, it says the latter.

THE SHOP IS NOW DESCRIBED TO A SEARCH ENGINE: schema.org Bakery in the head, with the address, the phone
number and those same opening times. For a shop whose customers find it by searching its town, that is the
difference between a blue link and a listing that shows "Open ⋅ closes 2pm" — and it cannot drift from
what the page says, because it is built from the same record. Serialised with Jackson rather than
string-built, so the day somebody interpolates a name into it, it is not an injection. (Jackson 3 —
`tools.jackson` — which is what Boot 4 ships; the fasterxml import does not compile.)

A SHARED LINK NOW HAS A PICTURE. og:image, og:image:alt and a large summary card: it was a grey box with a
title, which is what every link to this site has looked like in a message.

AND IT PRINTS. The catering page is a price list and a bakery prints price lists — Ctrl-P now gives the
prices on paper rather than a screenshot of a website: no chrome, no sage bands eating a cartridge, cards
that don't split across a page break, and the buttons that only exist to be clicked marked `no-print`.

Five new tests, all of them free of Spring, because this is a calculation over a constant: the week
collapses as a sign would write it, the minute of opening counts as open and the minute of closing does
not, and a Saturday afternoon is told when Tuesday starts.

Also swept the last one-off styles the earlier pass missed — max-w-3xl/4xl, an inline letter-spacing the
.h2 class already carries, and the mobile menu's link string.
2026-07-26 19:27:41 -05:00
austin 70af2922f5 The site renders itself: Thymeleaf pages, and a catering page among them
The public site was a React SPA. It is now server-rendered Thymeleaf, and the goodie box and catering
tables added in the previous commit have a page of their own. The look is unchanged: the templates
carry the same Tailwind classes the components did, and every one of the 241 classes the five pages
use resolves in the compiled stylesheet.

WHAT WENT AWAY. PageMetaController — 148 lines whose only job was to splice per-page <title> and OG
tags into one shell with regular expressions, with a test that read the real index.html so that
reformatting it failed the build instead of silently breaking the rewriting. A page rendered on the
server writes its own head. Also react-router (no client-side routes left), motion, vite-plugin-svgr,
and the SPA fallback (platform.web.spa.enabled=false): with the site server-rendered, forwarding a
mistyped URL to /index.html would answer with a blank admin shell and a 200 instead of the site's own
404 page.

WHAT GOT BETTER ON THE WAY, none of it visible. The category filter is a ?category= link, so every
filtered view is a URL you can send someone and a crawler can reach all forty items instead of the
twelve the default filter showed. The contact form is a form post: the enquiry is recorded before
delivery is attempted, and a refused relay re-renders the page with what the visitor typed still in
the boxes. The mobile menu is a <details> — the React version needed four effects to close on
navigation, close on Escape, stop the page behind it scrolling, and unmount (a panel parked off-screen
still extends the scrollable area, which is how you used to be able to scroll sideways and find the
menu); a new document cannot inherit an open menu.

THE PUBLIC SITE SHIPS 5 KB OF JAVASCRIPT, and works without it. The product cards are scroll-snap
strips, so the photos swipe on a phone and scroll with a trackpad unaided; gallery.js adds the arrows
and the dots, and creates them itself rather than having the template render controls that would sit
there dead.

Tailwind still needs its compiler, so npm remains a BUILD tool: the CLI compiles the templates into
static/css/site.css at process-classes (so `spring-boot:run` gets it too), and frontend/ now builds
only that stylesheet and the admin. The brand tokens are one file both stylesheets import — the
alternative was the shop front and the screen that edits it drifting a shade apart. The stylesheet URL
carries ?v=<sha>, because one hand-written CSS file has no content hash and a deploy has to be able to
tell a browser that what it cached is stale.

The admin is still React and is untouched, apart from losing the router it no longer needs. It is an
editor, not content.

PlatformContractTest stopped inheriting platform-starter-test's contract and restates it. The shared
version asserts that an unknown path forwards to the SPA shell, which is no longer true here, and its
test methods are package-private so it cannot be overridden. The platform should decide that assertion
from platform.web.spa.enabled — noted in the file.

9 new tests (46 total): every page's real title and og:url, the catalogue and the catering tables in
the HTML rather than fetched afterwards, server-side filtering, the 404, and that a crafted ?about=
link cannot put words of its own choosing in front of a customer.
2026-07-26 16:07:01 -05:00