Commits
Crepe out, BlockNote in: new editor and media library
Six hours and fourteen deployments against the selection jump, library swap
The selection jump: six hours and fourteen deployments
Yesterday I closed the day with the redesigned panel —the one taking shape in From Summary to editor: the panel takes shape— and today I had the worst afternoon of the last few weeks and, at the same time, one of the most productive: editor swap and a complete media library. Let me break it down.
In the Crepe/Milkdown editor, selecting text made the page jump. It looked like a trivial CSS issue (the language that gives a website its look), but it wasn't. The list of failed fixes is long: scroll-padding, single scroll on the canvas, overflow-anchor, TopBar out of the scroll (it broke the layout), portaled toolbar (blank page), position fixed with CSS variables. Fourteen deployments and 3 reverts without touching the real cause: Crepe's floating toolbars with the nested scroll didn't get along with Safari.
The lesson is burned into me now: if three structural fixes fail, the problem is the tool, not the fix. And never again remove features the user loves as a diagnostic experiment: I did it with the "/" menu and the block handles and it was a mistake.
BlockNote replaces Crepe
I switched tools: BlockNote with the official Mantine adapter. It ships with the "/" menu, the per-block handle, and the selection menu with paragraph-to-H1-H3 conversion — everything we required. Markdown goes in and out, so the content field in the API doesn't change: zero data migration. Milkdown uninstalled, 155 packages gone. After three CSS tweaks (theme, spacing, teal selection), the editor passed all tests and went into production.
Own media library
Until now images went as base64 data-URLs, the whole file embedded inside the text: they filled up the database and the pages. Now there's a media table, files in a Docker volume (a virtual disk for the container), and an /admin/medis page with grid view, multi-upload, preview, and URL copy. The editor uploads through uploadFile. SVG rejected for stored XSS (an SVG can carry executable code inside) and 25 MB max per file.
WebP and 2,000px automatic
Every uploaded image is converted to WebP, a much lighter modern format, quality 85, and if it exceeds 2,000x2,000 it is scaled down keeping proportions. EXIF transpose included for mobile photos (the camera rotation is respected). A 4,000x1,000 PNG becomes a 2,000x500 WEBP.
Two 520s, two causes
A 520 is an error from the intermediate server that hides a cause it can't tell you itself. The first one was structural — on the shared edge network of the server, another project has a service called web, and Docker's DNS (the internal directory that translates names to addresses) was resolving the name to two containers. NPM was distributing requests at random and half of them ended up at a server that wasn't mine. Unique alias web-base-astro and solved.
The second one was mine: the new MediaLibrary read localStorage (the browser's own memory) inside useState. Astro islands (interactive chunks of the page) also render on the server, where localStorage doesn't exist, and the island blew up mid-response. Default value on render and reading inside useEffect.
Phases 1 and 2: folders and settings
Media Phase 1: hierarchical folders with drag-and-drop, metadata (name, alt, description), multi-select, and card or list view. Phase 2: a new section in Settings with maximum dimension and format (WebP or AVIF) that the service reads on each upload.
With the spec written beforehand, the phases came out right on the first try, except for typos. The next day, AI was coming in fully: AI enters Settings: providers, SEO and automatic articles.