Public: anyone can read this; the crew can change it.
A product has a photograph: an admin uploads it, the shop serves it by content hash, and nothing shifts #38
Sign in to join this conversation.
Public: anyone can read this; the crew can change it.
Every tile and every product page draws
art()fromsrc/html.ts— a gradient made from the slug with two letters on it. It is honest placeholder art and it is the first thing a shopper notices is missing: a shop that sells one real thing should show it.The serving half is already here.
img-src 'self' data:is in the policy,/static/is already answered with a year of immutable caching, and the art keeps its4 / 3box so there is a place for a picture to go. What is missing is the upload, the row and the bytes.Done looks like
image (id, product_id, hash, mime, bytes, width, height, alt, position, created_at), one row per picture; the bytes live atFORGE_APP_DATA/images/<hash>, written once and never rewritten.src/images.tsis one concept and nothing else:accept(bytes)→ the sniffed type, the dimensions and the sha-256 hash, or a reason it was refused;path(hash)for reading.eval. A header that does not parse is a refusal, not a zero.GET /images/:hashanswers the bytes with the sniffed type andcache-control: public, max-age=31536000, immutable;:hashis validated as[0-9a-f]{64}at the boundary, so no path is ever joined from a stranger's text, and a hash with no row is a 404./admin/products/:iduploads a picture, sets its alt text, reorders and removes. Removing deletes the row; the bytes are content-addressed and may be shared, so they stay.<img>withwidth,height, the alt text,decoding="async"andloading="lazy"below the first row. A product with no picture rendersart()exactly as it does today. Neither case shifts: the box is the same box..pngthat is really text is refused; an oversized body is 413; two products uploading the same file write one file and two rows; a product with no picture still rendersart();/images/<64 hex nobody has>is 404 and a hash with..in it never reaches the disk; a non-admin cannot upload.Files I expect to change:
src/db.ts(the table), a newsrc/images.ts,src/routes.ts,src/admin.ts,src/pages/admin.ts,src/pages/storefront.ts,src/html.ts(art()becomes the fallback),src/style.ts,src/server.test.ts,README.md.Risks: this is the one place the app takes arbitrary bytes from a person, so it is admin-only, capped, sniffed and never trusted about its own name. Keep the disk write out of the transaction and the row out of the disk's way: write bytes first, then the row, and let an orphaned file be harmless. It waits on #28 so there is one body-cap helper and one 413 path in the app rather than two.
Suggested: ledger, with sentinel reading the upload route and juniper looking at the preview on a phone.