Skip to content

Commit c87e79c

Browse files
authored
feat: append optional image filenames in HTML and JSON pipeline output (#72)
- Add transformImages step to productHTMLPipe and productJSONPipe that appends filename to media URLs when present on image objects - Update isMediaBusFile check in create-pictures to handle URLs with filename path segment
1 parent d3b7c50 commit c87e79c

11 files changed

Lines changed: 274 additions & 11 deletions

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"dependencies": {
4848
"@adobe/helix-html-pipeline": "6.27.9",
4949
"@adobe/helix-shared-utils": "3.0.2",
50-
"@dylandepass/helix-product-shared": "1.5.2",
50+
"@dylandepass/helix-product-shared": "1.6.0",
5151
"dayjs": "1.11.19",
5252
"github-slugger": "2.0.0",
5353
"hast-util-from-html": "^2.0.3",

src/product-html-pipe.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import addHeadingIds from './steps/add-heading-ids.js';
1616
import { getPathInfo, validatePathInfo } from './utils/path.js';
1717
import initConfig from './steps/init-config.js';
1818
import fetchProductBusContent from './steps/fetch-productbus.js';
19+
import transformImages from './steps/transform-images.js';
1920
import fetchEdgeContent from './steps/fetch-edge-product.js';
2021
import { setLastModified } from './utils/last-modified.js';
2122
import html from './steps/make-html.js';
@@ -71,6 +72,7 @@ export async function productHTMLPipe(state, req) {
7172
return res;
7273
}
7374

75+
transformImages(state);
7476
state.timer?.update('render');
7577
await html(state);
7678

src/product-json-pipe.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
1515
import { validatePathInfo } from './utils/path.js';
1616
import initConfig from './steps/init-config.js';
1717
import fetchProductBusContent from './steps/fetch-productbus.js';
18+
import transformImages from './steps/transform-images.js';
1819
import { setLastModified } from './utils/last-modified.js';
1920
import { set404CacheHeaders, setProductCacheHeaders } from './steps/set-cache-headers.js';
2021

@@ -60,6 +61,8 @@ export async function productJSONPipe(state, req) {
6061
throw new PipelineStatusError(res.status, res.error);
6162
}
6263

64+
transformImages(state);
65+
6366
// set surrogate keys
6467
await setProductCacheHeaders(state, req, res);
6568

src/steps/create-pictures.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,8 @@ export function createOptimizedPicture(src, alt = '', title = undefined) {
5555
height = props.get('height');
5656
}
5757

58-
// Extract filename and check if it starts with 'media_'
59-
const filename = pathname.substring(pathname.lastIndexOf('/') + 1);
60-
const isMediaBusFile = filename.startsWith('media_');
58+
// Matches media bus URLs: /media_{sha1hash}.ext or /media_{sha1hash}/filename.ext
59+
const isMediaBusFile = /^\/media_[a-f0-9]+(?:\/[^/]+)?\.[a-z0-9]+$/i.test(pathname);
6160

6261
// If not a media file, return a simple picture with just an img tag (no optimization)
6362
if (!isMediaBusFile) {

src/steps/transform-images.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2026 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import { appendFilenameToMediaUrl } from '@dylandepass/helix-product-shared';
14+
15+
function transformImage(image) {
16+
if (!image || typeof image !== 'object') {
17+
return;
18+
}
19+
image.url = appendFilenameToMediaUrl(image.url, image.filename);
20+
}
21+
22+
export default function transformImages(state) {
23+
const data = state?.content?.data;
24+
if (!data || typeof data !== 'object') {
25+
return;
26+
}
27+
28+
if (Array.isArray(data.images)) {
29+
data.images.forEach(transformImage);
30+
}
31+
if (Array.isArray(data.variants)) {
32+
data.variants.forEach((variant) => (variant?.images || []).forEach(transformImage));
33+
}
34+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"sku": "product-with-image-filename",
3+
"name": "Test Product",
4+
"type": "simple",
5+
"images": [
6+
{
7+
"url": "./media_a1b2c3d4e5f6789012345678901234567890abcd.png",
8+
"filename": "test-product-image"
9+
}
10+
],
11+
"price": {
12+
"currency": "USD",
13+
"regular": "99.99",
14+
"final": "99.99"
15+
}
16+
}

test/product-html-pipe.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,34 @@ describe('Product HTML Pipe Test', () => {
9292
fetchMock.unmockGlobal();
9393
});
9494

95+
it('transforms image URLs with filename in rendered HTML', async () => {
96+
const fetchMockGlobal = fetchMock.mockGlobal();
97+
fetchMockGlobal.get('https://main--site--org.aem.live/products/product-with-image-filename', { status: 404 });
98+
99+
const s3Loader = new FileS3Loader();
100+
const state = DEFAULT_STATE(DEFAULT_CONFIG, {
101+
log: console,
102+
s3Loader,
103+
ref: 'main',
104+
path: '/products/product-with-image-filename',
105+
partition: 'live',
106+
timer: { update: () => {} },
107+
});
108+
state.info = getPathInfo('/products/product-with-image-filename');
109+
110+
const resp = await productHTMLPipe(
111+
state,
112+
new PipelineRequest(new URL('https://acme.com/products/product-with-image-filename')),
113+
);
114+
115+
assert.strictEqual(resp.status, 200);
116+
assert.ok(
117+
resp.body.includes('./media_a1b2c3d4e5f6789012345678901234567890abcd/test-product-image.png'),
118+
'Rendered HTML should contain the filename-appended image URL',
119+
);
120+
fetchMock.unmockGlobal();
121+
});
122+
95123
it('renders a configurable product html from repoless site', async () => {
96124
const fetchMockGlobal = fetchMock.mockGlobal();
97125
// Mock edge content fetch to return 404 (no authored content)

test/product-json-pipe.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,32 @@ describe('Product JSON Pipe Test', () => {
232232
assert.strictEqual(resp.status, 200);
233233
});
234234

235+
it('transforms image URLs with filename in JSON output', async () => {
236+
const s3Loader = new FileS3Loader();
237+
238+
const state = DEFAULT_STATE({
239+
log: console,
240+
s3Loader,
241+
ref: 'main',
242+
path: '/products/product-with-image-filename.json',
243+
partition: 'live',
244+
timer: { update: () => {} },
245+
});
246+
state.info = getPathInfo('/products/product-with-image-filename.json');
247+
248+
const resp = await productJSONPipe(
249+
state,
250+
new PipelineRequest(new URL('https://acme.com/products/product-with-image-filename.json')),
251+
);
252+
253+
assert.strictEqual(resp.status, 200);
254+
const body = JSON.parse(resp.body);
255+
assert.strictEqual(
256+
body.images[0].url,
257+
'./media_a1b2c3d4e5f6789012345678901234567890abcd/test-product-image.png',
258+
);
259+
});
260+
235261
it('handles state with timer but no update method correctly', async () => {
236262
const s3Loader = new FileS3Loader();
237263
s3Loader.statusCodeOverrides = {

test/steps/create-picture.test.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
/* eslint-env mocha */
1414
import assert from 'assert';
15-
import { constructImageUrl } from '../../src/steps/create-pictures.js';
15+
import { constructImageUrl, createOptimizedPicture } from '../../src/steps/create-pictures.js';
1616

1717
describe('constructImageUrl', () => {
1818
const baseState = {
@@ -109,3 +109,18 @@ describe('constructImageUrl', () => {
109109
assert.strictEqual(result, 'https://main--helix-pages--adobe.aem.network/images/products/featured/test.jpg');
110110
});
111111
});
112+
113+
describe('createOptimizedPicture', () => {
114+
it('creates optimized sources for media hash URL with filename segment', () => {
115+
const picture = createOptimizedPicture(
116+
'./media_13f34abcff863c53e25028911749e9a9d1d6f1c4/blue-ceramic-mug.jpg',
117+
'A blue mug',
118+
'A blue mug',
119+
);
120+
121+
assert.strictEqual(picture.tagName, 'picture');
122+
assert.ok(Array.isArray(picture.children));
123+
assert.ok(picture.children.length > 1, 'expected optimized source elements');
124+
assert.strictEqual(picture.children[0].tagName, 'source');
125+
});
126+
});

0 commit comments

Comments
 (0)