K KodePressdocumentation
Laravel 11 · MySQL 8 Specification Built 2026-10-10

10 — Media library and form builder#

10.1 Media library#

Media in the sidebar, and the same component opens as a modal from any image or gallery field. One implementation, two placements.

Feature Detail
Upload drag and drop onto the grid, or a file picker; multiple files; per-file progress
Folders media_folders, nestable, drag a file onto a folder to move it; a breadcrumb at the top
Views grid (default) and list with size, dimensions, type, upload date, uploader
Search original name, title, alt text, caption
Filters type (image / document / video), folder, date, unused
Detail panel preview, alt text, title, caption, dimensions, size, URL, Used on N pages, replace, delete
Replace uploads a new file in place, keeping the media.id, so every page using it updates at once
Unused files with no usage index entry; listed so the owner can clean up, never auto-deleted
Bulk move to folder, delete (with a usage warning naming the pages)

Alt text#

The detail panel asks for alt text with a short explanation of why. An image inserted without alt text is flagged in the editor outline with a soft warning, and Dashboard -> Needs attention counts images missing alt text. Decorative images get an explicit This image is decorative toggle, which emits alt="" — so a missing alt is always a mistake, never a choice we cannot distinguish.

Upload limits and validation#

Check Rule
Extensions allowed jpg, jpeg, png, gif, webp, avif, svg, pdf, doc, docx, xls, xlsx, ppt, pptx, zip, mp4, webm, mp3
Extensions blocked anything else, explicitly including php, phtml, php3..8, phar, sh, exe, bat, cgi, pl, py, htaccess, htm, html
MIME checked with finfo against the extension; a mismatch is rejected
Images re-encoded through the image pipeline, which destroys embedded payloads
SVG sanitised (scripts, event handlers, external references and <foreignObject> removed) before storage; if sanitisation changes nothing suspicious it is kept, otherwise rejected with a message
Size default 8 MB per file, configurable, capped by upload_max_filesize — the screen shows the server limit, because on shared hosting it is usually the real constraint
Filename slugified, de-duplicated with a numeric suffix; original kept in original_name
Storage path media/{year}/{month}/{slug}.{ext} on the public disk

Uploads are stored outside the webroot-executable path only by virtue of being on the public disk under storage/app/public, served via the public/storage symlink. The deploy checklist verifies that the webserver does not execute PHP under storage/, and the installer writes a defensive .htaccess (php_flag engine off, RemoveHandler, ForceType text/plain) into the media directory for Apache/LiteSpeed hosts.

Image pipeline#

upload
  -> validate (extension, mime, size)
  -> read with the detected driver (imagick, else gd)
  -> strip EXIF except orientation; apply orientation
  -> resize down if wider than `media.max_width` (default 2560)
  -> encode primary as WebP (quality 82); keep the original file as well
  -> generate variants: thumb 320w, medium 768w, large 1536w (WebP, skipping sizes larger than the source)
  -> write media row with conversions{} and dimensions

10.2 Form builder#

Settings -> Forms lists forms; the builder edits one. The form block on a page selects a form by id, so the same form can appear on several pages and all submissions land in one place.

Field types#

Type Stored value Validation offered
text string required, min, max
textarea string required, min, max
email string required, email format
phone string required, pattern (Bangladesh pattern offered as a preset)
number number required, min, max
select string required, one of the options
radio string required, one of the options
checkbox bool required (must be checked)
checkbox_group array required, min/max selections
date date required, min, max
file media id required, allowed types, max size
hidden string — (page URL, UTM values, a campaign id)
heading — layout only
paragraph — layout only
consent bool required; the label is richtext so it can link a privacy page

Each field has: key (auto from the label, editable, unique), label, placeholder, help, required, width (full / half / third), options (for choice types), default, and conditional (show this field when another field equals a value).

Form settings#

Setting Default
Submit button label translated Send
On success show a message (default) or redirect to a page
Success message translated default, editable richtext
Notify emails the admin email
Email subject and body templates with {{field_key}} placeholders
Reply-to the form's email field, when one exists
Store submissions on
Honeypot on — a hidden field plus a minimum fill time of 2 seconds
Rate limit 5 submissions per IP per 10 minutes, per form
reCAPTCHA / hCaptcha optional, keys in Settings; off by default (no third-party calls unless asked)

Submission flow#

POST /_kp/forms/{form}
  -> rate limit (per IP + form) and honeypot / fill-time check -> silent 200 on failure for bots
  -> validate against forms.fields (server-side, always; client-side hints too)
  -> handle file fields: validate, run the image pipeline if it is an image, create media rows
  -> store a form_submissions row (when store_submissions)
  -> send notification email (queue connection is sync, so this happens inline)
  -> audit log entry
  -> respond: Livewire swaps in the success message, or redirects

Submissions screen#

Per form: a table of submissions with the first three fields as columns, read/unread state, a detail drawer showing every field and the uploaded files, Mark as read, Delete, and Export CSV (current filter, UTF-8 with a BOM so Excel opens Bengali correctly). Unread counts appear as a badge on the sidebar Settings item and on the Dashboard.

Retention: forms.settings.retain_days (default null = forever). When set, a nightly command deletes older submissions and their uploaded files, and the setting screen states what will be deleted.

10.3 Newsletter block#

A thin wrapper: it creates (on first use) a form with a single email field and a consent checkbox, and renders inline. It is a separate block only because it appears in footers constantly and should be one click, not six.

Integrations with external mailing services are out of scope for core; the submission is stored and can be exported. A plugin may push to a provider.

10.4 Backup and restore (Phase 4)#

Settings -> Backup offers a one-click backup producing a ZIP containing a mysqldump-equivalent SQL file (written with PHP, since mysqldump may be unavailable) and the media directory.

Shared hosting makes this the most failure-prone feature in the product, so:

10.5 Tests for this document#

Test Asserts
php_upload_rejected by extension and by a spoofed MIME
svg_with_script_rejected_or_sanitised no <script> survives storage
image_converted_to_webp_with_variants three variants, correct dimensions, originals kept
oversized_image_is_downscaled to max_width
img_tag_has_dimensions_and_lazy and the hero image is eager
replacing_media_updates_every_page id stable, pages re-rendered
deleting_used_media_warns_with_page_list and requires confirmation
form_validates_server_side invalid POST returns errors even with client checks bypassed
honeypot_and_fill_time_block_bots silent 200, nothing stored
form_rate_limit_per_ip_and_form the 6th submission in 10 minutes is refused, and a different form is unaffected
submission_stored_when_mail_fails mail exception swallowed, row present, failure logged
csv_export_has_bom_and_bengali_intact round-trips through a UTF-8 read
backup_resumes_after_interruption state file drives a resume, archive is complete

KodePress documentation · generated from the Markdown sources by tools/build-docs-site.py · internal preview, not indexed.