/** * This Source Code is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright (c) Infonomic Company Limited */ import type { AfterStoreContext, BeforeStoreContext, CollectionFieldData } from '@byline/core' import { defineCollection, defineWorkflow } from '@byline/core' // ---- Schema (server-safe, no UI concerns) ---- /** * Media — the reference upload collection. * * Upload-capability is declared on individual `image` / `file` fields via * an `upload` block. The auto-mounted endpoint at * `POST /admin/api//upload` * accepts a `field` selector to choose which upload-capable field * receives the file (default when only one such field exists). * * Other collections reference items from this collection via a `relation` * field pointing at `'media'` — the populated relation envelope carries * the persisted `variants` array so a `` / `srcset` can be built * without a second round-trip. * * @example * ```ts * // In another collection's fields: * { * name: 'featuredImage', * label: 'Featured Image', * type: 'relation', * targetCollection: 'media', * displayField: 'title', * } * ``` */ export const Media = defineCollection({ path: 'media', labels: { singular: 'Media Item', plural: 'Media', }, useAsTitle: 'title', workflow: defineWorkflow({ draft: { label: 'Draft', verb: 'Revert to Draft' }, published: { label: 'Active', verb: 'Activate' }, archived: { label: 'Archived', verb: 'Archive' }, }), showStats: true, // Admin list-view quick-search. The collection list route's search box // matches these top-level text-store fields with adapter-native substring // queries against `store_text` — a lightweight "find the row I mean" // affordance that needs no indexing and no lifecycle hooks. Falls back to // `useAsTitle` when omitted; declared explicitly here for guidance. listSearch: ['title'], // Search-*provider* config (docs/06-search/index.md) — // distinct from `listSearch` above. This drives the configured full-text // SearchProvider and offers richer options: // per-field weighting/`boost`, `facets`, `filters`, and `zones`. Unlike // `listSearch`, `search` is inert on its own — it MUST be paired with index / // reindex / deindex document-lifecycle hooks that keep the provider index in // sync on create / update / publish / delete (see this collection's hooks). search: { body: ['title', 'caption'] }, fields: [ { name: 'image', label: 'Image', type: 'image', helpText: 'Select an image for this media item.', upload: { // Allow common image types. Extend with 'video/*', 'application/pdf' // etc. for a more general media field. mimeTypes: [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml', ], // 20 MB limit per file. maxFileSize: 20 * 1024 * 1024, // Named Sharp variants generated after the original is stored. // AVIF is widely supported across modern browsers (Chrome 85+, // Firefox 93+, Safari 16.4+) and typically yields ~20–30 % // smaller files than webp at comparable quality. Sharp's avif // defaults to a lower numeric quality (~50) than webp; the // values below are tuned for that — image-processor.ts uses // `quality: size.quality ?? 55` for the avif branch. sizes: [ { name: 'thumbnail', width: 400, height: 400, fit: 'cover', format: 'avif', quality: 55, }, { name: 'card', width: 600, fit: 'inside', format: 'avif', quality: 55, }, { name: 'mobile', width: 768, fit: 'inside', format: 'avif', quality: 55, }, { name: 'tablet', width: 1280, fit: 'inside', format: 'avif', quality: 55, }, { name: 'desktop', width: 2100, fit: 'inside', format: 'avif', quality: 55, }, ], // Worked example: definition-attached inline hooks are appropriate // when their entire implementation is isomorphic-safe. Hooks that // import server-only code belong in collections/server-hooks.ts. hooks: { beforeStore: (ctx: BeforeStoreContext) => { console.log('beforeStore hook called', ctx) }, afterStore: (ctx: AfterStoreContext) => { console.log('afterStore hook called', ctx) }, }, }, }, // Descriptive metadata fields. { name: 'title', label: 'Title', type: 'text', helpText: 'A short, descriptive title for this media item.', }, { name: 'altText', label: 'Alt Text', type: 'text', helpText: 'Descriptive text for screen readers and SEO. Recommended for images.', }, { name: 'caption', label: 'Caption', type: 'textArea', optional: true, helpText: 'Optional caption displayed beneath the image in the front-end.', }, { name: 'credit', label: 'Credit / Attribution', type: 'text', optional: true, helpText: 'Photographer, agency, or copyright holder.', }, ], }) export type MediaFields = CollectionFieldData