Working with Images

Hi everyone,

I’d like to start a discussion about working with images in OCMS, including SEO optimization, validation, upload restrictions, and compression. I’m interested in hearing about your approaches and the pros and cons of your solutions.

First, I’ll share my own approach.
I created the MediaGallery plugin, which is connected to the media and saves images to JSON as a widgetInput value.

Pros:

  • A-Z and Z-A sorting
  • Searching within additional information
  • Bulk deletion
  • Bulk sorting via drag & drop
  • Add extra fields for additional information
  • Configure widgetInput
  • Deleted images or renamed files are marked as missing and not displayed on the front end
  • Automatic generation of WebP format and HTML tags for the front end
  • Responsive UI

Cons:

  • Additional information must be re-entered for every record of every entry
  • I don’t know where I used which image
  • After deleting or editing, I have to track down where it was used if I want cleaner data in the DB

Optimization:

  • WebP format
  • Additional information on the front end
  • I plan to implement compression and upload size limits




When joining the discussion, please try to follow this format:
Your approach: …
Pros: …
Cons: …
Optimization: …

I’d like to look at this issue from a different angle. I believe this discussion will be very useful for everyone.

1 Like

I’ve just recreated astro js picture component using ai.

{#
Path: themes/your-theme/partials/components/picture.htm

Props:
- src (required): Path to image
- alt (required): Alt text
- width (optional): Base width
- height (optional): Base height
- mode (optional): 'crop', 'auto', 'fit'. Default: 'crop'
- formats (optional): ['webp', 'avif']. Default: ['webp']
- widths (optional): Array for srcset e.g. [600, 1200]

Performance Props:
- loading (optional): 'lazy' | 'eager'. Default: 'lazy'
- decoding (optional): 'async' | 'sync' | 'auto'. Default: 'async'
- fetchpriority (optional): 'high' | 'low' | 'auto'. Default: 'auto'
#}

{# --- Defaults --- #}
{% set formats = formats ?? ['webp'] %}
{% set mode = mode ?? 'auto' %}
{% set loading = loading ?? 'lazy' %}
{% set decoding = decoding ?? 'async' %}
{# fetchpriority defaults to 'auto' (browser default) if not set, but we can make it explicit #}
{% set fetchpriority = fetchpriority ?? 'auto' %}

{# --- File Analysis --- #}
{% set fallbackFormat = src|split('.')|last|lower %}
{% set isVector = fallbackFormat == 'svg' %}
{% set isAnim = fallbackFormat == 'gif' %}

{# --- Render --- #}
{% if isVector or isAnim %}
    {# 1. Special Formats (SVG/GIF) - No processing, just render #}
    <img src="{{ src|resize(width, height, { mode: mode }) }}"
         alt="{{ alt }}"
         class="{{ class }}"
         width="{{ width }}"
         height="{{ height }}"
         loading="{{ loading }}"
         decoding="{{ decoding }}"
         fetchpriority="{{ fetchpriority }}">
{% else %}
    {# 2. Optimized Picture Tag #}
    <picture class="{{ pictureClass }}">

        {# Generate Sources #}
        {% for format in formats %}
            {% set mimeType = 'image/' ~ format %}

            {% if widths is defined and widths is iterable %}
                {% set srcsetString = '' %}
                {% for w in widths %}
                    {% set h = height ? (w * (height / width))|round : null %}
                    {% set resized = src|resize(w, h, { mode: mode, extension: format }) %}
                    {% set srcsetString = srcsetString ~ resized ~ ' ' ~ w ~ 'w' %}
                    {% if not loop.last %}{% set srcsetString = srcsetString ~ ', ' %}{% endif %}
                {% endfor %}
                <source srcset="{{ srcsetString }}" type="{{ mimeType }}" sizes="{{ sizes }}">
            {% else %}
                <source srcset="{{ src|resize(width, height, { mode: mode, extension: format }) }}" type="{{ mimeType }}">
            {% endif %}
        {% endfor %}

        {# Fallback Image Generation #}
        {% set fallbackSrc = '' %}
        {% set fallbackSrcset = '' %}

        {% if widths is defined and widths is iterable %}
            {% for w in widths %}
                {% set h = height ? (w * (height / width))|round : null %}
                {% set resized = src|resize(w, h, { mode: mode }) %}
                {% set fallbackSrcset = fallbackSrcset ~ resized ~ ' ' ~ w ~ 'w' %}
                {% if not loop.last %}{% set fallbackSrcset = fallbackSrcset ~ ', ' %}{% endif %}
            {% endfor %}
            {% set fallbackSrc = src|resize(width, height, { mode: mode }) %}
        {% else %}
            {% set fallbackSrc = src|resize(width, height, { mode: mode }) %}
        {% endif %}

        {# Main IMG Tag #}
        <img src="{{ fallbackSrc }}"
             {% if fallbackSrcset %}srcset="{{ fallbackSrcset }}"{% endif %}
            {% if sizes %}sizes="{{ sizes }}"{% endif %}
             alt="{{ alt }}"
             class="{{ class }}"
             width="{{ width }}"
             height="{{ height }}"
             loading="{{ loading }}"
             decoding="{{ decoding }}"
             fetchpriority="{{ fetchpriority }}">
    </picture>
{% endif %}

and use of it:

{% partial 'picture'
    class="blog__img"
    src = blog.photo
    width = 1200
    height = 600
    alt = blog.title
%}

Great topic! The media manager is showing its age, so I’m interested in this too, especially to hear how others are using it.

My wish list so far:

  • Sidecar table in the database for metadata (alt text, captions, usage tracking)
  • Multisite support, as a subfolder inside the disk
  • Multiple disks support

On upload restrictions: forcing a file size is a more systemic problem. In the eCommerce plugin for example, clients quickly fill up the disk by uploading 4MB product images straight from their phones. A sensible default + per-field override would go a long way.

Curious whether others are solving the “where did I use this image” problem at the media layer or higher up.

Looks nice. Try the props tag if you are in v4.2:

(It’s in beta for 4.2 and will be officially included in v4.3)