All skills

Craft CMS 5 front-end Twig development — atomic design, template architecture, components, Vite buildchain. Covers atoms/molecules/organisms, props/extends/block patterns, layout chains, view routing, content builders, image presets, Tailwind named-key collections, multi-brand CSS tokens, JavaScript boundaries (Alpine/DataStar/Vue, tabs, accordions), Vite asset loading, and front-end auth (login, registration, password reset, profiles). Triggers on: {% include ... only %}, {% embed %}, _atoms/_molecules/_organisms/_views/_builders, component--variant.twig, _component--props.twig, collect({}), utilities prop, data-brand theming, hero/card components, Matrix block rendering, craft.vite.script, vite.php, vite.config.ts, buildchain, per-page scripts, Blitz static/page caching, ImageOptimize, Imager-X, responsive images, srcset, image transforms, SEOmatic meta/OpenGraph/JSON-LD, Sprig, htmx, multi-language, hreflang, localization, Formie form styling, login/registration form, RSS/Atom/JSON feeds, XML sitemap, search page, .search(), headless GraphQL, Next.js/Nuxt/Astro integration, example-templates command, render builder, fluent BaseTag {{ tag.render() }}, progressive enhancement. Always use when creating, editing, or reviewing Craft front-end Twig templates, components, layouts, views, builders, buildchain, or front-end auth — including plugin template integration (Blitz, SEOmatic, Sprig, Formie, Imager-X). Do NOT trigger for PHP plugin/module development (craftcms) or content modeling (craft-content-modeling).

Use this Skill: https://skilld.dev/gh/michtio/craftcms-claude-skills/craft-site

This session only. Nothing lands on disk.

referencesimage-presets.md

≈2.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Image Presets Reference

One image atom, many presets. Config-driven responsive images.

Documentation

Common Pitfalls

  • Hardcoded dimensions in templates — never write width: 800 directly in a view. Always go through presets.
  • Missing alt text fallback chain — always: explicit alt → asset alt field → asset title → empty string.
  • loading="eager" everywhere — only the first visible image (above-fold hero) should be eager. Everything else is lazy.
  • Missing sizes attribute — without it, the browser downloads the largest srcset image regardless of viewport.
  • Provider lock-in — don't call ImageOptimize API directly in views. The image atom abstracts the provider.
  • Multiple image variant files — use one atom with presets, not image--hero.twig, image--teaser.twig, etc.

The Single Image Atom

Instead of multiple image variant files, one universal atom with a preset parameter.

Props File

