Skip to content

Commit 75a691f

Browse files
committed
feat: refactored attribute handling, all attributes can be overridden
1 parent cd0fbab commit 75a691f

File tree

4 files changed

+192
-52
lines changed

4 files changed

+192
-52
lines changed

README.md

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ You can choose from many options to customize your images and pass them to the I
147147

148148
For each HTML element of the picture element you can add attributes, CSS classes, inline-styles, data-attributes and so on. You only need to add your attributes to one of these three attribute categories, which I call "loading modes": `shared`, `eager` and `lazy`. The `shared` mode is for attributes that should exists always, no matter what loading mode the image is. If you have attributes that should only be used in `eager` or `lazy` loading mode you can add them to one of it. Imagex will merge the `shared` attributes with the attributes of the current loading mode automatically. The attributes of the non-applicable loading mode will have no effect then.
149149

150+
**All attributes can be overridden:** User-defined attributes always take precedence over default attributes generated by Imagex. This means you can override any attribute, including `src`, `srcset`, `loading`, `width`, `height`, and others. If you don't specify an attribute, Imagex will use its default values as fallback. For `class` and `style` attributes, user values are merged with defaults rather than replaced.
151+
152+
⚠️ **Note:** When overriding dimension-related attributes (`width`, `height`, `src`, `srcset`), be aware that Imagex calculates dimensions based on the `ratio` parameter. Overriding these attributes makes you responsible for maintaining consistency between dimensions and image sources. [See detailed explanation below](#overriding-default-attributes).
153+
150154
| Option | Default | Type | Description |
151155
| ------ | ------- | ---- | ----------- |
152156
| `image` || File-Object | Required. Your initial image. Be sure to return a file object here: `$field->toFile()`. |
@@ -195,8 +199,8 @@ $options = [
195199
'lazy' => [
196200
// extend `shared` attributes in lazy loading mode
197201
],
198-
// Do not add `src`, `srcset` or `loading` or their equivalents for lazy loading (like `data-src`) here.
199-
// These attributes are handled automatically by Imagex and adding them here will throw an exception.
202+
// You can override any attribute generated by Imagex, including `src`, `srcset`, `loading`, etc.
203+
// User-defined attributes always take precedence over Imagex defaults.
200204
],
201205
'srcsetName' => 'my-srcset',
202206
'critical' => $isCritical ?? false,
@@ -213,6 +217,128 @@ $options = [
213217
<?php snippet('imagex-picture', $options) ?>
214218
```
215219

220+
### Overriding Default Attributes
221+
You can override any attribute that Imagex generates by default. User-defined attributes always take precedence.
222+
223+
#### Why Override Attributes?
224+
225+
While Imagex handles most scenarios automatically, there are practical reasons to override certain attributes:
226+
227+
**1. Improved SEO and Social Media Sharing**
228+
Many crawlers (Google, Facebook, Twitter) don't fully understand `<picture>` elements or modern formats and only read the `src` attribute of the `<img>` tag. By default, Imagex uses the smallest image from your srcset as `src` (e.g., 400px width). Overriding `src` with a larger, high-quality image ensures better:
229+
- Google Image Search results
230+
- Social media previews (Open Graph, Twitter Cards)
231+
- SEO rankings with higher-quality images
232+
233+
**2. Custom Lazy Loading Strategies**
234+
Override `src` with a low-quality image placeholder (LQIP) or blur-up effect for custom lazy loading implementations.
235+
236+
**3. Special Loading Behavior**
237+
Override `loading` or other attributes for specific images that need different behavior than the global settings.
238+
239+
#### Basic Example
240+
241+
```php
242+
<?php
243+
$options = [
244+
'image' => $image,
245+
'imgAttributes' => [
246+
'shared' => [
247+
'width' => 500, // Override default width
248+
'height' => 300, // Override default height
249+
],
250+
'lazy' => [
251+
'src' => 'custom-placeholder.jpg', // Override default src
252+
'loading' => 'custom-lazy', // Override lazy loading behavior
253+
],
254+
],
255+
'srcsetName' => 'my-srcset',
256+
];
257+
258+
snippet('imagex-picture', $options);
259+
```
260+
261+
In the basic example above:
262+
- `width` and `height` will be set to your custom values instead of the calculated defaults
263+
- `src` will use your custom placeholder image
264+
- `loading` attribute will be set to `eager` even though Imagex would normally set it to `lazy`
265+
- Attributes not specified by you (like `decoding`, `fetchpriority`, etc.) will still use Imagex defaults
266+
267+
#### SEO-Optimized Example
268+
269+
```php
270+
<?php
271+
// Provide a larger image for crawlers while keeping optimized srcset for browsers
272+
$options = [
273+
'image' => $image,
274+
'imgAttributes' => [
275+
'shared' => [
276+
'src' => $image->thumb(['width' => 1200, 'quality' => 85])->url(), // Large image for crawlers
277+
'alt' => $image->alt(),
278+
],
279+
],
280+
'srcsetName' => 'my-srcset', // Srcset will still use optimized sizes (400w, 800w, etc.)
281+
];
282+
283+
snippet('imagex-picture', $options);
284+
```
285+
286+
In this SEO example:
287+
- Modern browsers use the optimized `srcset` with modern formats (avif, webp) and appropriate sizes
288+
- Crawlers and social media bots get a high-quality 1200px image from the `src` attribute (which serves as a fallback)
289+
- Best of both worlds: Fast loading for users, quality images for SEO
290+
291+
**Note on src as fallback:** The `src` attribute primarily serves as a fallback for very old browsers and is what crawlers read. Modern browsers will always prefer images from `srcset` or `<source>` elements. This means you can safely override `src` with a different aspect ratio or larger size for SEO purposes without affecting the user experience, as browsers will load the correctly-sized images from your `srcset`.
292+
293+
**Example - Different ratio for SEO:**
294+
```php
295+
<?php
296+
// Use 16:9 for browsers, but 1:1 for SEO/social media
297+
$options = [
298+
'image' => $image,
299+
'ratio' => '16/9', // Browser gets 16:9 images via srcset
300+
'imgAttributes' => [
301+
'shared' => [
302+
// Crawlers get a square image
303+
'src' => $image->thumb(['width' => 1200, 'height' => 1200, 'crop' => true])->url(),
304+
],
305+
],
306+
'srcsetName' => 'my-srcset',
307+
];
308+
```
309+
Result: Browsers display 16:9 images (from srcset), crawlers and social media get 1:1 image (from src).
310+
311+
**Important Note on Ratio Handling:**
312+
Imagex automatically calculates image dimensions based on the specified `ratio` parameter. When you override dimension-related attributes (`width`, `height`, `src`, or `srcset`), you become responsible for maintaining consistency:
313+
314+
- **Overriding `width`/`height`**: The `srcset` will still use thumbnails generated with the original ratio, which may not match your custom dimensions.
315+
- **Overriding `src`/`srcset`**: The `width` and `height` attributes will still reflect the calculated ratio, which may not match your custom images.
316+
317+
If you override these attributes, ensure that your custom values are consistent with each other and with the aspect ratio of your images to avoid layout shifts or distorted images.
318+
319+
**Example - Correct way to override dimensions:**
320+
```php
321+
<?php
322+
// If you need custom dimensions, override all related attributes together
323+
$options = [
324+
'image' => $image,
325+
'imgAttributes' => [
326+
'shared' => [
327+
'width' => 600,
328+
'height' => 400, // Make sure this matches your desired ratio (3:2 in this case)
329+
],
330+
'lazy' => [
331+
// If you also override srcset, make sure your images have the same 3:2 ratio
332+
'srcset' => 'custom-300.jpg 300w, custom-600.jpg 600w, custom-900.jpg 900w',
333+
],
334+
],
335+
'srcsetName' => 'my-srcset',
336+
'ratio' => '3/2', // This ratio is now only used for generating the default thumbnails
337+
];
338+
```
339+
Note: In most cases, you should **not** need to override `width`, `height`, or `srcset`. Let Imagex handle these automatically based on your `ratio` parameter for best results.
340+
```
341+
216342
## Cache
217343
Imagex will do some simple calculations per image, like calculating the height by the given width and ratio. Basically Imagex get the srcset definition from the config file, calculate and set the height and output the final config. The result will be cached to reduce unnecessary calculations when you use the same combination of srcset-preset and ratio for other images.
218344

docs/examples/basic-config.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ $options = [
7878
'sizes' => '(min-width: 800px) 400px, 100vw',
7979
'class' => [
8080
'my-image',
81-
$conditionalClass ? 'my-image--variant' : null // let's ssume $conditionalClass is `true`
81+
$conditionalClass ? 'my-image--variant' : null // let's assume $conditionalClass is `true`
8282
],
8383
'style' => [
8484
'object-fit: cover;',
85-
'object-position: ' . $image->toFile()->focus(); . ';'
85+
'object-position: ' . $image->toFile()->focus() . ';'
8686
]
8787
]
8888
]
@@ -138,7 +138,7 @@ $options = [
138138
'class' => ['my-image'],
139139
'style' => [
140140
'object-fit: cover;',
141-
'object-position: ' . $image->toFile()->focus(); . ';'
141+
'object-position: ' . $image->toFile()->focus() . ';'
142142
]
143143
],
144144
],
@@ -168,7 +168,7 @@ $options = [
168168
https://example.com/image-400x225-crop-52-65-q75-sharpen10.webp 400w,
169169
https://example.com/image-800x450-crop-52-65-q75-sharpen10.webp 800w">
170170
<img
171-
class="my-image my-image--variant" sizes="(min-width: 800px) 400px, 100vw"
171+
class="my-image" sizes="(min-width: 800px) 400px, 100vw"
172172
style="object-fit: cover; object-position: 52% 65%;"
173173
width="400" height="225" decoding="async" loading="lazy"
174174
alt="A cat sits in the sun in front of yellow flowers."

helpers/attributes.php

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -38,49 +38,15 @@ function validateAttributeTypes(array $options): void
3838
}
3939
}
4040

