feat(media): make image insertion work end to end in the article editor - #132
Merged
Conversation
Inserting an image into an article body was only partly wired up. This
makes the whole path work and stops the editor offering controls the
published page cannot honour.
Uploading is now an editor capability. POST /v1/media was admin-only
while every user after the first defaults to editor, so an editor
dragging an image in got a silent 403: the failed attachment stayed on
its local blob URL, looked fine, and vanished on reload. Delete and
reindex stay admin-only.
Pasting is handled for all three of its shapes. A screenshot off the
clipboard arrives with a File and takes the upload path. Rich text
copied from another site arrives as a bare remote URL and was previously
ignored, leaving the article hotlinking a third party; it is now
sideloaded through a new POST /v1/media/fetch. An image copied from
another article in this CMS is recognised as already ours and left
alone rather than duplicated.
Because that endpoint makes the server fetch a caller-supplied URL, the
SSRF guard runs in the dialer's Control hook rather than on the URL
string, so it inspects the resolved IP on every redirect hop and blocks
hostnames resolving to internal space, DNS rebinding, and redirects to
the cloud metadata endpoint. Size is capped by reading one byte past the
limit rather than trusting Content-Length.
Images can now be inserted from the media library via a new toolbar
button, backed by the /v1/media/gallery endpoint that already existed
and had no callers. The library's alt text comes with the image, so an
asset described once is described everywhere it is used, and the picker
flags assets that have none.
Article bodies now store plain semantic markup -- figure/img/figcaption
using the same WordPress class names as the migrated corpus -- instead
of Trix's data-trix-attachment JSON, which means nothing outside a Trix
editor. The load and save conversions live together in trixImageHtml.ts
as explicit inverses. alt is always emitted, since a missing alt
attribute is an accessibility failure while alt="" is a valid signal.
Failed uploads and imports now remove the attachment and say why,
instead of leaving a preview that cannot survive a reload.
Finally, image resizing and alignment are removed from the editor. The
public site sizes article images entirely in CSS (#article figure img {
width: 100% }), which overrides anything an author sets, so both were
gestures that appeared to work and changed nothing on the page. Worse,
width/height are presentational hints that CSS outranks, so emitting
them would have left the width overridden while the height still
applied, stretching every image. Drag-to-reorder is kept because it does
survive. Alignment already present on migrated content is still
preserved through a save; it just can no longer be set here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Inserting an image into an article body was only partly wired up. This makes the whole path work, and stops the editor offering controls the published page cannot honour.
Editors can actually upload
POST /v1/mediawas admin-only, while every user after the first defaults toeditor. So an editor dragging an image in got a silent 403 — and because the client only handled201, the failed attachment stayed on its local blob URL, looked fine, and vanished on reload. Delete and reindex stay admin-only.Pasting
Handled for all three shapes it arrives in:
Sideloading goes through a new
POST /v1/media/fetch. Since that makes the server fetch a caller-supplied URL, the SSRF guard runs in the dialer'sControlhook rather than on the URL string — it sees the resolved IP, on every redirect hop, so it catches hostnames resolving to internal space, DNS rebinding, and public URLs redirecting to169.254.169.254. Size is capped by reading one byte past the limit rather than trustingContent-Length. Covered bymedia_fetch_test.go.Insert from the media library
New toolbar button, backed by
/v1/media/gallery— an endpoint that already existed, was documented as "the trimmed shape used by image pickers", and had zero callers. The library'salt_textcomes with the image, so an asset described once is described everywhere it's used; the picker flags assets that have none.Articles store semantic markup
Bodies now store
<figure class="wp-caption"><img src alt><figcaption class="wp-caption-text">instead of Trix'sdata-trix-attachmentJSON, which means nothing outside a Trix editor and rendered unstyled on the public site. The class names match the migrated WordPress corpus, so it renders through Scalene's existing#article figurestyling with no changes needed there.Load and save conversions live together in
trixImageHtml.tsas explicit inverses.altis always emitted, even empty — a missingaltattribute is an accessibility failure, whereasalt=""is a valid "decorative" signal.Round trips verified against WP classic captions, block figures, caption-less and alt-less figures, galleries, and escaping: all stable and idempotent, with multi-image galleries and still-uploading blob attachments correctly left untouched.
Failures are visible
A failed upload or import now removes the attachment and says why, instead of leaving a preview that cannot survive a reload and would publish an article whose image silently isn't there.
Resize and alignment removed
The public site sizes article images entirely in CSS —
#article figure img { width: 100% }— which overrides anything an author sets. Both gestures appeared to work in the editor and changed nothing on the page.Emitting sizes would have been actively harmful, not merely useless:
width/heightare presentational hints that CSS outranks, so the width would be overridden while the height still applied, stretching every image. There's noheight: autoon the public side to save it.Drag-to-reorder is kept — it does survive. Alignment already present on migrated content is preserved through a save; it just can't be set from here anymore. Flattening it out of the corpus would be a one-line serializer change, but that should be a deliberate call rather than a side effect of removing a gesture.
Testing
go build ./... && go test ./...— all green, including new SSRF guard teststsc --noEmit,eslint src/,vite build— all clean🤖 Generated with Claude Code