mcp-magento-cms 1.0.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.
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": 7,
3
+ "identifier": "ai-sample-page",
4
+ "title": "AI Sample Page",
5
+ "page_layout": "cms-full-width",
6
+ "meta_title": "",
7
+ "meta_keywords": "",
8
+ "meta_description": "",
9
+ "content_heading": "AI Sample Page Heading",
10
+ "content": "<style>#html-body [data-pb-style=KW2FBFE]{justify-content:flex-start;display:flex;flex-direction:column;background-position:left top;background-size:cover;background-repeat:no-repeat;background-attachment:scroll}</style><div data-content-type=\"row\" data-appearance=\"contained\" data-element=\"main\"><div data-enable-parallax=\"0\" data-parallax-speed=\"0.5\" data-background-images=\"{}\" data-background-type=\"image\" data-video-loop=\"true\" data-video-play-only-visible=\"true\" data-video-lazy-load=\"true\" data-video-fallback-src=\"\" data-background-lazy-load=\"\" data-element=\"inner\" data-pb-style=\"KW2FBFE\"><div data-content-type=\"text\" data-appearance=\"default\" data-element=\"main\"><div>\r\n<h2>What is Lorem Ipsum?</h2>\r\n<p><strong>Lorem Ipsum</strong>&nbsp;is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since 1966, when designers at Letraset and James Mosley, the librarian at St Bride Printing Library in London, took a 1914 Cicero translation and scrambled it to make dummy text for Letraset's Body Type sheets. It has survived not only many decades, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised thanks to these sheets and more recently with desktop publishing software like Aldus PageMaker and Microsoft Word including versions of Lorem Ipsum.</p>\r\n</div>\r\n<div>\r\n<h2>Why do we use it?</h2>\r\n<p>It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>\r\n</div>\r\n<p>&nbsp;</p>\r\n<div>\r\n<h2>Where does it come from?</h2>\r\n<p>Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of \"de Finibus Bonorum et Malorum\" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, \"Lorem ipsum dolor sit amet..\", comes from a line in section 1.10.32.</p>\r\n<p>The standard chunk of Lorem Ipsum used since 1966 is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from \"de Finibus Bonorum et Malorum\" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.</p>\r\n</div></div></div></div>",
11
+ "creation_time": "2026-08-06 11:35:54",
12
+ "update_time": "2026-08-06 11:35:54",
13
+ "sort_order": "0",
14
+ "custom_theme": "",
15
+ "custom_root_template": "",
16
+ "active": true
17
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": 8,
3
+ "identifier": "test-ai-page",
4
+ "title": "test-ai-page",
5
+ "page_layout": "1column",
6
+ "meta_title": "test-ai-page",
7
+ "meta_keywords": "",
8
+ "meta_description": "",
9
+ "content_heading": "",
10
+ "content": "<div data-content-type=\"row\" data-appearance=\"contained\" data-element=\"main\"><div data-enable-parallax=\"0\" data-parallax-speed=\"0.5\" data-background-images=\"{}\" data-background-type=\"image\" data-video-loop=\"true\" data-video-play-only-visible=\"true\" data-video-lazy-load=\"true\" data-video-fallback-src=\"\" data-element=\"inner\" data-pb-style=\"RFE976409C7A5\" style=\"justify-content: flex-start; display: flex; flex-direction: column; background-position: left top; background-size: cover; background-repeat: no-repeat; background-attachment: scroll; border-style: none; border-width: 1px; border-radius: 0px; margin: 0px 0px 10px; padding: 10px;\"><div data-content-type=\"text\" data-appearance=\"default\" data-element=\"main\" data-pb-style=\"T2CED8B12276F\" class=\"bypass-html-filter\" style=\"border-style: none; border-width: 1px; border-radius: 0px; margin: 0px; padding: 0px;\"><p>lorem ipsum</p></div></div></div><style>#html-body [data-pb-style=RFE976409C7A5]{justify-content:flex-start;display:flex;flex-direction:column;background-position:left top;background-size:cover;background-repeat:no-repeat;background-attachment:scroll}#html-body [data-pb-style=T2CED8B12276F]{border-style:none}</style>",
11
+ "creation_time": "2026-08-06 11:54:59",
12
+ "update_time": "2026-08-06 11:54:59",
13
+ "sort_order": "0",
14
+ "active": true
15
+ }
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Creates a demo CMS page with multiple native Page Builder sections:
4
+ * 1. Banner (poster) with background image, heading, text and a button
5
+ * 2. Row with Heading + Text (Lorem Ipsum)
6
+ * 3. Row with Column Group (3 columns: image + heading + text)
7
+ * 4. Row with Divider + inline Buttons
8
+ *
9
+ * Markup follows the Page Builder master format (data-pb-style + <style> block)
10
+ * so the page opens cleanly in the Admin Page Builder editor.
11
+ *
12
+ * NODE_EXTRA_CA_CERTS=~/.local/share/mkcert/rootCA.pem node scripts/create-demo-page.js
13
+ */
14
+ import { MagentoClient } from '../src/magentoClient.js';
15
+
16
+ const client = MagentoClient.fromEnv();
17
+
18
+ // --- style id helper (Page Builder uses short uppercase ids) -----------------
19
+ const usedIds = new Set();
20
+ function pbStyleId() {
21
+ let id;
22
+ do {
23
+ id = Array.from({ length: 7 }, () =>
24
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)]
25
+ ).join('');
26
+ } while (usedIds.has(id) || /^\d/.test(id));
27
+ usedIds.add(id);
28
+ return id;
29
+ }
30
+
31
+ const styles = [];
32
+ function addStyle(id, css) {
33
+ styles.push(`#html-body [data-pb-style=${id}]{${css}}`);
34
+ }
35
+
36
+ // --- shared snippets ---------------------------------------------------------
37
+ const ROW_INNER_ATTRS =
38
+ 'data-enable-parallax="0" data-parallax-speed="0.5" data-background-images="{}"' +
39
+ ' data-background-type="image" data-video-loop="true" data-video-play-only-visible="true"' +
40
+ ' data-video-lazy-load="true" data-video-fallback-src="" data-background-lazy-load=""' +
41
+ ' data-element="inner"';
42
+
43
+ const ROW_INNER_CSS =
44
+ 'justify-content:flex-start;display:flex;flex-direction:column;' +
45
+ 'background-position:left top;background-size:cover;background-repeat:no-repeat;' +
46
+ 'background-attachment:scroll';
47
+
48
+ function row(appearance, innerHtml, extraCss = '') {
49
+ const id = pbStyleId();
50
+ addStyle(id, ROW_INNER_CSS + (extraCss ? ';' + extraCss : ''));
51
+ return (
52
+ `<div data-content-type="row" data-appearance="${appearance}" data-element="main">` +
53
+ `<div ${ROW_INNER_ATTRS} data-pb-style="${id}">${innerHtml}</div></div>`
54
+ );
55
+ }
56
+
57
+ function heading(tag, text, css = '') {
58
+ const id = pbStyleId();
59
+ if (css) addStyle(id, css);
60
+ return `<${tag} data-content-type="heading" data-appearance="default" data-element="main"${css ? ` data-pb-style="${id}"` : ''}>${text}</${tag}>`;
61
+ }
62
+
63
+ function textBlock(innerHtml) {
64
+ return `<div data-content-type="text" data-appearance="default" data-element="main">${innerHtml}</div>`;
65
+ }
66
+
67
+ function image(mediaPath, alt) {
68
+ const figId = pbStyleId();
69
+ const imgId = pbStyleId();
70
+ addStyle(figId, 'margin:0;padding:0;border-style:none');
71
+ addStyle(imgId, 'max-width:100%;height:auto');
72
+ const src = `{{media url=${mediaPath}}}`;
73
+ return (
74
+ `<figure data-content-type="image" data-appearance="full-width" data-element="main" data-pb-style="${figId}">` +
75
+ `<img class="pagebuilder-mobile-hidden" src="${src}" alt="${alt}" title="" data-element="desktop_image" data-pb-style="${imgId}">` +
76
+ `<img class="pagebuilder-mobile-only" src="${src}" alt="${alt}" title="" data-element="mobile_image" data-pb-style="${imgId}">` +
77
+ `</figure>`
78
+ );
79
+ }
80
+
81
+ // --- section 1: banner (poster) ----------------------------------------------
82
+ const bannerMedia = 'wysiwyg/melissa/banner-1.png';
83
+ const bWrapId = pbStyleId();
84
+ const bOverlayId = pbStyleId();
85
+ const bContentId = pbStyleId();
86
+ addStyle(
87
+ bWrapId,
88
+ `background-image:url({{media url=${bannerMedia}}});background-position:center center;` +
89
+ 'background-size:cover;background-repeat:no-repeat;background-attachment:scroll'
90
+ );
91
+ addStyle(bOverlayId, 'background-color:transparent;border-style:none;border-width:1px;border-radius:0px;min-height:420px');
92
+ addStyle(bContentId, 'min-height:50px');
93
+
94
+ const bannerSection =
95
+ `<div data-content-type="banner" data-appearance="poster" data-show-button="always"` +
96
+ ` data-show-overlay="never" data-element="main">` +
97
+ `<div data-element="empty_link">` +
98
+ `<div class="pagebuilder-banner-wrapper" data-background-images="{\\&quot;desktop_image\\&quot;:\\&quot;{{media url=${bannerMedia}}}\\&quot;}"` +
99
+ ` data-background-type="image" data-video-loop="true" data-video-play-only-visible="true"` +
100
+ ` data-video-lazy-load="true" data-video-fallback-src="" data-element="wrapper" data-pb-style="${bWrapId}">` +
101
+ `<div class="pagebuilder-overlay pagebuilder-poster-overlay" data-overlay-color="" data-element="overlay" data-pb-style="${bOverlayId}">` +
102
+ `<div class="pagebuilder-poster-content">` +
103
+ `<div data-element="content" data-pb-style="${bContentId}">` +
104
+ `<h1>Lorem Ipsum Dolor Sit Amet</h1>` +
105
+ `<p>Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.</p>` +
106
+ `</div>` +
107
+ `<button class="pagebuilder-banner-button pagebuilder-button-primary" data-element="button" type="button">Shop Now</button>` +
108
+ `</div></div></div></div></div>`;
109
+
110
+ const section1 = row('full-width', bannerSection, 'padding:0');
111
+
112
+ // --- section 2: heading + text -----------------------------------------------
113
+ const loremText =
114
+ '<p><strong>Lorem Ipsum</strong> is simply dummy text of the printing and typesetting industry. ' +
115
+ 'Lorem Ipsum has been the industry\u2019s standard dummy text ever since 1966, when designers at ' +
116
+ 'Letraset scrambled a 1914 Cicero translation to make dummy text for Body Type sheets. It has ' +
117
+ 'survived not only many decades, but also the leap into electronic typesetting, remaining ' +
118
+ 'essentially unchanged.</p>' +
119
+ '<p>It is a long established fact that a reader will be distracted by the readable content of a ' +
120
+ 'page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less ' +
121
+ 'normal distribution of letters, as opposed to using \u2018Content here, content here\u2019, making it look ' +
122
+ 'like readable English.</p>';
123
+
124
+ const section2 = row(
125
+ 'contained',
126
+ heading('h2', 'What is Lorem Ipsum?', 'text-align:center;margin:10px 0') + textBlock(loremText),
127
+ 'padding:30px 10px'
128
+ );
129
+
130
+ // --- section 3: three columns (image + heading + text) ------------------------
131
+ const colDefs = [
132
+ {
133
+ img: 'wysiwyg/melissa/melissa-1.png',
134
+ title: 'Sed Ut Perspiciatis',
135
+ body: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam.',
136
+ },
137
+ {
138
+ img: 'wysiwyg/melissa/melissa-2.png',
139
+ title: 'Nemo Enim Ipsam',
140
+ body: 'Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores.',
141
+ },
142
+ {
143
+ img: 'wysiwyg/melissa/melissa-3.png',
144
+ title: 'Ut Enim Ad Minima',
145
+ body: 'Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea.',
146
+ },
147
+ ];
148
+
149
+ const COLUMN_CSS =
150
+ 'justify-content:flex-start;display:flex;flex-direction:column;' +
151
+ 'background-position:left top;background-size:cover;background-repeat:no-repeat;' +
152
+ 'background-attachment:scroll;width:33.3333%;margin:0;padding:10px;align-self:stretch';
153
+
154
+ const columns = colDefs
155
+ .map((c) => {
156
+ const id = pbStyleId();
157
+ addStyle(id, COLUMN_CSS);
158
+ return (
159
+ `<div data-content-type="column" data-appearance="full-height" data-background-images="{}"` +
160
+ ` data-background-type="image" data-video-loop="true" data-video-play-only-visible="true"` +
161
+ ` data-video-lazy-load="true" data-video-fallback-src="" data-element="main" data-pb-style="${id}">` +
162
+ image(c.img, c.title) +
163
+ heading('h3', c.title, 'text-align:center') +
164
+ textBlock(`<p style="text-align: center;">${c.body}</p>`) +
165
+ `</div>`
166
+ );
167
+ })
168
+ .join('');
169
+
170
+ const lineId = pbStyleId();
171
+ addStyle(lineId, 'display:flex');
172
+ const columnGroup =
173
+ `<div data-content-type="column-group" data-appearance="default" data-grid-size="12" data-element="main">` +
174
+ `<div data-content-type="column-line" data-appearance="default" data-element="main" data-pb-style="${lineId}">` +
175
+ columns +
176
+ `</div></div>`;
177
+
178
+ const section3 = row(
179
+ 'contained',
180
+ heading('h2', 'Why Do We Use It?', 'text-align:center;margin:10px 0') + columnGroup,
181
+ 'padding:30px 10px'
182
+ );
183
+
184
+ // --- section 4: divider + buttons ---------------------------------------------
185
+ const divId = pbStyleId();
186
+ const hrId = pbStyleId();
187
+ addStyle(divId, 'text-align:center;padding:10px 0');
188
+ addStyle(hrId, 'display:inline-block;width:100%;border-width:1px;border-color:#e0e0e0;margin:0');
189
+ const divider =
190
+ `<div data-content-type="divider" data-appearance="default" data-element="main" data-pb-style="${divId}">` +
191
+ `<hr data-element="line" data-pb-style="${hrId}"></div>`;
192
+
193
+ const btnsId = pbStyleId();
194
+ addStyle(btnsId, 'text-align:center');
195
+ function buttonItem(label, cssClass) {
196
+ const id = pbStyleId();
197
+ addStyle(id, 'display:inline-block');
198
+ return (
199
+ `<div data-content-type="button-item" data-appearance="default" data-element="main" data-pb-style="${id}">` +
200
+ `<a class="${cssClass}" href="#" target="" data-link-type="default" data-element="link">` +
201
+ `<span data-element="link_text">${label}</span></a></div>`
202
+ );
203
+ }
204
+ const buttons =
205
+ `<div data-content-type="buttons" data-appearance="inline" data-same-width="false" data-element="main" data-pb-style="${btnsId}">` +
206
+ buttonItem('Where Can I Get Some?', 'pagebuilder-button-primary') +
207
+ buttonItem('Read The 1914 Translation', 'pagebuilder-button-secondary') +
208
+ `</div>`;
209
+
210
+ const section4 = row(
211
+ 'contained',
212
+ divider + heading('h2', 'Where Does It Come From?', 'text-align:center;margin:10px 0') +
213
+ textBlock(
214
+ '<p style="text-align: center;">Contrary to popular belief, Lorem Ipsum is not simply random text. ' +
215
+ 'It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. ' +
216
+ 'It comes from sections 1.10.32 and 1.10.33 of \u201cde Finibus Bonorum et Malorum\u201d by Cicero.</p>'
217
+ ) +
218
+ buttons,
219
+ 'padding:30px 10px'
220
+ );
221
+
222
+ // --- assemble + publish --------------------------------------------------------
223
+ const content = `<style>${styles.join('')}</style>` + section1 + section2 + section3 + section4;
224
+
225
+ const page = {
226
+ identifier: 'pb-demo-lorem-ipsum',
227
+ title: 'PB Demo — Lorem Ipsum',
228
+ page_layout: 'cms-full-width',
229
+ content_heading: '',
230
+ content,
231
+ active: true,
232
+ };
233
+
234
+ const created = await client.createPage(page);
235
+ console.log(`Created page id=${created.id} identifier=${created.identifier}`);
236
+ console.log(`Admin edit URL: ${client.pageEditUrl(created.id)}`);
237
+ console.log(`Frontend URL: ${client.baseUrl}/${created.identifier}`);
@@ -0,0 +1,80 @@
1
+ <?php
2
+ /**
3
+ * Creates (or reuses) a Magento Integration named "MCP CMS Studio" with
4
+ * full CMS permissions and prints its permanent access token.
5
+ *
6
+ * Run inside the Magento root:
7
+ * ddev exec php tools/mcp-magento-cms/scripts/create-integration.php
8
+ */
9
+
10
+ use Magento\Framework\App\Bootstrap;
11
+
12
+ require __DIR__ . '/../../../app/bootstrap.php';
13
+
14
+ $bootstrap = Bootstrap::create(BP, $_SERVER);
15
+ $objectManager = $bootstrap->getObjectManager();
16
+
17
+ /** @var \Magento\Framework\App\State $state */
18
+ $state = $objectManager->get(\Magento\Framework\App\State::class);
19
+ try {
20
+ $state->setAreaCode('adminhtml');
21
+ } catch (\Exception $e) {
22
+ // area already set
23
+ }
24
+
25
+ $integrationName = 'MCP CMS Studio';
26
+
27
+ /** @var \Magento\Integration\Api\IntegrationServiceInterface $integrationService */
28
+ $integrationService = $objectManager->get(\Magento\Integration\Api\IntegrationServiceInterface::class);
29
+ /** @var \Magento\Integration\Api\OauthServiceInterface $oauthService */
30
+ $oauthService = $objectManager->get(\Magento\Integration\Api\OauthServiceInterface::class);
31
+ /** @var \Magento\Integration\Api\AuthorizationServiceInterface $authorizationService */
32
+ $authorizationService = $objectManager->get(\Magento\Integration\Api\AuthorizationServiceInterface::class);
33
+
34
+ $integration = $integrationService->findByName($integrationName);
35
+
36
+ if (!$integration || !$integration->getId()) {
37
+ $integration = $integrationService->create([
38
+ 'name' => $integrationName,
39
+ 'status' => \Magento\Integration\Model\Integration::STATUS_ACTIVE,
40
+ 'setup_type' => 0,
41
+ ]);
42
+ echo "Created integration '{$integrationName}' (id {$integration->getId()})\n";
43
+ } else {
44
+ echo "Found existing integration '{$integrationName}' (id {$integration->getId()})\n";
45
+ }
46
+
47
+ // Grant CMS + store read permissions
48
+ $resources = [
49
+ 'Magento_Cms::cms',
50
+ 'Magento_Cms::page',
51
+ 'Magento_Cms::save',
52
+ 'Magento_Cms::page_delete',
53
+ 'Magento_Cms::block',
54
+ 'Magento_Cms::save_design',
55
+ 'Magento_Backend::stores',
56
+ 'Magento_Backend::stores_settings',
57
+ 'Magento_Backend::store',
58
+ ];
59
+ $authorizationService->grantPermissions($integration->getId(), $resources);
60
+ echo "Granted CMS permissions\n";
61
+
62
+ // Create consumer credentials + permanent access token if missing
63
+ $consumerId = $integration->getConsumerId();
64
+ $userTypeIntegration = \Magento\Authorization\Model\UserContextInterface::USER_TYPE_INTEGRATION;
65
+
66
+ $tokenFactory = $objectManager->get(\Magento\Integration\Model\Oauth\TokenFactory::class);
67
+ $existing = $tokenFactory->create()->loadByConsumerIdAndUserType($consumerId, $userTypeIntegration);
68
+
69
+ if (!$existing->getId() || !$existing->getToken() || $existing->getType() !== 'access') {
70
+ $oauthService->createAccessToken($consumerId, true);
71
+ $existing = $tokenFactory->create()->loadByConsumerIdAndUserType($consumerId, $userTypeIntegration);
72
+ }
73
+
74
+ $integration = $integrationService->get($integration->getId());
75
+ if ((int)$integration->getStatus() !== \Magento\Integration\Model\Integration::STATUS_ACTIVE) {
76
+ $integration->setStatus(\Magento\Integration\Model\Integration::STATUS_ACTIVE);
77
+ $integrationService->update($integration->getData());
78
+ }
79
+
80
+ echo 'ACCESS_TOKEN=' . $existing->getToken() . "\n";
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Creates the "melissa home test" CMS page from the Figma "Home-Approved" design:
4
+ * Section 1: Hero Slider (3 slides, native Page Builder slider/slide elements)
5
+ * Section 2: Category section — heading, filter buttons, 9 colored category cards
6
+ *
7
+ * Content, colors and copy taken from Figma nodes 1:577 (SLIDER) and 1:621 (category).
8
+ *
9
+ * NODE_EXTRA_CA_CERTS=~/.local/share/mkcert/rootCA.pem node scripts/create-melissa-home.js
10
+ */
11
+ import { MagentoClient } from '../src/magentoClient.js';
12
+
13
+ const client = MagentoClient.fromEnv();
14
+
15
+ const usedIds = new Set();
16
+ function pbStyleId() {
17
+ let id;
18
+ do {
19
+ id = Array.from({ length: 7 }, () =>
20
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)]
21
+ ).join('');
22
+ } while (usedIds.has(id) || /^\d/.test(id));
23
+ usedIds.add(id);
24
+ return id;
25
+ }
26
+
27
+ const styles = [];
28
+ function addStyle(id, css) {
29
+ styles.push(`#html-body [data-pb-style=${id}]{${css}}`);
30
+ }
31
+
32
+ const ROW_INNER_ATTRS =
33
+ 'data-enable-parallax="0" data-parallax-speed="0.5" data-background-images="{}"' +
34
+ ' data-background-type="image" data-video-loop="true" data-video-play-only-visible="true"' +
35
+ ' data-video-lazy-load="true" data-video-fallback-src="" data-background-lazy-load=""' +
36
+ ' data-element="inner"';
37
+
38
+ const ROW_INNER_CSS =
39
+ 'justify-content:flex-start;display:flex;flex-direction:column;' +
40
+ 'background-position:left top;background-size:cover;background-repeat:no-repeat;' +
41
+ 'background-attachment:scroll';
42
+
43
+ function row(appearance, innerHtml, extraCss = '') {
44
+ const id = pbStyleId();
45
+ addStyle(id, ROW_INNER_CSS + (extraCss ? ';' + extraCss : ''));
46
+ return (
47
+ `<div data-content-type="row" data-appearance="${appearance}" data-element="main">` +
48
+ `<div ${ROW_INNER_ATTRS} data-pb-style="${id}">${innerHtml}</div></div>`
49
+ );
50
+ }
51
+
52
+ function heading(tag, text, css = '') {
53
+ const id = pbStyleId();
54
+ if (css) addStyle(id, css);
55
+ return `<${tag} data-content-type="heading" data-appearance="default" data-element="main"${css ? ` data-pb-style="${id}"` : ''}>${text}</${tag}>`;
56
+ }
57
+
58
+ function textBlock(innerHtml, css = '') {
59
+ const id = pbStyleId();
60
+ if (css) addStyle(id, css);
61
+ return `<div data-content-type="text" data-appearance="default" data-element="main"${css ? ` data-pb-style="${id}"` : ''}>${innerHtml}</div>`;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Section 1: HERO SLIDER (Figma node 1:577)
66
+ // ---------------------------------------------------------------------------
67
+ const slides = [
68
+ {
69
+ name: 'Slide 1',
70
+ image: 'wysiwyg/melissa/banner-1.png',
71
+ eyebrow: 'New Trend 2023',
72
+ title: 'Summer<br>Collection',
73
+ button: 'Explore now',
74
+ },
75
+ {
76
+ name: 'Slide 2',
77
+ image: 'wysiwyg/melissa/banner-2.png',
78
+ eyebrow: 'New Trend 2020',
79
+ title: 'Floral print<br>shorts',
80
+ button: 'Explore now',
81
+ },
82
+ {
83
+ name: 'Slide 3',
84
+ image: 'wysiwyg/melissa/banner-2.png',
85
+ eyebrow: 'New Trend 2020',
86
+ title: 'Floral print<br>shorts',
87
+ button: 'Explore now',
88
+ },
89
+ ];
90
+
91
+ function slide({ name, image, eyebrow, title, button }) {
92
+ const wrapId = pbStyleId();
93
+ const overlayId = pbStyleId();
94
+ const contentId = pbStyleId();
95
+ addStyle(
96
+ wrapId,
97
+ `background-image:url({{media url=${image}}});background-position:center center;` +
98
+ 'background-size:cover;background-repeat:no-repeat;background-attachment:scroll;' +
99
+ 'border-style:none;border-radius:0px;min-height:600px'
100
+ );
101
+ addStyle(overlayId, 'border-radius:0px;background-color:transparent;min-height:600px;padding:60px');
102
+ addStyle(contentId, 'min-height:50px');
103
+
104
+ return (
105
+ `<div data-content-type="slide" data-slide-name="${name}" data-appearance="collage-left"` +
106
+ ` data-show-button="always" data-show-overlay="never" data-element="main">` +
107
+ `<div data-element="empty_link">` +
108
+ `<div class="pagebuilder-slide-wrapper" data-background-images="{\\&quot;desktop_image\\&quot;:\\&quot;{{media url=${image}}}\\&quot;}"` +
109
+ ` data-background-type="image" data-video-loop="true" data-video-play-only-visible="true"` +
110
+ ` data-video-lazy-load="true" data-video-fallback-src="" data-element="wrapper" data-pb-style="${wrapId}">` +
111
+ `<div class="pagebuilder-overlay" data-overlay-color="" data-element="overlay" data-pb-style="${overlayId}">` +
112
+ `<div class="pagebuilder-collage-content">` +
113
+ `<div data-element="content" data-pb-style="${contentId}">` +
114
+ `<p><strong>${eyebrow}</strong></p>` +
115
+ `<h1>${title}</h1>` +
116
+ `</div>` +
117
+ `<button class="pagebuilder-slide-button pagebuilder-button-secondary" data-element="button" type="button">${button}</button>` +
118
+ `</div></div></div></div></div>`
119
+ );
120
+ }
121
+
122
+ const sliderId = pbStyleId();
123
+ addStyle(sliderId, 'min-height:600px');
124
+ const slider =
125
+ `<div data-content-type="slider" data-appearance="default" data-autoplay="false"` +
126
+ ` data-autoplay-speed="500" data-fade="false" data-infinite-loop="true" data-show-arrows="true"` +
127
+ ` data-show-dots="true" data-element="main" data-pb-style="${sliderId}" class="pagebuilder-slider">` +
128
+ slides.map(slide).join('') +
129
+ `</div>`;
130
+
131
+ const section1 = row('full-width', slider, 'padding:0');
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Section 2: CATEGORY (Figma node 1:621)
135
+ // ---------------------------------------------------------------------------
136
+ const categories = [
137
+ { label: 'Sandals', color: '#fd5e94', image: 'wysiwyg/melissa/img1.png' },
138
+ { label: 'Slide', color: '#3d8f57', image: 'wysiwyg/melissa/img2.png' },
139
+ { label: 'Platforms', color: '#725ca0', image: 'wysiwyg/melissa/img3.png' },
140
+ { label: 'Flip Flops', color: '#02a3cd', image: 'wysiwyg/melissa/img4.png' },
141
+ { label: 'Flats', color: '#6d483c', image: 'wysiwyg/melissa/img5.png' },
142
+ { label: 'Heels', color: '#822411', image: 'wysiwyg/melissa/img6.png' },
143
+ { label: 'Mules', color: '#08704f', image: 'wysiwyg/melissa/img7.png' },
144
+ { label: 'Boots', color: '#4c2f2b', image: 'wysiwyg/melissa/img8.png' },
145
+ { label: 'Sneakers', color: '#c24952', image: 'wysiwyg/melissa/img9.png' },
146
+ ];
147
+
148
+ const COLUMN_CSS =
149
+ 'justify-content:flex-start;display:flex;flex-direction:column;' +
150
+ 'background-position:left top;background-size:cover;background-repeat:no-repeat;' +
151
+ 'background-attachment:scroll;width:11.1111%;margin:0;padding:5px;align-self:stretch';
152
+
153
+ function categoryCard({ label, color, image }) {
154
+ const colId = pbStyleId();
155
+ const figId = pbStyleId();
156
+ const imgId = pbStyleId();
157
+ addStyle(colId, COLUMN_CSS);
158
+ addStyle(figId, `background-color:${color};border-radius:10px;margin:0;padding:10px;border-style:none`);
159
+ addStyle(imgId, 'max-width:100%;height:auto;border-radius:6px');
160
+ const src = `{{media url=${image}}}`;
161
+ return (
162
+ `<div data-content-type="column" data-appearance="full-height" data-background-images="{}"` +
163
+ ` data-background-type="image" data-video-loop="true" data-video-play-only-visible="true"` +
164
+ ` data-video-lazy-load="true" data-video-fallback-src="" data-element="main" data-pb-style="${colId}">` +
165
+ `<figure data-content-type="image" data-appearance="full-width" data-element="main" data-pb-style="${figId}">` +
166
+ `<img class="pagebuilder-mobile-hidden" src="${src}" alt="${label}" title="${label}" data-element="desktop_image" data-pb-style="${imgId}">` +
167
+ `<img class="pagebuilder-mobile-only" src="${src}" alt="${label}" title="${label}" data-element="mobile_image" data-pb-style="${imgId}">` +
168
+ `</figure>` +
169
+ textBlock(`<p style="text-align: center;"><strong>${label}</strong></p>`, 'margin-top:10px') +
170
+ `</div>`
171
+ );
172
+ }
173
+
174
+ // Builds a fresh 9-card grid (unique style ids per instance so each tab
175
+ // gets its own editable copy).
176
+ function categoryGrid() {
177
+ const lineId = pbStyleId();
178
+ addStyle(lineId, 'display:flex');
179
+ return (
180
+ `<div data-content-type="column-group" data-appearance="default" data-grid-size="12" data-element="main">` +
181
+ `<div data-content-type="column-line" data-appearance="default" data-element="main" data-pb-style="${lineId}">` +
182
+ categories.map(categoryCard).join('') +
183
+ `</div></div>`
184
+ );
185
+ }
186
+
187
+ // Page Builder Tabs element: adults / kids / baby
188
+ function tabId() {
189
+ return 'melissa-tab-' + Math.random().toString(36).slice(2, 10);
190
+ }
191
+
192
+ function tabsElement(items) {
193
+ const navId = pbStyleId();
194
+ const headersId = pbStyleId();
195
+ const contentId = pbStyleId();
196
+ addStyle(navId, 'text-align:center');
197
+ addStyle(headersId, 'border-radius:16px');
198
+ addStyle(contentId, 'border-width:1px;border-radius:0px;min-height:200px');
199
+
200
+ const ids = items.map(() => tabId());
201
+
202
+ const nav =
203
+ `<ul role="tablist" class="tabs-navigation" data-element="navigation" data-pb-style="${navId}">` +
204
+ items
205
+ .map(
206
+ (item, i) =>
207
+ `<li role="tab" class="tab-header" data-element="headers" data-pb-style="${headersId}">` +
208
+ `<a href="#${ids[i]}" class="tab-title"><span class="tab-title">${item.name}</span></a></li>`
209
+ )
210
+ .join('') +
211
+ `</ul>`;
212
+
213
+ const panels = items
214
+ .map((item, i) => {
215
+ const itemStyleId = pbStyleId();
216
+ addStyle(
217
+ itemStyleId,
218
+ 'justify-content:flex-start;display:flex;flex-direction:column;' +
219
+ 'background-position:left top;background-size:cover;background-repeat:no-repeat;' +
220
+ 'background-attachment:scroll;border-width:1px;border-radius:0px;min-height:200px;padding:30px 0'
221
+ );
222
+ return (
223
+ `<div data-content-type="tab-item" data-appearance="default" data-tab-name="${item.name}"` +
224
+ ` data-background-images="{}" data-background-type="image" data-video-loop="true"` +
225
+ ` data-video-play-only-visible="true" data-video-lazy-load="true" data-video-fallback-src=""` +
226
+ ` data-element="main" id="${ids[i]}" data-pb-style="${itemStyleId}">${item.content}</div>`
227
+ );
228
+ })
229
+ .join('');
230
+
231
+ return (
232
+ `<div data-content-type="tabs" data-appearance="default" data-active-tab="0" data-element="main">` +
233
+ nav +
234
+ `<div class="tabs-content" data-element="content" data-pb-style="${contentId}">${panels}</div></div>`
235
+ );
236
+ }
237
+
238
+ const categoryTabs = tabsElement([
239
+ { name: 'adults', content: categoryGrid() },
240
+ { name: 'kids', content: categoryGrid() },
241
+ { name: 'baby', content: categoryGrid() },
242
+ ]);
243
+
244
+ const section2 = row(
245
+ 'contained',
246
+ heading('h2', 'A look for every style + age', 'text-align:center;margin:10px 0;font-weight:500') +
247
+ textBlock('<p style="text-align: center;">Top sale on this week</p>') +
248
+ categoryTabs,
249
+ 'padding:40px 10px'
250
+ );
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Assemble + publish
254
+ // ---------------------------------------------------------------------------
255
+ const content = `<style>${styles.join('')}</style>` + section1 + section2;
256
+
257
+ const page = {
258
+ identifier: 'melissa-home-test',
259
+ title: 'melissa home test',
260
+ page_layout: 'cms-full-width',
261
+ content_heading: '',
262
+ content,
263
+ active: true,
264
+ };
265
+
266
+ // Upsert: update the existing page if it already exists, otherwise create it.
267
+ let result;
268
+ try {
269
+ const found = await client.get(
270
+ `/cmsPage/search?searchCriteria[filter_groups][0][filters][0][field]=identifier` +
271
+ `&searchCriteria[filter_groups][0][filters][0][value]=${page.identifier}` +
272
+ `&searchCriteria[filter_groups][0][filters][0][condition_type]=eq`
273
+ );
274
+ const existing = found?.items?.[0];
275
+ if (existing) {
276
+ result = await client.updatePage(existing.id, { ...existing, ...page });
277
+ console.log(`Updated page id=${result.id} identifier=${result.identifier}`);
278
+ } else {
279
+ result = await client.createPage(page);
280
+ console.log(`Created page id=${result.id} identifier=${result.identifier}`);
281
+ }
282
+ } catch (e) {
283
+ console.error(e.message || e);
284
+ process.exit(1);
285
+ }
286
+ console.log(`Admin edit URL: ${client.pageEditUrl(result.id)}`);
287
+ console.log(`Frontend URL: ${client.baseUrl}/${result.identifier}`);
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI: node scripts/health-check.js
4
+ * Loads .env and pings Magento CMS API.
5
+ */
6
+ import { MagentoClient, loadDotEnv } from '../src/magentoClient.js';
7
+
8
+ loadDotEnv();
9
+
10
+ try {
11
+ const client = MagentoClient.fromEnv();
12
+ const result = await client.health();
13
+ console.log(JSON.stringify(result, null, 2));
14
+ process.exit(0);
15
+ } catch (e) {
16
+ console.error(e.message || e);
17
+ process.exit(1);
18
+ }