41-
/**
42-
* Validates the attributes array against a set of disallowed attributes for each loading mode.
43-
*
44-
* @param array $options The attributes array to validate
45-
* @throws InvalidArgumentException If disallowed attributes are detected in any loading mode.
46-
*/
47-
function validateAttributes(array $options): void
48-
{
49-
$disallowedAttributes = [
50-
'shared' => ['type', 'src', 'data-src', 'srcset', 'data-srcset', 'loading', 'width', 'height'],
51-
'eager' => ['srcset', 'data-srcset', 'loading'],
52-
'lazy' => ['data-srcset', 'loading'],
53-
];
54-
55-
$violations = [];
56-
57-
foreach ($options as $loadingMode => $attributes) {
58-
// Check if there are disallowed attributes defined for the current loading mode
59-
if (!isset($disallowedAttributes[$loadingMode])) {
60-
continue;
61-
}
62-
63-
foreach ($attributes as $attribute => $value) {
64-
// Check if the attribute is disallowed in the current loadingMode
65-
if (in_array($attribute, $disallowedAttributes[$loadingMode])) {
66-
$violations[] = "attribute \"$attribute\" in \"$loadingMode\"";
67-
}
68-
}
69-
}
70-
71-
if (!empty($violations)) {
72-
throw new InvalidArgumentException('[kirby-imagex] Disallowed attributes detected: ' . implode(', ', $violations) . '.');
73-
}
74-
}
75-
7641
/**
7742
* Merges HTML attributes for different loading modes with optional default values.
7843
*
44+
* User attributes always override default attributes. Defaults are used as fallback.
7945
* Extend 'shared' by 'eager' or 'lazy' loading mode attributes.
8046
*
81-
* @param array $options Attributes structured by loading modes.
47+
* @param array $attributes User-defined attributes structured by loading modes.
8248
* @param string $loadingMode The loading mode to merge attributes for.
83-
* @param array $defaultOptions Optional default attributes to apply before merging.
49+
* @param array $defaultAttributes Optional default attributes to apply as fallback.
8450
* @return array Merged array of HTML attributes for specified loading mode.
8551
* @throws InvalidArgumentException If $loadingMode is invalid or missing.
8652
*/
@@ -89,8 +55,6 @@ function mergeHTMLAttributes(array $attributes, string $loadingMode, array $defa
8955
validateAttributeTypes($defaultAttributes);
9056
validateAttributeTypes($attributes);
9157

92-
validateAttributes($attributes);
93-
9458
if (!in_array($loadingMode, ['shared', 'eager', 'lazy'])) {
9559
throw new InvalidArgumentException("[kirby-imagex] Invalid loadingMode: \"$loadingMode\".");
9660
}
@@ -100,7 +64,7 @@ function mergeHTMLAttributes(array $attributes, string $loadingMode, array $defa
10064
}
10165

10266
$mergableAttributes = ['class', 'style'];
103-
$mergedAttributes = $defaultAttributes['shared'] ?? [];
67+
$mergedAttributes = [];
10468

10569
// Function to merge attributes, handling both array and string values
10670
$mergeAttributeValues = function ($key, $currentValue, $newValue) use ($mergableAttributes) {
@@ -118,21 +82,28 @@ function mergeHTMLAttributes(array $attributes, string $loadingMode, array $defa
11882
}
11983
};
12084

121-
// Merge default loading mode-specific attributes
85+
// Step 1: Start with default 'shared' attributes
86+
if (isset($defaultAttributes['shared'])) {
87+
foreach ($defaultAttributes['shared'] as $attr => $value) {
88+
$mergedAttributes[$attr] = $value;
89+
}
90+
}
91+
92+
// Step 2: Merge default loading mode-specific attributes
12293
if (isset($defaultAttributes[$loadingMode])) {
12394
foreach ($defaultAttributes[$loadingMode] as $attr => $value) {
12495
$mergedAttributes[$attr] = $mergeAttributeValues($attr, $mergedAttributes[$attr] ?? '', $value);
12596
}
12697
}
12798

128-
// Merge shared source attributes
99+
// Step 3: Merge/override with user 'shared' attributes (user attributes have priority)
129100
if (isset($attributes['shared'])) {
130101
foreach ($attributes['shared'] as $attr => $value) {
131102
$mergedAttributes[$attr] = $mergeAttributeValues($attr, $mergedAttributes[$attr] ?? '', $value);
132103
}
133104
}
134105

135-
// Merge loading mode-specific source attributes
106+
// Step 4: Merge/override with user loading mode-specific attributes (highest priority)
136107
if (isset($attributes[$loadingMode])) {
137108
foreach ($attributes[$loadingMode] as $attr => $value) {
138109
$mergedAttributes[$attr] = $mergeAttributeValues($attr, $mergedAttributes[$attr] ?? '', $value);

tests/attributesTest.php

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,18 @@ public function testValidateAttributesValidInput()
2222
validateAttributes($options);
2323
}
2424

25-
public function testValidateAttributesInvalidInput()
25+
public function testValidateAttributesAllowsAllAttributes()
2626
{
27-
$this->expectExceptionMessage('[kirby-imagex] Disallowed attributes detected: attribute "type" in "shared", attribute "srcset" in "eager", attribute "loading" in "lazy".');
28-
27+
// All attributes are now allowed and can be overridden by users
2928
$options = [
3029
'shared' => ['type' => 'image/jpeg', 'data-attribute' => 'my-attr-value', 'style' => ['background: red;']],
3130
'eager' => ['data-src' => 'image-eager.jpg', 'srcset' => 'image-eager.jpg'],
3231
'lazy' => ['loading' => 'lazy', 'src' => 'image-lazy.jpg'],
3332
];
3433

34+
// Expect no exception
35+
$this->expectNotToPerformAssertions();
36+
3537
validateAttributes($options);
3638
}
3739

@@ -128,4 +130,45 @@ public function testMergeHTMLAttributesLazy()
128130

129131
$this->assertEquals($expected, $result);
130132
}
133+
134+
public function testMergeHTMLAttributesUserOverridesDefaults()
135+
{
136+
// Test that user attributes override default attributes
137+
$userAttributes = [
138+
'shared' => [
139+
'width' => 500,
140+
'height' => 300,
141+
],
142+
'lazy' => [
143+
'src' => 'custom-image.jpg',
144+
'loading' => 'eager',
145+
],
146+
];
147+
$loadingMode = 'lazy';
148+
$defaultAttributes = [
149+
'shared' => [
150+
'width' => 1000,
151+
'height' => 600,
152+
'decoding' => 'async',
153+
],
154+
'lazy' => [
155+
'src' => 'default-image.jpg',
156+
'loading' => 'lazy',
157+
'data-src' => 'default-data-image.jpg',
158+
],
159+
];
160+
161+
$expected = [
162+
'width' => 500, // User override
163+
'height' => 300, // User override
164+
'decoding' => 'async', // From defaults
165+
'src' => 'custom-image.jpg', // User override
166+
'loading' => 'eager', // User override
167+
'data-src' => 'default-data-image.jpg', // From defaults (not overridden by user)
168+
];
169+
170+
$result = mergeHTMLAttributes($userAttributes, $loadingMode, $defaultAttributes);
171+
172+
$this->assertEquals($expected, $result);
173+
}
131174
}

0 commit comments

Comments
 (0)