Problem
You cannot upload a video, or a PDF, from anywhere in the editor — including on mobile,
where there is no other route to get a file onto the site.
The data-edit-media tag is not the limitation. Hydra's getMediaFields() is entirely
type-agnostic: it collects named elements with a rect and reports them. Everything that
constrains the flow to images is admin-side.
What is actually hardcoded
| Site |
Hardcoded |
Iframe/OpenObjectBrowser.jsx:66 |
mode: 'image' for the field-media click path |
customizations/.../LinkButton/AddLinkForm.jsx:324 |
upload button gated on objectBrowserPickerType === 'image' |
customizations/.../LinkButton/AddLinkForm.jsx:329 |
accept="image/*" on the file input |
customizations/.../Widgets/ImageWidget.jsx:393,434 |
accept="image/*" on both Dropzones |
customizations/.../Widgets/ImageWidget.jsx:203 |
'@type': 'Image', bytes under an image key |
Note AddLinkForm — the picker — already renders the file input and calls an injected
this.props.onFileUpload(file). ImageWidget merely supplies that handler. So the picker is
already the shared surface across the sidebar widget, the link editor, and the iframe media
tap. It just only ever gets handed an image-shaped upload.
That is why the fix belongs in the picker, not in ImageWidget. ImageWidget is the widget
for an image field and should stay image-specific. And generalising it would not help the
link case at all: linking to a PDF goes through AddLinkForm, never through ImageWidget.
Why the PDF case forces the design
Uploading a PDF as '@type': 'Image' would be rejected or produce a broken object. The
portal_type and the bytes key both have to vary with the mime type.
Volto already has exactly this logic, in ContentsUploadModal.jsx:122-131:
const image = fields[1].split('/')[0] === 'image';
return {
'@type': image ? 'Image' : 'File',
title: file.name,
[image ? 'image' : 'file']: { data, encoding, 'content-type': …, filename },
};
And manage/Widgets/FileWidget.jsx already takes an accept prop and validates against
it ("File is not of the accepted type {accept}"). So "the widget declares what it accepts"
is an existing Volto pattern, not an invention.
validateFileUploadSize is mime-agnostic (size only). restrictFileUpload is just a boolean
that disables drag/upload. Neither blocks this.
Proposal
One upload-capable picker that takes two things from its caller, both derived from the schema:
accept — mime filter for the file input (image/*, video/*, unset = anything).
- target content type —
Image for image/*, File otherwise; bytes under image /
file accordingly. Reuse the ContentsUploadModal rule rather than reinventing it.
Then each entry point derives accept from the field's declared widget instead of asserting it:
- sidebar image field →
image/* → creates Image
- video block
url → video/* → creates File
- link editor → unrestricted → creates
File ← the PDF case
- hydra
data-edit-media tap → resolves the field's widget from blockPathMap, replacing
View.jsx's hardcoded mode: 'image'
This keeps the chrome pattern intact (docs/architecture.md): the frontend's tag says which
field, the schema says what it accepts, the admin renders the picker. The tag follows the
widget.
Bonus: the video block already plays uploaded files
core/.../Blocks/Video/Body.jsx:142-152 renders a native <video> whose src is
isInternalURL(data.url) ? '…/@@download/file' : data.url. So the render side is done —
only the "get a File into data.url" half is missing.
Two gotchas for whoever picks this up:
- That player is gated on
data.url.match('.mp4'), so a .webm/.mov upload renders
nothing. The accept list and that check must agree.
- Volto's video block schema declares
url: { widget: 'url' } (a plain text input) and its
Edit placeholder says "Youtube, Vimeo, Peertube" — it was designed as an external-embed
block. Giving it a media widget is a deliberate behaviour change, not a bug fix.
Acceptance criteria
Notes
ImageWidget and AddLinkForm are already shadows in this repo
(packages/volto-hydra/src/customizations/), so this touches our shadows, not core/
(which is git-ignored).
- Separately,
View.jsx:4708 builds a NamedBlobImage value shape for page-level image
fields. Different concern, but it's the third place that assumes "Image".
Researched in the mobile-editing session; filed to be done as its own PR.
Problem
You cannot upload a video, or a PDF, from anywhere in the editor — including on mobile,
where there is no other route to get a file onto the site.
The
data-edit-mediatag is not the limitation. Hydra'sgetMediaFields()is entirelytype-agnostic: it collects named elements with a rect and reports them. Everything that
constrains the flow to images is admin-side.
What is actually hardcoded
Iframe/OpenObjectBrowser.jsx:66mode: 'image'for the field-media click pathcustomizations/.../LinkButton/AddLinkForm.jsx:324objectBrowserPickerType === 'image'customizations/.../LinkButton/AddLinkForm.jsx:329accept="image/*"on the file inputcustomizations/.../Widgets/ImageWidget.jsx:393,434accept="image/*"on both Dropzonescustomizations/.../Widgets/ImageWidget.jsx:203'@type': 'Image', bytes under animagekeyNote
AddLinkForm— the picker — already renders the file input and calls an injectedthis.props.onFileUpload(file).ImageWidgetmerely supplies that handler. So the picker isalready the shared surface across the sidebar widget, the link editor, and the iframe media
tap. It just only ever gets handed an image-shaped upload.
That is why the fix belongs in the picker, not in
ImageWidget.ImageWidgetis the widgetfor an image field and should stay image-specific. And generalising it would not help the
link case at all: linking to a PDF goes through
AddLinkForm, never throughImageWidget.Why the PDF case forces the design
Uploading a PDF as
'@type': 'Image'would be rejected or produce a broken object. Theportal_type and the bytes key both have to vary with the mime type.
Volto already has exactly this logic, in
ContentsUploadModal.jsx:122-131:And
manage/Widgets/FileWidget.jsxalready takes anacceptprop and validates againstit (
"File is not of the accepted type {accept}"). So "the widget declares what it accepts"is an existing Volto pattern, not an invention.
validateFileUploadSizeis mime-agnostic (size only).restrictFileUploadis just a booleanthat disables drag/upload. Neither blocks this.
Proposal
One upload-capable picker that takes two things from its caller, both derived from the schema:
accept— mime filter for the file input (image/*,video/*, unset = anything).Imageforimage/*,Fileotherwise; bytes underimage/fileaccordingly. Reuse theContentsUploadModalrule rather than reinventing it.Then each entry point derives
acceptfrom the field's declared widget instead of asserting it:image/*→ createsImageurl→video/*→ createsFileFile← the PDF casedata-edit-mediatap → resolves the field's widget fromblockPathMap, replacingView.jsx's hardcodedmode: 'image'This keeps the chrome pattern intact (
docs/architecture.md): the frontend's tag says whichfield, the schema says what it accepts, the admin renders the picker. The tag follows the
widget.
Bonus: the video block already plays uploaded files
core/.../Blocks/Video/Body.jsx:142-152renders a native<video>whose src isisInternalURL(data.url) ? '…/@@download/file' : data.url. So the render side is done —only the "get a File into
data.url" half is missing.Two gotchas for whoever picks this up:
data.url.match('.mp4'), so a.webm/.movupload rendersnothing. The
acceptlist and that check must agree.url: { widget: 'url' }(a plain text input) and itsEdit placeholder says "Youtube, Vimeo, Peertube" — it was designed as an external-embed
block. Giving it a media widget is a deliberate behaviour change, not a bug fix.
Acceptance criteria
@@download/file).'@type': 'Image'and is unchanged in the sidebar.accept="video/*"rejects a PDF in the file dialog.View.jsxno longer hardcodesmode: 'image'.Notes
ImageWidgetandAddLinkFormare already shadows in this repo(
packages/volto-hydra/src/customizations/), so this touches our shadows, notcore/(which is git-ignored).
View.jsx:4708builds aNamedBlobImagevalue shape for page-level imagefields. Different concern, but it's the third place that assumes "Image".
Researched in the mobile-editing session; filed to be done as its own PR.