@dylandepass/helix-product-pipeline 2.0.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ # [2.2.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.1.0...v2.2.0) (2026-02-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * Remove media pipe and move to helix-product-pipeline-worker ([645d2f5](https://github.com/adobe-rnd/helix-product-pipeline/commit/645d2f5850f0b4f5abfe25e6330c68e31508d812))
7
+
8
+ # [2.1.0](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.0.2...v2.1.0) (2026-02-03)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * support metadata from edge ([8a9c032](https://github.com/adobe-rnd/helix-product-pipeline/commit/8a9c032f2b5e55449a4677e4ce6f4adb62d9c9a2))
14
+ * update tests ([f0cb051](https://github.com/adobe-rnd/helix-product-pipeline/commit/f0cb051da917b2b5c3d56e460f2c9b9284da9a0c))
15
+
16
+
17
+ ### Features
18
+
19
+ * extract metadata from authored edge content and optimize cache keys ([0be7f2b](https://github.com/adobe-rnd/helix-product-pipeline/commit/0be7f2b90424216f3a29d8fd536ac19407281947))
20
+
1
21
  ## [2.0.2](https://github.com/adobe-rnd/helix-product-pipeline/compare/v2.0.1...v2.0.2) (2026-01-29)
2
22
 
3
23
 
package/README.md CHANGED
@@ -34,6 +34,7 @@ Helix Product Pipeline is the shared rendering engine that powers both `helix-pr
34
34
 
35
35
  - Fetches product data from Product Bus storage
36
36
  - Optionally merges with authored content from Edge Delivery Services
37
+ - Extracts metadata from authored content head and merges into product head
37
38
  - Generates responsive picture elements with multiple breakpoints (600px, 750px/2000px)
38
39
  - Creates schema.org JSON-LD structured data for rich search results
39
40
  - Adds heading IDs for navigation and anchor links
@@ -50,12 +51,13 @@ The HTML pipeline executes these processing steps in sequence:
50
51
  4. **make-html** - Create initial HTML structure
51
52
  5. **render-head** - Generate HTML head with meta tags
52
53
  6. **fetch-edge-product** - Optionally fetch authored content from Edge Delivery
53
- 7. **render-body** - Render product content, images, and variants
54
- 8. **render-jsonld** - Generate schema.org JSON-LD structured data
55
- 9. **add-heading-ids** - Add IDs to headings for anchor navigation
56
- 10. **create-pictures** - Generate responsive picture elements
57
- 11. **stringify-response** - Serialize to HTML string
58
- 12. **set-cache-headers** - Set CDN and browser cache headers with surrogate keys
54
+ 7. **extract-authored-metadata** - Extract metadata from authored content and merge into product head
55
+ 8. **render-body** - Render product content, images, and variants
56
+ 9. **render-jsonld** - Generate schema.org JSON-LD structured data
57
+ 10. **add-heading-ids** - Add IDs to headings for anchor navigation
58
+ 11. **create-pictures** - Generate responsive picture elements
59
+ 12. **stringify-response** - Serialize to HTML string
60
+ 13. **set-cache-headers** - Set CDN and browser cache headers with surrogate keys
59
61
 
60
62
  ### CDN Support
61
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dylandepass/helix-product-pipeline",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "Helix Product Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  "dependencies": {
48
48
  "@adobe/helix-html-pipeline": "6.27.9",
49
49
  "@adobe/helix-shared-utils": "3.0.2",
50
- "@dylandepass/helix-product-shared": "1.3.2",
50
+ "@dylandepass/helix-product-shared": "1.5.2",
51
51
  "dayjs": "1.11.19",
52
52
  "github-slugger": "2.0.0",
53
53
  "hast-util-from-html": "^2.0.3",
package/src/global.d.ts CHANGED
@@ -50,7 +50,8 @@ declare global {
50
50
  }
51
51
 
52
52
  export interface PipelineContent extends ImportedPipelineContent {
53
- edge: string;
53
+ edge?: string;
54
+ edgeHast?: Root;
54
55
  data: ProductBusEntry;
55
56
  }
56
57
 
package/src/index.js CHANGED
@@ -15,4 +15,4 @@ export { productHTMLPipe } from './product-html-pipe.js';
15
15
  export { productIndexPipe } from './product-index-pipe.js';
16
16
  export { productMerchantFeedPipe } from './product-merchant-feed-pipe.js';
17
17
  export { productSitemapPipe } from './product-sitemap-pipe.js';
18
- export { productMediaPipe } from './product-media-pipe.js';
18
+ export { applyByoCdnHeaders } from './utils/cache-headers.js';
@@ -26,6 +26,7 @@ import tohtml from './steps/stringify-response.js';
26
26
  import fetch404 from './steps/fetch-404.js';
27
27
  import { setProductCacheHeaders } from './steps/set-cache-headers.js';
28
28
  import { getUnauthorizedBody } from './utils/http-response.js';
29
+ import extractAuthoredMetadata from './steps/extract-authored-metadata.js';
29
30
 
30
31
  export async function productHTMLPipe(state, req) {
31
32
  const { log } = state;
@@ -72,9 +73,11 @@ export async function productHTMLPipe(state, req) {
72
73
 
73
74
  state.timer?.update('render');
74
75
  await html(state);
75
- await renderHead(state);
76
76
 
77
77
  await fetchEdgeContent(state, req, res);
78
+ await extractAuthoredMetadata(state);
79
+
80
+ await renderHead(state);
78
81
  await renderBody(state, req, res);
79
82
  await renderJsonld(state, res);
80
83
  await addHeadingIds(state);
@@ -0,0 +1,120 @@
1
+ /*
2
+ * Copyright 2025 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 { selectAll, select } from 'hast-util-select';
14
+ import { fromHtml } from 'hast-util-from-html';
15
+
16
+ /**
17
+ * Set of meta tag names to ignore when extracting authored metadata.
18
+ * These are standard meta tags that we don't want to override.
19
+ */
20
+ const IGNORED_META_NAMES = new Set([
21
+ 'viewport',
22
+ ]);
23
+
24
+ /**
25
+ * Prefixes for meta tag names to ignore.
26
+ * og: and twitter: prefixes are standard social meta tags.
27
+ */
28
+ const IGNORED_META_PREFIXES = ['og:', 'twitter:'];
29
+
30
+ /**
31
+ * Check if a meta tag name should be ignored.
32
+ * @param {string} name - The meta tag name
33
+ * @returns {boolean} True if the name should be ignored
34
+ */
35
+ function shouldIgnoreMetaName(name) {
36
+ if (!name) return true;
37
+ if (IGNORED_META_NAMES.has(name)) return true;
38
+ return IGNORED_META_PREFIXES.some((prefix) => name.startsWith(prefix));
39
+ }
40
+
41
+ /**
42
+ * Extract metadata from the head element of an HTML document.
43
+ * Only extracts meta tags with name attribute (not property).
44
+ * Ignores og:*, twitter:*, and viewport meta tags.
45
+ *
46
+ * @param {import('hast').Element} head - The head element
47
+ * @returns {Object} Extracted metadata key/value pairs
48
+ */
49
+ function extractMetadataFromHead(head) {
50
+ const metadata = {};
51
+
52
+ /* c8 ignore next */
53
+ if (!head) return metadata;
54
+
55
+ // Select all meta tags with a name attribute
56
+ const metaTags = selectAll('meta[name]', head);
57
+
58
+ for (const meta of metaTags) {
59
+ const name = meta.properties?.name;
60
+ const content = meta.properties?.content;
61
+
62
+ // Only add if name is valid and content exists
63
+ if (typeof name === 'string' && !shouldIgnoreMetaName(name) && content !== undefined) {
64
+ metadata[name] = content;
65
+ }
66
+ }
67
+
68
+ return metadata;
69
+ }
70
+
71
+ /**
72
+ * Extract the main element's children as HAST nodes.
73
+ * @param {import('hast').Root} hast - The parsed HTML document
74
+ * @returns {import('hast').Root|undefined} HAST root with main's children,
75
+ * or undefined if not found
76
+ */
77
+ function extractMainContent(hast) {
78
+ const main = select('main', hast);
79
+ if (!main || !main.children || main.children.length === 0) {
80
+ return undefined;
81
+ }
82
+
83
+ // Return main's children as a HAST root node
84
+ return { type: 'root', children: main.children };
85
+ }
86
+
87
+ /**
88
+ * Extract metadata and body content from authored HTML document.
89
+ *
90
+ * @param {PipelineState} state - Pipeline state object
91
+ * @returns {Promise<void>}
92
+ */
93
+ export default async function extractAuthoredMetadata(state) {
94
+ const { content } = state;
95
+ const { edge, data } = content;
96
+
97
+ if (!edge) {
98
+ return;
99
+ }
100
+
101
+ const hast = fromHtml(edge, { fragment: false });
102
+ const head = select('head', hast);
103
+ const authoredMetadata = extractMetadataFromHead(head);
104
+
105
+ // If metadata was extracted, merge it with existing product metadata
106
+ if (Object.keys(authoredMetadata).length > 0) {
107
+ if (!data.metadata) {
108
+ data.metadata = {};
109
+ }
110
+
111
+ // Authored metadata takes precedence
112
+ Object.assign(data.metadata, authoredMetadata);
113
+ }
114
+
115
+ // Extract main content as HAST and store for render-body to use directly
116
+ const mainContent = extractMainContent(hast);
117
+ if (mainContent) {
118
+ content.edgeHast = mainContent;
119
+ }
120
+ }
@@ -26,7 +26,7 @@ export default async function fetchEdgeContent(state, req, res) {
26
26
  } = state;
27
27
  const { originalPath } = info;
28
28
 
29
- const contentUrl = `https://${ref}--${site}--${org}.aem.live${originalPath}.plain.html`;
29
+ const contentUrl = `https://${ref}--${site}--${org}.aem.live${originalPath}`;
30
30
  try {
31
31
  /** @type {Record<string, string>} */
32
32
  const headers = {};
@@ -45,22 +45,54 @@ function renderMedia(media) {
45
45
  }
46
46
 
47
47
  /**
48
- * Rewrite image URLs in authored content to include /content-images/ prefix.
48
+ * Rewrite a single URL to include /content-images/ prefix for media files.
49
+ * @param {string} url - The URL to potentially rewrite
50
+ * @returns {string} The rewritten URL
51
+ */
52
+ function rewriteMediaUrl(url) {
53
+ /* c8 ignore next */
54
+ if (!url) return url;
55
+ return url.replace(/\/(media_[a-f0-9]+\.\w+)/g, '/content-images/$1');
56
+ }
57
+
58
+ /**
59
+ * Recursively rewrite image URLs in HAST nodes to include /content-images/ prefix.
49
60
  * This allows the aem.network to route these images back to aem.live instead of the product media bus.
50
- * @param {string} html - HTML content containing image references
51
- * @returns {string} HTML with rewritten image URLs
61
+ * @param {import('hast').Root|import('hast').Element} node - HAST node to process
52
62
  */
53
- export function rewriteContentImageUrls(html) {
54
- return html.replace(/\/(media_[a-f0-9]+\.\w+)/g, '/content-images/$1');
63
+ export function rewriteContentImageUrls(node) {
64
+ /* c8 ignore next */
65
+ if (!node) return;
66
+
67
+ // Rewrite src and srcset attributes on img and source elements
68
+ if (node.type === 'element' && node.properties) {
69
+ if (node.tagName === 'img' || node.tagName === 'source') {
70
+ if (node.properties.src && typeof node.properties.src === 'string') {
71
+ node.properties.src = rewriteMediaUrl(node.properties.src);
72
+ }
73
+ if (node.properties.srcSet && typeof node.properties.srcSet === 'string') {
74
+ node.properties.srcSet = rewriteMediaUrl(node.properties.srcSet);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Recursively process children
80
+ if (node.children) {
81
+ for (const child of node.children) {
82
+ if (child.type === 'element') {
83
+ rewriteContentImageUrls(child);
84
+ }
85
+ }
86
+ }
55
87
  }
56
88
 
57
89
  /**
58
90
  * Render the product content.
59
- * @param {string} edge
60
- * @param {string} description
91
+ * @param {import('hast').Root} edgeHast - HAST nodes from authored content
92
+ * @param {string} description - Product description (may be HTML or plain text)
61
93
  * @returns {import('hast').Element | import('hast').Root}
62
94
  */
63
- function renderProductContent(edge, description) {
95
+ function renderProductContent(edgeHast, description) {
64
96
  const parts = [];
65
97
 
66
98
  // Add description first if it exists
@@ -74,11 +106,10 @@ function renderProductContent(edge, description) {
74
106
  }
75
107
  }
76
108
 
77
- // Add edge content after if it exists
78
- if (edge) {
79
- const rewrittenEdge = rewriteContentImageUrls(edge);
80
- const edgeFragment = fromHtml(rewrittenEdge, { fragment: true });
81
- parts.push(...edgeFragment.children);
109
+ // Add edge content after if it exists (already HAST, just rewrite URLs)
110
+ if (edgeHast) {
111
+ rewriteContentImageUrls(edgeHast);
112
+ parts.push(...edgeHast.children);
82
113
  }
83
114
 
84
115
  // Return undefined if nothing to render
@@ -110,7 +141,7 @@ function variantDataAttrs(variant) {
110
141
  */
111
142
  export default async function render(state, req, res) {
112
143
  const { content } = state;
113
- const { hast, edge } = content;
144
+ const { hast, edgeHast } = content;
114
145
 
115
146
  const {
116
147
  name,
@@ -120,7 +151,7 @@ export default async function render(state, req, res) {
120
151
  variants = [],
121
152
  } = content.data;
122
153
 
123
- const productContent = renderProductContent(edge, description);
154
+ const productContent = renderProductContent(edgeHast, description);
124
155
 
125
156
  const main = select('main', hast);
126
157
  main.children = [
@@ -13,11 +13,13 @@
13
13
  /* eslint-disable camelcase */
14
14
 
15
15
  import {
16
- computeProductPathKey, computeSiteKey, compute404Key, computeMediaKeys, computeAuthoredContentKey,
16
+ computeProductPathKey,
17
+ computeSiteKey,
18
+ compute404Key,
19
+ computeAuthoredContentKey,
20
+ computeProductKeys,
17
21
  } from '@dylandepass/helix-product-shared';
18
-
19
- export const isMediaRequest = (url) => /\/media_[0-9a-f]{40,}[/a-zA-Z0-9_-]*\.[0-9a-z]+$/.test(url.pathname);
20
- const BYO_CDN_TYPES = ['akamai', 'cloudflare', 'fastly', 'cloudfront'];
22
+ import { applyByoCdnHeaders } from '../utils/cache-headers.js';
21
23
 
22
24
  /**
23
25
  * Sets the cache headers for a product
@@ -27,92 +29,29 @@ const BYO_CDN_TYPES = ['akamai', 'cloudflare', 'fastly', 'cloudfront'];
27
29
  * @returns {void}
28
30
  */
29
31
  export function setCachingHeaders(req, resp, cacheKeys) {
30
- const url = new URL(req.url);
31
-
32
- // determine BYO CDN type
33
- let byo_cdn_type = req.headers.get('x-byo-cdn-type');
34
- const via = req.headers.get('via') || '';
35
- const cdn_loop = req.headers.get('cdn-loop') || '';
36
- // FIXME: cloudflare seems to strip the CDN-Loop header, i.e. the worker can't see it...
37
- if (!BYO_CDN_TYPES.includes(byo_cdn_type)) {
38
- // sniff downstream cdn type
39
- if (/[Aa]kamai/.test(via)) {
40
- byo_cdn_type = 'akamai';
41
- } else if (via.includes('varnish') || cdn_loop.startsWith('Fastly')) {
42
- byo_cdn_type = 'fastly';
43
- } else if (cdn_loop.includes('cloudflare') || req.headers.has('cf-worker')) {
44
- byo_cdn_type = 'cloudflare';
45
- } else if (via.includes('CloudFront')) {
46
- byo_cdn_type = 'cloudfront';
47
- } else {
48
- // invalid/unsupported CDN type
49
- byo_cdn_type = undefined;
50
- }
51
- }
52
-
53
32
  // cache instructions for BYO CDN and the browser
54
33
  const pushInvalidation = req.headers.get('x-push-invalidation') === 'enabled';
34
+
55
35
  // browser ttl & browser cache-control
56
36
  let browserTTL;
57
37
  if ([302, 400, 401].includes(resp.status)) {
58
38
  browserTTL = 0;
59
39
  resp.headers.set('cache-control', 'private; no-cache');
60
40
  } else {
61
- if (isMediaRequest(url)) {
62
- browserTTL = resp.status === 200 ? 2592000 : 3600;
63
- } else {
64
- browserTTL = 7200;
65
- }
41
+ browserTTL = 7200;
66
42
  resp.headers.set('cache-control', `max-age=${browserTTL}, must-revalidate`);
67
43
  }
68
44
 
69
45
  // CDN ttl
70
46
  let cdnTTL;
71
47
  if ([400, 401].includes(resp.status)) {
72
- // don't cache 400 & 401 repsonses in order to avoid potential downstream caching issues
48
+ // don't cache 400 & 401 responses in order to avoid potential downstream caching issues
73
49
  cdnTTL = 0;
74
- } else if (isMediaRequest(url)) {
75
- cdnTTL = resp.status === 200 ? 2592000 : 3600;
76
50
  } else {
77
51
  cdnTTL = pushInvalidation ? 172800 : 300;
78
52
  }
79
- switch (byo_cdn_type) {
80
- case 'cloudflare':
81
- resp.headers.set('cdn-cache-control', `max-age=${cdnTTL}, must-revalidate`);
82
- break;
83
- case 'fastly':
84
- resp.headers.set('surrogate-control', `max-age=${cdnTTL}, stale-while-revalidate=0`);
85
- break;
86
- case 'akamai':
87
- resp.headers.set('edge-control', `!no-store,max-age=${cdnTTL}s,downstream-ttl=${browserTTL}s`);
88
- break;
89
- case 'cloudfront':
90
- resp.headers.set('cache-control', `max-age=${browserTTL}, s-maxage=${cdnTTL}, must-revalidate`);
91
- break;
92
- default:
93
- break;
94
- }
95
53
 
96
- // BYO CDN cache tags
97
- // apply cache tags: transform Fastly Surrogate-Key to Cloudflare Cache-Tag
98
- if (cacheKeys.length) {
99
- switch (byo_cdn_type) {
100
- case 'fastly':
101
- resp.headers.set('surrogate-key', cacheKeys.join(' '));
102
- break;
103
- case 'akamai':
104
- resp.headers.set('edge-cache-tag', cacheKeys.join(' '));
105
- break;
106
- case 'cloudflare':
107
- resp.headers.set('cache-tag', `${cacheKeys.join(',')}`);
108
- break;
109
- case 'cloudfront':
110
- // cloudfront doesn't support cache tags/keys ...
111
- break;
112
- default:
113
- break;
114
- }
115
- }
54
+ applyByoCdnHeaders(resp, req, cacheKeys, browserTTL, cdnTTL);
116
55
  }
117
56
 
118
57
  /**
@@ -164,25 +103,6 @@ export async function setProductCacheHeaders(state, req, resp) {
164
103
  org, site, contentBusId, info,
165
104
  } = state;
166
105
 
167
- const productKeys = [
168
- await computeProductPathKey(org, site, info.path),
169
- await computeSiteKey(org, site),
170
- ];
171
- const authoredContentKey = await computeAuthoredContentKey(contentBusId, info.originalPath);
172
- setCachingHeaders(req, resp, [...productKeys, authoredContentKey]);
173
- }
174
-
175
- /**
176
- * Sets the cache headers for a product
177
- * @param {PipelineState} state
178
- * @param {PipelineRequest} req
179
- * @param {PipelineResponse} resp
180
- * @returns {Promise<void>}
181
- */
182
- export async function setMediaCacheHeaders(state, req, resp) {
183
- const { org, site } = state;
184
- const url = new URL(req.url);
185
- const keys = await computeMediaKeys(org, site, url.pathname);
186
-
187
- setCachingHeaders(req, resp, keys);
106
+ const productKeys = await computeProductKeys(org, site, info.path, contentBusId);
107
+ setCachingHeaders(req, resp, productKeys);
188
108
  }
@@ -0,0 +1,76 @@
1
+ /*
2
+ * Copyright 2025 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
+ const BYO_CDN_TYPES = ['akamai', 'cloudflare', 'fastly', 'cloudfront'];
14
+
15
+ /**
16
+ * Detects the BYO CDN type from the request headers.
17
+ *
18
+ * @param {Request|object} req - The request object (must have headers)
19
+ * @returns {string|undefined} - The detected CDN type or undefined
20
+ */
21
+ const detectByoCdnType = (req) => {
22
+ const { headers } = req;
23
+ let byoCdnType = headers.get('x-byo-cdn-type');
24
+ const via = headers.get('via') || '';
25
+ const cdnLoop = headers.get('cdn-loop') || '';
26
+
27
+ if (!BYO_CDN_TYPES.includes(byoCdnType)) {
28
+ if (/[Aa]kamai/.test(via)) {
29
+ byoCdnType = 'akamai';
30
+ } else if (via.includes('varnish') || cdnLoop.startsWith('Fastly')) {
31
+ byoCdnType = 'fastly';
32
+ } else if (cdnLoop.includes('cloudflare') || headers.has('cf-worker')) {
33
+ byoCdnType = 'cloudflare';
34
+ } else if (via.includes('CloudFront')) {
35
+ byoCdnType = 'cloudfront';
36
+ } else {
37
+ byoCdnType = undefined;
38
+ }
39
+ }
40
+
41
+ return BYO_CDN_TYPES.includes(byoCdnType) ? byoCdnType : undefined;
42
+ };
43
+
44
+ /**
45
+ * Applies BYO CDN specific cache headers to the response.
46
+ *
47
+ * @param {Response|object} resp - The response object
48
+ * @param {Request|object} req - The request object
49
+ * @param {string[]} cacheKeys - List of cache keys/tags
50
+ * @param {number} browserTTL - Browser TTL in seconds
51
+ * @param {number} cdnTTL - CDN TTL in seconds
52
+ */
53
+ export const applyByoCdnHeaders = (resp, req, cacheKeys, browserTTL, cdnTTL) => {
54
+ const byoCdnType = detectByoCdnType(req);
55
+ const keys = cacheKeys && cacheKeys.length ? cacheKeys : [];
56
+
57
+ switch (byoCdnType) {
58
+ case 'cloudflare':
59
+ resp.headers.set('cdn-cache-control', `max-age=${cdnTTL}, must-revalidate`);
60
+ if (keys.length) resp.headers.set('cache-tag', keys.join(','));
61
+ break;
62
+ case 'fastly':
63
+ resp.headers.set('surrogate-control', `max-age=${cdnTTL}, stale-while-revalidate=0`);
64
+ if (keys.length) resp.headers.set('surrogate-key', keys.join(' '));
65
+ break;
66
+ case 'akamai':
67
+ resp.headers.set('edge-control', `!no-store,max-age=${cdnTTL}s,downstream-ttl=${browserTTL}s`);
68
+ if (keys.length) resp.headers.set('edge-cache-tag', keys.join(' '));
69
+ break;
70
+ case 'cloudfront':
71
+ resp.headers.set('cache-control', `max-age=${browserTTL}, s-maxage=${cdnTTL}, must-revalidate`);
72
+ break;
73
+ default:
74
+ break;
75
+ }
76
+ };
@@ -1,64 +0,0 @@
1
- /*
2
- * Copyright 2025 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 { PipelineResponse, PipelineStatusError } from '@adobe/helix-html-pipeline';
14
- import { validatePathInfo } from './utils/path.js';
15
- import initConfig from './steps/init-config.js';
16
- import fetchMedia from './steps/fetch-media.js';
17
- import { setLastModified } from './utils/last-modified.js';
18
- import { setMediaCacheHeaders } from './steps/set-cache-headers.js';
19
-
20
- export async function productMediaPipe(state, req) {
21
- state.type = 'media';
22
-
23
- if (!validatePathInfo(state.info)) {
24
- return new PipelineResponse('', {
25
- status: 404,
26
- headers: {
27
- 'x-error': 'invalid path',
28
- },
29
- });
30
- }
31
-
32
- const res = new PipelineResponse('');
33
-
34
- try {
35
- await initConfig(state, req, res);
36
-
37
- state.timer?.update('content-fetch');
38
- await fetchMedia(state, res);
39
- if (res.status === 404) {
40
- return new PipelineResponse('', {
41
- status: 404,
42
- headers: {
43
- 'x-error': 'not found',
44
- },
45
- });
46
- } else if (res.error) {
47
- if (res.status < 400) {
48
- return res;
49
- }
50
- throw new PipelineStatusError(res.status, res.error);
51
- }
52
-
53
- setLastModified(state, res);
54
-
55
- await setMediaCacheHeaders(state, req, res);
56
-
57
- res.headers = state.content.headers;
58
- res.body = state.content.data;
59
- } catch (e) {
60
- res.error = e.message;
61
- }
62
-
63
- return res;
64
- }
@@ -1,41 +0,0 @@
1
- /*
2
- * Copyright 2022 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
- import { extractLastModified, recordLastModified } from '../utils/last-modified.js';
13
-
14
- /**
15
- * Loads the content from either the content-bus or code-bus and stores it in `state.content`
16
- * @param {PipelineState} state
17
- * @param {PipelineResponse} res
18
- * @returns {Promise<void>}
19
- */
20
- export default async function fetchMedia(state, res) {
21
- const {
22
- info, org, site,
23
- } = state;
24
- const bucketId = 'helix-product-bus';
25
-
26
- const key = `${org}/${site}/media/${info.originalFilename}`;
27
- const ret = await state.s3Loader.getObject(bucketId, key);
28
-
29
- if (ret.status === 200) {
30
- res.status = 200;
31
- delete res.error;
32
- state.content.data = ret.body;
33
- state.content.headers = ret.headers;
34
-
35
- recordLastModified(state, res, 'content', extractLastModified(ret.headers));
36
- } else {
37
- // keep 404, but propagate others as 502
38
- res.status = ret.status === 404 ? 404 : 502;
39
- res.error = `failed to load ${info.resourcePath} from product-bus: ${ret.status}`;
40
- }
41
- }
@@ -1,91 +0,0 @@
1
- /*
2
- * Copyright 2022 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
- /* eslint-disable */
14
- // @ts-nocheck
15
-
16
- import { slugger } from '@dylandepass/helix-product-shared';
17
- import { extractLastModified, recordLastModified } from '../utils/last-modified.js';
18
-
19
- const INDEXER_URL_KEYS = ['index', 'merchant-center-feed', 'sitemap'];
20
-
21
- /**
22
- * Loads the content from either the content-bus or code-bus and stores it in `state.content`
23
- * @param {PipelineState} state
24
- * @param {PipelineRequest} req
25
- * @param {PipelineResponse} res
26
- * @returns {Promise<void>}
27
- */
28
- export default async function fetchContent(state, req, res) {
29
- const {
30
- info, owner, site, config,
31
- } = state;
32
- const bucketId = 'adobe-commerce-catalog';
33
- const { route } = config;
34
- const { storeCode, storeViewCode } = route;
35
-
36
- let sku;
37
-
38
- // if route params only contains the urlKey than we
39
- // need to head first to get the sku
40
-
41
- // conditioanlly strip .json and .xml off urlKey
42
- let urlKey = route.params.urlKey?.replace(/\.json$/, '');
43
- urlKey = urlKey?.replace(/\.xml$/, '');
44
-
45
- if (Object.keys(route.params).length === 1 && urlKey && !INDEXER_URL_KEYS.includes(urlKey)) {
46
- const headKey = `${owner}/${site}/${storeCode}/${storeViewCode}/urlkeys/${urlKey}`;
47
-
48
- const headRes = await state.s3Loader.headObject(bucketId, headKey);
49
-
50
- if (headRes.status === 200) {
51
- sku = headRes.headers.get('sku');
52
- } else {
53
- res.status = headRes.status === 404 ? 404 : 502;
54
- res.error = `HEAD: failed to load ${info.resourcePath} from product-bus: ${headRes.status}`;
55
- return;
56
- }
57
- } else if (!INDEXER_URL_KEYS.includes(urlKey)) {
58
- sku = route.params.sku?.replace(/\.json$/, '').replace(/\.xml$/, '');
59
- }
60
-
61
- /** @type {string} */
62
- let key;
63
- if ((!sku && !INDEXER_URL_KEYS.includes(urlKey)) || (sku && !INDEXER_URL_KEYS.includes(sku))) {
64
- const slug = slugger(sku);
65
- key = `${owner}/${site}/${storeCode}/${storeViewCode}/products/${slug}.json`;
66
- } else if (urlKey === 'index' || sku === 'index' || urlKey === 'sitemap' || sku === 'sitemap') {
67
- const id = req.params?.id ?? 'default';
68
- key = `${owner}/${site}/${storeCode}/${storeViewCode}/index/${id}.json`;
69
- } else if (urlKey === 'merchant-center-feed' || sku === 'merchant-center-feed') {
70
- key = `${owner}/${site}/${storeCode}/${storeViewCode}/merchant-feed/default.json`;
71
- }
72
-
73
- const ret = await state.s3Loader.getObject(bucketId, key);
74
-
75
- if (ret.status === 200) {
76
- try {
77
- res.status = 200;
78
- delete res.error;
79
- state.content.data = JSON.parse(ret.body);
80
-
81
- recordLastModified(state, res, 'content', extractLastModified(ret.headers));
82
- } catch (e) {
83
- res.status = 400;
84
- res.error = `failed to parse ${info.resourcePath} from product-bus: ${e.message}`;
85
- }
86
- } else {
87
- // keep 404, but propagate others as 502
88
- res.status = ret.status === 404 ? 404 : 502;
89
- res.error = `failed to load ${urlKey === 'index' ? key : info.resourcePath} from product-bus: ${ret.status}`;
90
- }
91
- }