{# _atoms/images/_image--props.twig #}

{# Preset definitions — widths, sizes, loading strategy, aspect ratio #}
{%- set presets = {
    hero: {
        widths: [640, 960, 1280, 1920, 2560],
        sizes: '100vw',
        loading: 'eager',
        ratio: null,
    },
    teaser: {
        widths: [300, 450, 600, 900],
        sizes: '(min-width: 1024px) 33vw, 100vw',
        loading: 'lazy',
        ratio: 'aspect-[16/9]',
    },
    content: {
        widths: [400, 600, 900, 1200],
        sizes: '(min-width: 1024px) 50vw, 100vw',
        loading: 'lazy',
        ratio: null,
    },
    profile: {
        widths: [150, 300, 450],
        sizes: '150px',
        loading: 'lazy',
        ratio: 'aspect-square',
    },
    branding: {
        widths: [200, 400],
        sizes: '200px',
        loading: 'lazy',
        ratio: null,
    },
    gallery: {
        widths: [400, 600, 900, 1200, 1800],
        sizes: '(min-width: 1024px) 50vw, 100vw',
        loading: 'lazy',
        ratio: null,
    },
} -%}

{%- set config = presets[preset ?? 'content'] ?? presets.content -%}

{%- set props = collect({
    image: image ?? null,
    alt: alt ?? null,
    config: config,
    ratio: ratio ?? config.ratio ?? null,
    loading: config.loading,
    utilities: utilities ?? null,
}) -%}

{%- block image -%}{%- endblock -%}

Variant File

{# _atoms/images/image--responsive.twig #}
{%- extends '_atoms/images/_image--props' -%}

{%- block image -%}

    {%- set asset = props.get('image') -%}

    {%- if asset -%}
        {%- set alttext = props.get('alt') ?? asset.alt ?? asset.title -%}

        {%- set classes = collect({
            display: 'block w-full h-auto',
            ratio: props.get('ratio'),
            fit: props.get('ratio') ? 'object-cover' : null,
            utilities: props.get('utilities'),
        }) -%}

        {# With ImageOptimize plugin #}
        {%- if asset.optimizedImages is defined -%}

            <picture>
                {%- if asset.optimizedImages.srcsetWebp() -%}
                    {{ tag('source', {
                        type: 'image/webp',
                        srcset: asset.optimizedImages.srcsetWebp(),
                        sizes: props.get('config').sizes,
                    }) }}
                {%- endif -%}

                {{ tag('img', {
                    class: classes.implode(' '),
                    src: asset.optimizedImages.src(),
                    srcset: asset.optimizedImages.srcset(),
                    sizes: props.get('config').sizes,
                    alt: alttext,
                    loading: props.get('loading'),
                    width: asset.width,
                    height: asset.height,
                    style: asset.optimizedImages.placeholderBox() ? {
                        'background-image': "url('" ~ asset.optimizedImages.placeholderImage() ~ "')",
                        'background-size': 'cover',
                    } : false,
                }) }}
            </picture>

        {# With Craft Cloud / native transforms #}
        {%- else -%}
            {%- set webpSrcset = props.get('config').widths|map(w =>
                asset.getUrl({ width: w, format: 'webp' }) ~ ' ' ~ w ~ 'w'
            )|join(', ') -%}
            {%- set srcset = props.get('config').widths|map(w =>
                asset.getUrl({ width: w }) ~ ' ' ~ w ~ 'w'
            )|join(', ') -%}

            <picture>
                {{ tag('source', {
                    type: 'image/webp',
                    srcset: webpSrcset,
                    sizes: props.get('config').sizes,
                }) }}

                {{ tag('img', {
                    class: classes.implode(' '),
                    src: asset.getUrl({ width: props.get('config').widths|last }),
                    srcset: srcset,
                    sizes: props.get('config').sizes,
                    alt: alttext,
                    loading: props.get('loading'),
                    width: asset.width,
                    height: asset.height,
                }) }}
            </picture>
        {%- endif -%}
    {%- endif -%}

{%- endblock -%}

Preset Definitions

Preset Use Case Widths Sizes Loading
hero Full-width hero images 640–2560 100vw eager
teaser Card thumbnails, grid items 300–900 33vw at desktop lazy
content Inline content images 400–1200 50vw at desktop lazy
profile Author photos, avatars 150–450 150px lazy
branding Logos, partner logos 200–400 200px lazy
gallery Gallery/lightbox images 400–1800 50vw at desktop lazy

Preset Naming

Lowercase context names: hero, teaser, content, profile, branding, gallery.

Adding Project-Specific Presets

Add to the presets hash in _image--props.twig:

{%- set presets = presets|merge({
    floorplan: {
        widths: [600, 900, 1200],
        sizes: '(min-width: 768px) 50vw, 100vw',
        loading: 'lazy',
        ratio: null,
    },
}) -%}

Calling the Image Atom

{# Standard usage #}
{%- include '_atoms/images/image--responsive' with {
    image: entry.heroImage.eagerly().one(),
    preset: 'hero',
} only -%}

{# With alt text override #}
{%- include '_atoms/images/image--responsive' with {
    image: entry.teamPhoto.one(),
    preset: 'profile',
    alt: entry.memberName,
    utilities: 'rounded-full',
} only -%}

{# With aspect ratio override #}
{%- include '_atoms/images/image--responsive' with {
    image: entry.thumbnail.one(),
    preset: 'teaser',
    ratio: 'aspect-[4/3]',
} only -%}

Hosting / Transform Strategies

Strategy When to use Notes
ImageOptimize plugin VPS / Servd hosting Full srcset/webp generation at upload. Best quality control. Not compatible with Craft Cloud.
Craft Cloud transforms Craft Cloud hosting Native Craft transforms. Craft Cloud intercepts and serves via Cloudflare Images — no special template syntax needed. See the craft-cloud skill's assets-and-transforms.md for the edge transform limits (70MB, 100MP, 12,000px max).
Small Pics / Cloudflare Images External transform service URL-based. Best for CDN-first architectures.

The image atom handles both paths. It checks for the optimizedImages field handle (ImageOptimize plugin) and falls back to native Craft transforms via asset.getUrl(). On Craft Cloud, these native transforms are automatically served through Cloudflare Images — the template code is identical either way.

A11y Requirements

  • alt is always required. Fallback chain: explicit alt prop → asset alt field → asset title.
  • Decorative images: pass alt: '' explicitly. Never omit.
  • loading="eager" only on hero/above-fold images. Everything else is lazy.
  • width and height always rendered to prevent CLS.

DS Module Vision (Future)

With the craft.ds module, image rendering collapses to:

{{ craft.ds.image(entry.heroImage.one(), 'hero') }}

The PHP service resolves the preset, detects the hosting environment, generates the <picture> element, and handles all transform logic server-side. The vanilla Twig approach documented here is the foundation that the module will formalize.

Source: SKILL.md on GitHub

1 warning16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill is generally safe and follows professional Craft CMS development practices. A low-severity risk regarding indirect prompt injection was identified due to the typical architectural pattern of rendering rich-text content from the CMS database into the front-end templates.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

Signed by skilld at d3a91c1. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago

README badge

README badge for michtio/craftcms-claude-skills/craft-site