Media: bundle uploads, global media library, image field picker, recompression rules #12
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle#12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What to build
The hybrid media model (docs/DECISIONS.md D18): image fields upload into the page's leaf bundle by default; a global Media Library at
assets/media/holds shared assets; the image picker offers Upload and Choose-from-library. Enforced rules: jpg/png/webp/avif + PDF only; no SVG or video uploads; ~15 MB cap; images auto-recompressed and downscaled server-side before entering git; alt text required by default (per-field overridable). Responsive rendering stays the theme's job.Acceptance criteria
assets/media/Blocked by
Agent Brief
Category: enhancement
Summary: Hybrid media model: image fields open an Upload/Library picker; uploads land in the page's leaf bundle by default, shared assets live in a global media library under
assets/media/; the server enforces allowed types by content sniffing, a ~15 MB cap, server-side downscale/recompression before anything enters git, required alt text (per-field opt-out), and a best-effort reference-scan warning on delete.Dependency note: this issue is hard-blocked by #3 (sidecar schemas, generated forms, canonical save path) — the scheduler handles ordering. In #3 the
imagefield type is deliberately a plain path string typed by hand; this issue replaces that input with the real picker and adds the media backend. The issue body'sdocs/DECISIONS.md D18reference points at a file that is not committed — this brief is self-contained.Current behavior:
The harness serves the SolidJS shell and a supervised
hugo serverpreview, and the content engine round-trips.mdfiles through a typed element tree with canonical serialization. Once #3 lands, element params are editable through schema-generated forms and saved canonically, but animageparam is just a text input holding a path: nothing can upload a file, no media endpoints exist, no binary asset is ever validated or transformed, and there is noassets/media/library. Sample-site pages are standalone.mdfiles, not leaf bundles.Desired behavior:
Picker. An
imagefield in a generated form opens a picker dialog with two tabs: Upload and Library. Upload from the page-editing context stores the file in that page's leaf bundle (the directory of itsindex.md) and stores a page-relative reference in the param. If the page is currently a standalone.mdfile rather than a bundle, the first bundle upload must still work — e.g. by transparently converting the page to a leaf bundle (slug.md→slug/index.md) with its content preserved byte-for-byte; how is the implementer's choice, but the user never sees a "this page can't hold images" dead end. The Library tab browses the global library, shows folders and image thumbnails, allows uploading into the library, and picking an asset stores a stable reference (distinguishable from a bundle-relative path) that the site's shortcode templates can resolve and that survives the canonical round-trip unchanged.Global library. Shared assets live under
assets/media/in the site directory, organized in subfolders. The harness API exposes: list (folders + items with thumbnail URLs), upload (multipart), and delete. Thumbnails for browsing are generated/served by the harness and must not add derivative files to the repo.Enforced types. Only jpg, png, webp, avif, and PDF are accepted — decided by content sniffing (magic bytes / decoding), never by file extension or client-declared MIME type. SVG, video, executables, and everything else are rejected with a clear, human-readable message shown in the picker. A file whose sniffed type is allowed but whose extension disagrees must not be stored under the misleading extension.
Size and recompression. Uploads over the cap (~15 MB, defined as a single named constant) are rejected with a clear message before processing. Accepted images are downscaled and recompressed server-side before being written anywhere inside the repo: dimensions are clamped to a documented maximum edge length (e.g. ~2560 px; exact value is the implementer's choice, but it must be a named, documented constant), EXIF orientation is applied so the stored pixels are upright, and the result is re-encoded at a sensible quality. The original upload bytes and original dimensions never enter the repo. PDFs are sniffed and size-capped but stored unmodified. If the server cannot encode a given input format after processing, re-encoding to another allowed format is acceptable; silently storing the unprocessed original is not.
Alt text. When an image is chosen or uploaded for an image field, alt text is required by default: saving the page with a missing/empty alt on such a field blocks the save with an inline error on that field (same validation surface as #3's required params). A sidecar-level per-field flag opts a specific image field out of the requirement (for decorative images). The alt value persists through the canonical serializer alongside the image reference and is available to the shortcode template; the exact persistence shape (e.g. a paired param) is the implementer's choice.
Delete. Deleting a media item (library or bundle asset) triggers a best-effort reference scan across the site's content files; if references are found, the UI warns and names the referencing pages before the user confirms. Unreferenced items delete without a warning. "Best-effort" means a textual scan is acceptable — it must never block deletion, only inform.
Rendering. Responsive rendering (srcset, sizes, art direction) remains the theme's job — the harness stores one processed asset per upload.
Key interfaces:
imagefield type from #3's sidecar schema: same param value on the shortcode (a string reference), now populated by the picker; the sidecar grows an opt-out flag for the alt requirement on a per-field basis, and alt persistence must round-trip through the content engine.Acceptance criteria:
.mdpage to a bundle, or equivalent, without losing content), a library upload lands underassets/media/evil.svgtoevil.pngdoes not get it throughOut of scope:
Closing as superseded: the maintainer has decided on a clean-slate reset of the project (issue #29, PR #30). The v1 implementation this issue builds on is being removed and the plan will be re-drawn from scratch; this spec is retired rather than actioned.