Cover art and screenshots
A mod's discovery presence — its card in browse listings and its /games/<slug> details page — uses a square cover image, a matching thumbnail, and up to 12 landscape screenshots. All of it lives in the reserved media/ folder (packages/shared/src/modMedia.ts), a convention with real enforcement behind it, not just a naming suggestion.
Target sizes and encoding#
| Asset | Target size | Constant |
|---|---|---|
| Cover image | 512 × 512 px (square) | COVER_SIZE = 512 |
| Cover thumbnail | 256 × 256 px (square) | COVER_THUMB_SIZE = 256 |
| Screenshot | 1280 × 720 px (landscape) | SCREENSHOT_WIDTH = 1280, SCREENSHOT_HEIGHT = 720 |
All processed media is encoded WebP at quality 0.7 (MEDIA_WEBP_QUALITY = 0.7) by the editor's media pipeline.
Conventional paths#
media/cover.webp ← mediaCoverPath()
media/cover-thumb.webp ← mediaCoverThumbPath()
media/screenshot-1.webp ← mediaScreenshotPath(1)
media/screenshot-2.webp ← mediaScreenshotPath(2)
…
coverImage and each entry in screenshots[] are ordinary repo-path strings in the manifest — see Manifest reference § coverImage and § screenshots. Nothing forces the exact filenames above; what's enforced is that the path lives under media/ and is an image.
Three real rules, not just convention#
-
Hidden from the editor's file explorer.
TableEditModeShell.tsxfiltersmedia/paths out of the project-files list (isMediaPath()) — it's a production folder for the media pipeline, not something you browse or hand-edit alongside your other assets. -
Image-only at scan time. The scanner's
validateMediaImageAsset()runs on every file whose path starts withmedia/, whether or not it's separately listed inassets[]— cover/screenshot paths referenced only fromcoverImage/screenshots[]are hashed and validated exactly like a declared asset (collectUndeclaredMediaTargets()sweeps them in). Allowed extensions here are a 4-item subset of the general asset allowlist:.webp .png .jpg .jpeg. Anything else undermedia/— including otherwise-valid asset types like.glbor.mp3— is rejected:`Files under media/ must be images (${sorted list of .bmp/.gif/.jpg/.jpeg/.png/.webp}); got ${extension || "no extension"}.`(codemedia-unsupported-type)(PNG/JPEG are accepted alongside WebP so an author can commit a raw source image; the editor's own pipeline always writes WebP.)
-
Path safety, checked before the image rule.
coverImage/screenshots[].pathmust be safe relative paths that actually resolve undermedia/— a value trying to point outside it fails first with:"coverImage and screenshots must be relative paths under the media/ folder."(codemedia-path-invalid)
The screenshot crop rectangle#
Each entry in screenshots[] may carry an optional crop: a normalized rectangle (x, y, w, h, each a fraction 0–1 of the source image) recording how the published screenshot was framed, so the editor can re-open the original and re-crop rather than forcing a re-upload.
{ "path": "media/screenshot-1.webp", "crop": { "x": 0, "y": 0.1, "w": 1, "h": 0.8 } }
Constraints: x, y ∈ [0, 1]; w, h ∈ (0, 1] (strictly greater than 0); and the rectangle must stay inside the source image — checked as x + w ≤ 1.0001 and y + h ≤ 1.0001 (the 0.0001 slack absorbs floating-point rounding from the crop UI, nothing more). Violating either axis produces the same message:
"Crop rectangle must stay within the image bounds (x+w and y+h ≤ 1)."
Worked example — a complete media declaration#
"coverImage": "media/cover.webp",
"screenshots": [
{ "path": "media/screenshot-1.webp" },
{ "path": "media/screenshot-2.webp", "crop": { "x": 0.05, "y": 0, "w": 0.9, "h": 1 } }
]
media/cover-thumb.webp doesn't need its own manifest field — it's a derived thumbnail the editor generates alongside the full cover and serves from the same reserved folder.
See also#
- Manifest reference §
coverImage/screenshots— the manifest-side field shapes. - Assets — the general 18-extension allowlist that
media/'s 4-extension rule narrows. - Required and conventional files — where
media/sits in the overall convention table.
