diff --git a/scripts/notebooks/README.md b/scripts/notebooks/README.md
new file mode 100644
index 000000000..5df71a4d0
--- /dev/null
+++ b/scripts/notebooks/README.md
@@ -0,0 +1,42 @@
+# MAS Notebooks
+
+This directory contains Jupyter notebooks for migrating and managing merch cards.
+
+## Prerequisites
+
+### Environment Setup
+
+**Deno Runtime**: These notebooks require Deno to run JavaScript/TypeScript code.
+
+## Notebooks
+
+### 1. `eds_export.ipynb` - EDS Content Export
+
+**Purpose**: Exports merch card content from Adobe's Edge Delivery Services (EDS/Helix) system.
+
+**Key Features**:
+
+- Supports both Markdown (`.md`) and HTML (`.plain.html`) formats
+- Download a folder to local
+
+**Output**:
+
+- `${repo}_filelist.txt` - List of all discovered file paths
+- `./html/` directory - Downloaded HTML content files
+- `./md/` directory - Downloaded MD content files
+
+### 2. `aem_import.ipynb` - AEM ODIN Import
+
+**Purpose**: Parses exported HTML content and imports it as structured data into the ODIN system.
+
+**Input**:
+
+- `${repo}_filelist.txt` - List of all file paths to be imported
+
+### 3. `mas_update.ipynb` - Update M@S cards
+
+**Purpose**: Batch update existing merch cards
+
+### 4. `mas_copy.ipynb` - Copy M@S cards
+
+**Purpose**: Copy merch cards from one surface to the other
diff --git a/scripts/notebooks/aem_import.ipynb b/scripts/notebooks/aem_import.ipynb
new file mode 100644
index 000000000..b80ebc9d4
--- /dev/null
+++ b/scripts/notebooks/aem_import.ipynb
@@ -0,0 +1,593 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "ac3bb06e",
+ "metadata": {},
+ "source": [
+ "* Initialize the libraries and variables\n",
+ "* Add `AEM_TOKEN` to the `.env` file"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "06343339",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import { logError } from \"./logError.js\";\n",
+ "import { reloadEnv } from \"./reloadEnv.js\"\n",
+ "await reloadEnv();\n",
+ "\n",
+ "const token = Deno.env.get(\"AEM_TOKEN\");\n",
+ "const aemOrigin = 'https://author-p22655-e59433.adobeaemcloud.com';\n",
+ "const fileListFilename = 'cc_filelist.txt';\n",
+ "const format = 'html';"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f3654cab",
+ "metadata": {},
+ "source": [
+ "* Test if the token is working"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "56d56ccf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const url = `${aemOrigin}/bin/querybuilder.json?path=/content/dam/mas&path.flat=true&type=sling:Folder&p.limit=-1`;\n",
+ "\n",
+ "const response = await fetch(url, {\n",
+ " headers: {\n",
+ " \"Authorization\": `Bearer ${token}`\n",
+ " }\n",
+ "});\n",
+ "\n",
+ "if (!response.ok) {\n",
+ " logError(response);\n",
+ "}\n",
+ "\n",
+ "console.log(response.status);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9ba43e93",
+ "metadata": {},
+ "source": [
+ "* Read the list of file paths"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1a42fff8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const fileList = (await Deno.readTextFile(fileListFilename)).split('\\n');\n",
+ "console.log(`File count: ${fileList.length}`);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cc7052cd",
+ "metadata": {},
+ "source": [
+ "* Parse the HTMLs or MDs into fields "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6bd580ae",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import { DOMParser } from \"https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts\";\n",
+ "\n",
+ "const getText = (element) => element?.textContent?.trim() || '';\n",
+ "const getHref = (element) => element?.getAttribute('href') || '';\n",
+ "\n",
+ "const extractOsi = (url) => {\n",
+ " try {\n",
+ " const urlObj = new URL(url);\n",
+ " return urlObj.searchParams.get('osi') || '';\n",
+ " } catch (e) {\n",
+ " return '';\n",
+ " }\n",
+ "};\n",
+ "\n",
+ "const ext = {'md': '.md', 'html': '.plain.html'}[format];\n",
+ "const fileFolder = {'md': './md', 'html': './html'}[format];\n",
+ "\n",
+ "const items = [];\n",
+ "const itemErrors = [];\n",
+ "\n",
+ "function processCta(ctaLinks, filePath) {\n",
+ " const ctas = [];\n",
+ " let osi = '';\n",
+ "\n",
+ " if (ctaLinks.length > 3 || ctaLinks.length === 0) {\n",
+ " throw new Error(`${filePath}: Got ${ctaLinks.length} CTAs\\n${ctaChild.outerHTML}`);\n",
+ " }\n",
+ "\n",
+ " for (const ctaLink of ctaLinks) {\n",
+ " const href = getHref(ctaLink);\n",
+ " const [text, aria] = getText(ctaLink).split('|').map(s => s.trim());\n",
+ " ctas.push({\n",
+ " text,\n",
+ " aria,\n",
+ " href: href,\n",
+ " osi: extractOsi(href),\n",
+ " parentTag: ctaLink.parentElement.tagName,\n",
+ " });\n",
+ " }\n",
+ "\n",
+ " const osiCtas = ctas.filter(cta => cta.osi);\n",
+ " if (osiCtas.length > 0) {\n",
+ " osi = osiCtas[0].osi;\n",
+ " }\n",
+ "\n",
+ " return { ctas, osi };\n",
+ "}\n",
+ "\n",
+ "for (let i = 0; i < fileList.length; i++) {\n",
+ " const filePath = fileList[i];\n",
+ " const localPath = `${fileFolder}${filePath}${ext}`;\n",
+ "\n",
+ " const htmlContent = await Deno.readTextFile(localPath);\n",
+ "\n",
+ " const doc = new DOMParser().parseFromString(htmlContent, 'text/html');\n",
+ "\n",
+ " const rows = doc.querySelectorAll('.merch-card.catalog > div > div');\n",
+ "\n",
+ " if (rows.length === 0) {\n",
+ " itemErrors.push(filePath);\n",
+ " continue;\n",
+ " }\n",
+ "\n",
+ " const result = {\n",
+ " filePath,\n",
+ " column1Html: '',\n",
+ " deviceTypes: [],\n",
+ " recommendedFor: [],\n",
+ " mnemonicIcons: [],\n",
+ " cardTitle: '',\n",
+ " cardTitleLink: '',\n",
+ " description: '',\n",
+ " ctas: [],\n",
+ " ctasHtml: '',\n",
+ " osi: '',\n",
+ " tags: [],\n",
+ " };\n",
+ "\n",
+ " let rowIndex = 0;\n",
+ "\n",
+ " if (rows.length > 2) {\n",
+ " // Row 1: Device types and Recommended for\n",
+ " const row1 = rows[rowIndex++];\n",
+ " // Clean up excess whitespaces\n",
+ " result.column1Html = row1.innerHTML\n",
+ " .split('\\n')\n",
+ " .map(line => line.trim())\n",
+ " .filter(line => line.length > 0)\n",
+ " .join('');\n",
+ " const lists = row1.querySelectorAll('ul');\n",
+ " if (lists.length >= 1) {\n",
+ " const deviceItems = lists[0].querySelectorAll('li');\n",
+ " result.deviceTypes = Array.from(deviceItems).map(li => getText(li));\n",
+ " }\n",
+ " if (lists.length >= 2) {\n",
+ " const recItems = lists[1].querySelectorAll('li');\n",
+ " result.recommendedFor = Array.from(recItems).map(li => getText(li));\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " if (rows.length >= 2) {\n",
+ " // Row 2: Main content with 4 or 5 children\n",
+ " const row2 = rows[rowIndex++];\n",
+ " const children = Array.from(row2.children);\n",
+ "\n",
+ " // Child 1: Mnemonic icons (could be multiple icons, each with its link)\n",
+ " if (children.length >= 1) {\n",
+ " const iconLinks = children[0].querySelectorAll('a');\n",
+ " iconLinks.forEach(iconLink => {\n",
+ " const text = getText(iconLink);\n",
+ " const parts = text.split('|').map(s => s.trim());\n",
+ " result.mnemonicIcons.push({\n",
+ " icon: parts[0] || '',\n",
+ " alt: parts[1] || '',\n",
+ " link: getHref(iconLink)\n",
+ " });\n",
+ " });\n",
+ " }\n",
+ "\n",
+ " // Child 2: Title and title link (h3)\n",
+ " if (children.length >= 2) {\n",
+ " const titleLink = children[1].querySelector('a');\n",
+ " if (titleLink) {\n",
+ " result.cardTitle = getText(titleLink);\n",
+ " result.cardTitleLink = getHref(titleLink);\n",
+ " } else {\n",
+ " result.cardTitle = getText(children[1]);\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " // Child 3: Description (includes description text and learn more link)\n",
+ " if (children.length >= 3) {\n",
+ " const descChild = children[2];\n",
+ " result.description = descChild.outerHTML;\n",
+ " }\n",
+ "\n",
+ " // Child 4: Footer CTAs (strong and em links)\n",
+ " if (children.length >= 4) {\n",
+ " const ctaChild = children[3];\n",
+ " result.ctasHtml = ctaChild.innerHTML;\n",
+ " \n",
+ " const ctaLinks = ctaChild.querySelectorAll('a');\n",
+ " const { ctas, osi } = processCta(ctaLinks, filePath);\n",
+ " result.ctas = ctas;\n",
+ " result.osi = osi;\n",
+ " }\n",
+ "\n",
+ " if (children.length >= 5) {\n",
+ " // Move line 4 to description\n",
+ " const learnmore = result.ctas.map(x => `${x.text}`).join(' | ');\n",
+ " result.description = result.description.replace(new RegExp('
$'), `
${learnmore}`);\n",
+ "\n",
+ " // Process the new CTAs\n",
+ " const ctaChild = children[4];\n",
+ " result.ctasHtml = ctaChild.innerHTML;\n",
+ "\n",
+ " const ctaLinks = ctaChild.querySelectorAll('a');\n",
+ " const { ctas, osi } = processCta(ctaLinks, filePath);\n",
+ " result.ctas = ctas;\n",
+ " result.osi = osi; \n",
+ " }\n",
+ " }\n",
+ "\n",
+ " if (rows.length >= 2) {\n",
+ " // Row 3: Tags/categories\n",
+ " const row3 = rows[rowIndex++];\n",
+ " const tagParagraphs = row3.querySelectorAll('p');\n",
+ " result.tags = Array.from(tagParagraphs).map(p => getText(p));\n",
+ " }\n",
+ "\n",
+ " items.push(result);\n",
+ "}\n",
+ "\n",
+ "console.log(`Read ${items.length} cards`);\n",
+ "console.log(`Errors: ${itemErrors.length}\\n${itemErrors.join('\\n')}`);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b5d25b8b",
+ "metadata": {},
+ "source": [
+ "* Review the parsed items"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c32e12b2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "console.log(JSON.stringify(items[0], null, 2));"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ae810ec8",
+ "metadata": {},
+ "source": [
+ "* Normalize the items for writing to ODIN\n",
+ "* Some fields need be proccessed first"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1658addf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const normItems = [];\n",
+ "for (let i = 0; i < items.length; i++) {\n",
+ " // Clone the item\n",
+ " const item = JSON.parse(JSON.stringify(items[i]));\n",
+ "\n",
+ " // Update the mnemonic icons links to hlx.page/aem.page to aem.live\n",
+ "\n",
+ " item.mnemonicIcons.forEach(x => {\n",
+ " x.icon = x.icon.replace('hlx.page', 'aem.live').replace('aem.page', 'aem.live');\n",
+ " });\n",
+ " \n",
+ " // Update the card description\n",
+ " const descDoc = new DOMParser().parseFromString(item.description, 'text/html');\n",
+ " const descLinks = descDoc.querySelectorAll('a');\n",
+ " if (descLinks.length > 0) {\n",
+ " descLinks.forEach(x => {\n",
+ " const [text, aria] = x.textContent.split('|').map(s => s.trim());\n",
+ " x.textContent = text;\n",
+ " x.setAttribute('aria-label', aria);\n",
+ " });\n",
+ " }\n",
+ " item.description = descDoc.body.innerHTML;\n",
+ "\n",
+ " // Update the ctas text\n",
+ " item.ctas.forEach(x => {\n",
+ " const ctaText = x.text.split('|').map(s => s.trim());\n",
+ " x.text = ctaText[0];\n",
+ " x.alt = ctaText[1];\n",
+ " });\n",
+ "\n",
+ " item.ctasHtml = item.ctas.map(x => {\n",
+ " const attrs = [];\n",
+ " let className = { 'strong': 'accent', 'em': 'primary-outline' }[x.parentTag.toLowerCase()];\n",
+ " if (className) {\n",
+ " attrs.push(`class=\"${className}\"`);\n",
+ " } else {\n",
+ " attrs.push(`class=\"primary-link\"`);\n",
+ " }\n",
+ " if (x.href) {\n",
+ " attrs.push(`href=\"${x.href}\"`);\n",
+ " }\n",
+ " if (x.aria) {\n",
+ " attrs.push(`aria-label=\"${x.aria}\"`);\n",
+ " }\n",
+ " return `${x.text}`;\n",
+ " }).join(' ');\n",
+ " \n",
+ " normItems.push(item);\n",
+ "}\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c49c19c0",
+ "metadata": {},
+ "source": [
+ "* Review the normalized items"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cb5b74a4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "console.log(JSON.stringify(normItems[0], null, 2));"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "36faa114",
+ "metadata": {},
+ "source": [
+ "* Write to ODIN\n",
+ "* Update `parentPath` if needed"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4868b27f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const url = `${aemOrigin}/adobe/sites/cf/fragments`;\n",
+ "const parentPath = `/content/dam/mas/sandbox/en_US`;\n",
+ "\n",
+ "const odinResults = [];\n",
+ "const odinErrors = [];\n",
+ "\n",
+ "//for (let i=0; i icon.icon)\n",
+ "\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"mnemonicLink\",\n",
+ " \"type\": \"text\",\n",
+ " \"multiple\": true,\n",
+ " \"locked\": false,\n",
+ " \"values\": result.mnemonicIcons.map(icon => icon.link)\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"shortDescription\",\n",
+ " \"type\": \"long-text\",\n",
+ " \"multiple\": false,\n",
+ " \"locked\": false,\n",
+ " \"mimeType\": \"text/html\",\n",
+ " \"values\": [\n",
+ " result.shortDescription\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"description\",\n",
+ " \"type\": \"long-text\",\n",
+ " \"multiple\": false,\n",
+ " \"locked\": false,\n",
+ " \"mimeType\": \"text/html\",\n",
+ " \"values\": [\n",
+ " result.description\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"ctas\",\n",
+ " \"type\": \"long-text\",\n",
+ " \"multiple\": false,\n",
+ " \"locked\": false,\n",
+ " \"mimeType\": \"text/html\",\n",
+ " \"values\": [\n",
+ " result.ctasHtml\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"name\": \"osi\",\n",
+ " \"type\": \"text\",\n",
+ " \"multiple\": false,\n",
+ " \"locked\": false,\n",
+ " \"values\": [\n",
+ " result.osi\n",
+ " ]\n",
+ " },\n",
+ " ];\n",
+ "\n",
+ " const response = await fetch(url, {\n",
+ " method: 'POST',\n",
+ " headers: {\n",
+ " 'Content-Type': 'application/json',\n",
+ " \"x-api-key\": \"mas-studio\",\n",
+ " \"Authorization\": `Bearer ${token}`,\n",
+ " 'x-aem-affinity-type': 'api',\n",
+ " },\n",
+ " body: JSON.stringify({\n",
+ " title,\n",
+ " name,\n",
+ " modelId,\n",
+ " parentPath,\n",
+ " description,\n",
+ " fields,\n",
+ " }),\n",
+ " })\n",
+ "\n",
+ " if (!response.ok) {\n",
+ " odinErrors.push(response);\n",
+ " } else {\n",
+ " const data = await response.json();\n",
+ " odinResults.push(data);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "console.log(`Imported ${odinResults.length} cards`);\n",
+ "console.log(`Errors: ${odinErrors.length}`);\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3f4a36c0",
+ "metadata": {},
+ "source": [
+ "* List IDs of imported cards "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "abb41f67",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const ids = odinResults.map(x => x.id);\n",
+ "console.log(ids);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a4e1a69d",
+ "metadata": {},
+ "source": [
+ "* Check the imported card\n",
+ "* Update `cardId`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6fe224c2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const cardId = ids[0];\n",
+ "const url = `${aemOrigin}/adobe/sites/cf/fragments/${cardId}?references=direct-hydrated`;\n",
+ "\n",
+ "const response = await fetch(url, {\n",
+ " headers: {\n",
+ " \"x-api-key\": \"mas-studio\",\n",
+ " \"Authorization\": `Bearer ${token}`,\n",
+ " pragma: 'no-cache',\n",
+ " 'cache-control': 'no-cache',\n",
+ " 'x-aem-affinity-type': 'api',\n",
+ " }\n",
+ "});\n",
+ "\n",
+ "if (!response.ok) {\n",
+ " logError(response);\n",
+ "}\n",
+ "\n",
+ "const data = await response.json();\n",
+ "await Deno.writeTextFile('catalog.json', JSON.stringify(data, null, 2));\n",
+ "console.log(JSON.stringify(data, null, 2));"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Deno",
+ "language": "typescript",
+ "name": "deno"
+ },
+ "language_info": {
+ "codemirror_mode": "typescript",
+ "file_extension": ".ts",
+ "mimetype": "text/x.typescript",
+ "name": "typescript",
+ "nbconvert_exporter": "script",
+ "pygments_lexer": "typescript",
+ "version": "5.7.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/scripts/notebooks/eds_export.ipynb b/scripts/notebooks/eds_export.ipynb
new file mode 100644
index 000000000..d1186dc75
--- /dev/null
+++ b/scripts/notebooks/eds_export.ipynb
@@ -0,0 +1,203 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "c682a6ee",
+ "metadata": {},
+ "source": [
+ "* Add `EDS_TOKEN` to the `.env` file"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7c34ac05",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import { logError } from \"./logError.js\";\n",
+ "import { reloadEnv } from \"./reloadEnv.js\"\n",
+ "await reloadEnv();\n",
+ "\n",
+ "const token = Deno.env.get(\"EDS_TOKEN\");\n",
+ "\n",
+ "const org = 'adobecom';\n",
+ "const repo = 'cc';\n",
+ "const merchCardPath = '/cc-shared/fragments/merch/products/catalog/merch-card';\n",
+ "const format = 'html'; // 'md' or 'html'\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e3b635ce",
+ "metadata": {},
+ "source": [
+ "* Test if the token is working"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7157d132",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const response = await fetch(`https://admin.hlx.page/status/${org}/${repo}/main/`, {\n",
+ " headers: {\n",
+ " 'x-auth-token': token\n",
+ " }\n",
+ "});\n",
+ "\n",
+ "if (!response.ok) {\n",
+ " logError(response);\n",
+ "}\n",
+ "\n",
+ "console.log(response.status);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "518700eb",
+ "metadata": {},
+ "source": [
+ "* Query the list of files with Helix API"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "7e75782e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import { scheduler } from \"node:timers/promises\";\n",
+ "\n",
+ "const response = await fetch(`https://admin.hlx.page/status/${org}/${repo}/main/*`, {\n",
+ " method: 'POST',\n",
+ " headers: {\n",
+ " 'x-auth-token': token,\n",
+ " 'Content-Type': 'application/json'\n",
+ " },\n",
+ "\n",
+ " body: JSON.stringify({\n",
+ " \"select\": [\n",
+ " 'live'\n",
+ " ],\n",
+ " \"paths\": [\n",
+ " `${merchCardPath}/*`\n",
+ " ],\n",
+ " \"pathsOnly\": true\n",
+ " })\n",
+ "});\n",
+ "\n",
+ "if (!response.ok) {\n",
+ " logError(response);\n",
+ "}\n",
+ "\n",
+ "const data = await response.json();\n",
+ "console.log(`Job link: ${data.links.self}`);\n",
+ "\n",
+ "const jobLink = data.links.self;\n",
+ "\n",
+ "let jobDetails;\n",
+ "\n",
+ "let retry = 10;\n",
+ "while (retry > 0) {\n",
+ " const responseState = await fetch(jobLink, {\n",
+ " headers: {\n",
+ " 'x-auth-token': token\n",
+ " }\n",
+ " });\n",
+ "\n",
+ " if (!responseState.ok) {\n",
+ " logError(responseState);\n",
+ " }\n",
+ "\n",
+ " const dataState = await responseState.json();\n",
+ "\n",
+ " console.log(`Job state: ${dataState.state}`);\n",
+ "\n",
+ " if (dataState.state === 'stopped') {\n",
+ " jobDetails = dataState.links.details;\n",
+ " break;\n",
+ " }\n",
+ "\n",
+ " await scheduler.wait(5000)\n",
+ " retry -= 1;\n",
+ "}\n",
+ "\n",
+ "const responseDetail = await fetch(jobDetails, {\n",
+ " headers: {\n",
+ " 'x-auth-token': token\n",
+ " }\n",
+ "});\n",
+ "\n",
+ "if (!responseDetail.ok) {\n",
+ " logError(responseDetail);\n",
+ "}\n",
+ "\n",
+ "const dataDetail = await responseDetail.json();\n",
+ "const fileList = dataDetail.data.resources['live'];\n",
+ "\n",
+ "console.log(`page and asset count: ${fileList.length}`);\n",
+ "\n",
+ "const fileName = `${repo}_filelist.txt`;\n",
+ "await Deno.writeTextFile(fileName, fileList.join('\\n'));\n",
+ "console.log(`\\nOutput saved to: ${fileName}`);\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "964960a6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import { ensureDir } from \"https://deno.land/std@0.208.0/fs/ensure_dir.ts\";\n",
+ "import { dirname } from \"https://deno.land/std@0.208.0/path/mod.ts\";\n",
+ "\n",
+ "const ext = {'md': '.md', 'html': '.plain.html'}[format];\n",
+ "\n",
+ "let successCount = 0;\n",
+ "let failureCount = 0;\n",
+ "\n",
+ "if (ext) {\n",
+ " for (let i = 0; i < fileList.length; i++) {\n",
+ " const filePath = fileList[i];\n",
+ " const response = await fetch(`https://main--${repo}--${org}.aem.live${filePath}${ext}`);\n",
+ " if (!response.ok) {\n",
+ " logError(response);\n",
+ " failureCount++;\n",
+ " } else {\n",
+ " const data = await response.text();\n",
+ " const localPath = `./${format}${filePath}${ext}`;\n",
+ " await ensureDir(dirname(localPath));\n",
+ " await Deno.writeTextFile(localPath, data);\n",
+ " successCount++;\n",
+ " }\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "console.log(`\\nResults: ${successCount} succeeded, ${failureCount} failed out of ${fileList.length} total`);\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Deno",
+ "language": "typescript",
+ "name": "deno"
+ },
+ "language_info": {
+ "codemirror_mode": "typescript",
+ "file_extension": ".ts",
+ "mimetype": "text/x.typescript",
+ "name": "typescript",
+ "nbconvert_exporter": "script",
+ "pygments_lexer": "typescript",
+ "version": "5.7.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/scripts/notebooks/logError.js b/scripts/notebooks/logError.js
new file mode 100644
index 000000000..cb5d31c4f
--- /dev/null
+++ b/scripts/notebooks/logError.js
@@ -0,0 +1,18 @@
+export async function logError(response) {
+ console.error(`Request failed with status: ${response.status} ${response.statusText}`);
+
+ console.error('Response Headers:');
+ for (const [key, value] of response.headers.entries()) {
+ console.error(` ${key}: ${value}`);
+ }
+
+ try {
+ const errorData = await response.json();
+ console.error('Error Response Body:', errorData);
+ } catch (e) {
+ const errorText = await response.text();
+ console.error('Error Response Body (text):', errorText);
+ }
+
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+}
diff --git a/scripts/notebooks/mas_copy.ipynb b/scripts/notebooks/mas_copy.ipynb
new file mode 100644
index 000000000..2e828ad1e
--- /dev/null
+++ b/scripts/notebooks/mas_copy.ipynb
@@ -0,0 +1,231 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "50093cc3",
+ "metadata": {},
+ "source": [
+ "* Initialize the libraries and variables"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a80c4080",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import { DOMParser } from \"https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts\";\n",
+ "import { logError } from \"./logError.js\";\n",
+ "import { reloadEnv } from \"./reloadEnv.js\"\n",
+ "await reloadEnv();\n",
+ "\n",
+ "const token = Deno.env.get(\"AEM_TOKEN\");\n",
+ "const aemOrigin = 'https://author-p22655-e59433.adobeaemcloud.com';\n",
+ "const destination = '/content/dam/mas/acom/en_US/';"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0ce87c5a",
+ "metadata": {},
+ "source": [
+ "* Test if the token is working"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fb7c757f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const url = `${aemOrigin}/bin/querybuilder.json?path=/content/dam/mas&path.flat=true&type=sling:Folder&p.limit=-1`;\n",
+ "\n",
+ "const response = await fetch(url, {\n",
+ " headers: {\n",
+ " \"Authorization\": `Bearer ${token}`\n",
+ " }\n",
+ "});\n",
+ "\n",
+ "if (!response.ok) {\n",
+ " logError(response);\n",
+ "}\n",
+ "\n",
+ "console.log(response.status);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "54772fa4",
+ "metadata": {},
+ "source": [
+ "* Read card id to be copied from a file"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "222b9fe0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const idListFilename = './copy_idlist.txt';\n",
+ "const idList = (await Deno.readTextFile(idListFilename)).split('\\n');\n",
+ "console.log(`Id count: ${idList.length}`);\n",
+ "console.log(idList.join('\\n'));"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "35ab10c9",
+ "metadata": {},
+ "source": [
+ "* Fetch all cards to be copied"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "01537438",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const cardsToBeCopied = [];\n",
+ "\n",
+ "for (const item of idList) {\n",
+ " const cardId = item;\n",
+ " const url = `${aemOrigin}/adobe/sites/cf/fragments/${cardId}`;\n",
+ "\n",
+ "\n",
+ " // Get the card\n",
+ " const response = await fetch(url, {\n",
+ " headers: {\n",
+ " \"x-api-key\": \"mas-studio\",\n",
+ " \"Authorization\": `Bearer ${token}`,\n",
+ " pragma: 'no-cache',\n",
+ " 'cache-control': 'no-cache',\n",
+ " 'x-aem-affinity-type': 'api',\n",
+ " }\n",
+ " });\n",
+ "\n",
+ " if (!response.ok) {\n",
+ " logError(response);\n",
+ " } else {\n",
+ " const data = await response.json();\n",
+ " cardsToBeCopied.push(data);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "console.log(`Total cards: ${cardsToBeCopied.length}`);\n",
+ "console.log(JSON.stringify(cardsToBeCopied[0], null, 2));\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "438c3756",
+ "metadata": {},
+ "source": [
+ "* Save to the destination"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ddb54094",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const cardsDone = [];\n",
+ "const cardsFailed = [];\n",
+ "\n",
+ "for (const item of cardsToBeCopied) {\n",
+ " const cardId = item.id;\n",
+ " const url = `${aemOrigin}/adobe/sites/cf/fragments/${cardId}`;\n",
+ "\n",
+ " console.log(`Getting card ${cardId}`);\n",
+ "\n",
+ " // Get the card\n",
+ " const response = await fetch(url, {\n",
+ " headers: {\n",
+ " \"x-api-key\": \"mas-studio\",\n",
+ " \"Authorization\": `Bearer ${token}`,\n",
+ " pragma: 'no-cache',\n",
+ " 'cache-control': 'no-cache',\n",
+ " 'x-aem-affinity-type': 'api',\n",
+ " }\n",
+ " });\n",
+ "\n",
+ " if (!response.ok) {\n",
+ " logError(response);\n",
+ " cardsFailed.push(item);\n",
+ " continue;\n",
+ " }\n",
+ " const data = await response.json();\n",
+ "\n",
+ " // Save to the destination\n",
+ " const parentPath = destination;\n",
+ " const name = data.path.replace(/^.*[\\\\\\/]/, '');\n",
+ "\n",
+ " console.log(`Saving card ${name} to ${parentPath}`);\n",
+ "\n",
+ " const title = data.title;\n",
+ " const modelId = data.model.id;\n",
+ " const description = data.description;\n",
+ " const fields = data.fields;\n",
+ " \n",
+ " const urlSave = `${aemOrigin}/adobe/sites/cf/fragments`;\n",
+ "\n",
+ " const responseSave = await fetch(urlSave, {\n",
+ " method: 'POST',\n",
+ " headers: {\n",
+ " 'Content-Type': 'application/json',\n",
+ " \"x-api-key\": \"mas-studio\",\n",
+ " \"Authorization\": `Bearer ${token}`,\n",
+ " 'x-aem-affinity-type': 'api',\n",
+ " },\n",
+ " body: JSON.stringify({\n",
+ " title,\n",
+ " name,\n",
+ " modelId,\n",
+ " parentPath,\n",
+ " description,\n",
+ " fields,\n",
+ " }),\n",
+ " })\n",
+ "\n",
+ " if (!responseSave.ok) {\n",
+ " logError(responseSave);\n",
+ " cardsFailed.push(item);\n",
+ " console.log(`Failed to save card ${name} to ${parentPath}`);\n",
+ " } else {\n",
+ " console.log(`Saving card ${name} to ${parentPath}`);\n",
+ " const data = await responseSave.json();\n",
+ " cardsDone.push(data);\n",
+ " } \n",
+ "}\n",
+ "\n",
+ "console.log(`Total cards copied: ${cardsDone.length}`);\n",
+ "console.log(`Total cards failed: ${cardsFailed.length}`);"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Deno",
+ "language": "typescript",
+ "name": "deno"
+ },
+ "language_info": {
+ "codemirror_mode": "typescript",
+ "file_extension": ".ts",
+ "mimetype": "text/x.typescript",
+ "name": "typescript",
+ "nbconvert_exporter": "script",
+ "pygments_lexer": "typescript",
+ "version": "5.7.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/scripts/notebooks/mas_update.ipynb b/scripts/notebooks/mas_update.ipynb
new file mode 100644
index 000000000..5015cd520
--- /dev/null
+++ b/scripts/notebooks/mas_update.ipynb
@@ -0,0 +1,262 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "50093cc3",
+ "metadata": {},
+ "source": [
+ "* Initialize the libraries and variables"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a80c4080",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import { DOMParser } from \"https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts\";\n",
+ "import { logError } from \"./logError.js\";\n",
+ "import { reloadEnv } from \"./reloadEnv.js\"\n",
+ "await reloadEnv();\n",
+ "\n",
+ "const token = Deno.env.get(\"AEM_TOKEN\");\n",
+ "const aemOrigin = 'https://author-p22655-e59433.adobeaemcloud.com';"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "54772fa4",
+ "metadata": {},
+ "source": [
+ "* Fetch the cards to be proccessed\n",
+ "* Update the query"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "01537438",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const cardPath = '/content/dam/mas/sandbox';\n",
+ "const createdBy = ''; // with @adobe.com\n",
+ "\n",
+ "const query = {\n",
+ " filter: {\n",
+ " path: cardPath,\n",
+ " created: {\n",
+ " by: [createdBy],\n",
+ " }\n",
+ " } \n",
+ "};\n",
+ "\n",
+ "const items = [];\n",
+ "let cursor = 'start';\n",
+ "\n",
+ "while (cursor) {\n",
+ " let url = `${aemOrigin}/adobe/sites/cf/fragments/search?query=${JSON.stringify(query)}`;\n",
+ " \n",
+ " if (cursor !== 'start') {\n",
+ " url += `&cursor=${cursor}`;\n",
+ " }\n",
+ "\n",
+ " const response = await fetch(url, {\n",
+ " headers: {\n",
+ " \"x-api-key\": \"mas-studio\",\n",
+ " \"Authorization\": `Bearer ${token}`,\n",
+ " } \n",
+ " });\n",
+ "\n",
+ " if (!response.ok) {\n",
+ " logError(response);\n",
+ " }\n",
+ "\n",
+ " const data = await response.json();\n",
+ " \n",
+ " items.push(...data.items);\n",
+ " cursor = data.cursor;\n",
+ "\n",
+ " console.log(`Fetched ${data.items.length} items`);\n",
+ "}\n",
+ "\n",
+ "console.log(`Total items: ${items.length}`);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "438c3756",
+ "metadata": {},
+ "source": [
+ "* Review the items"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ab2398ed",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "console.log(JSON.stringify(items[0], null, 2));"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "99244f3b",
+ "metadata": {},
+ "source": [
+ "* Filter and keep cards to be modified\n",
+ "* Find cards that contain descriptions with links of the `secondary-link` class."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "daa3c664",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "const toBeModified = [];\n",
+ "\n",
+ "for (const item of items) {\n",
+ " const descHtml = item.fields.filter(f => f.name === 'description')[0].values[0];\n",
+ " const doc = new DOMParser().parseFromString(descHtml, 'text/html');\n",
+ " const links = doc.querySelectorAll('a');\n",
+ " if ([...links].some(x => x.getAttribute('class')?.includes('secondary-link'))) {\n",
+ " toBeModified.push(item);\n",
+ " }\n",
+ "}\n",
+ "\n",
+ "console.log(`Total items to be modified: ${toBeModified.length}`);"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "14dd8328",
+ "metadata": {},
+ "source": [
+ "* Review cards to be updated"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b116ec36",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "console.log(JSON.stringify(toBeModified[0], null, 2));"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3d524c68",
+ "metadata": {},
+ "source": [
+ "* Updated the cards to be modified\n",
+ "* For each card, read the card first and keep etag and then update the card"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ddb54094",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "for (const item of toBeModified) {\n",
+ " const cardId = item.id;\n",
+ " const url = `${aemOrigin}/adobe/sites/cf/fragments/${cardId}`;\n",
+ "\n",
+ " console.log(`Getting card ${cardId}`);\n",
+ "\n",
+ " // Get the card\n",
+ " const response = await fetch(url, {\n",
+ " headers: {\n",
+ " \"x-api-key\": \"mas-studio\",\n",
+ " \"Authorization\": `Bearer ${token}`,\n",
+ " pragma: 'no-cache',\n",
+ " 'cache-control': 'no-cache',\n",
+ " 'x-aem-affinity-type': 'api',\n",
+ " }\n",
+ " });\n",
+ "\n",
+ " if (!response.ok) {\n",
+ " logError(response);\n",
+ " }\n",
+ "\n",
+ " const data = await response.json();\n",
+ " const etag = response.headers.get('etag');\n",
+ "\n",
+ " // Update the description\n",
+ "\n",
+ " // Get the current value\n",
+ " const descField = data.fields.filter(f => f.name === 'description')[0];\n",
+ " const descHtml = descField.values[0];\n",
+ "\n",
+ " console.log(`Before:\\n ${descHtml}`);\n",
+ "\n",
+ " // Prepare the update\n",
+ " const doc = new DOMParser().parseFromString(descHtml, 'text/html');\n",
+ " const links = doc.querySelectorAll('a');\n",
+ " for (const link of links) {\n",
+ " if (link.getAttribute('class')?.includes('secondary-link')) {\n",
+ " link.setAttribute('class', 'primary-link');\n",
+ " }\n",
+ " }\n",
+ " descField.values[0] = doc.body.innerHTML;\n",
+ " console.log(`Ready to update:\\n ${descField.values[0]}`);\n",
+ "\n",
+ " // Update the card\n",
+ " const responsePut = await fetch(url, {\n",
+ " method: 'PUT',\n",
+ " headers: {\n",
+ " 'Content-Type': 'application/json',\n",
+ " \"x-api-key\": \"mas-studio\",\n",
+ " \"Authorization\": `Bearer ${token}`,\n",
+ " pragma: 'no-cache',\n",
+ " 'cache-control': 'no-cache',\n",
+ " 'x-aem-affinity-type': 'api',\n",
+ " 'If-Match': etag,\n",
+ " },\n",
+ " body: JSON.stringify({\n",
+ " title: data.title,\n",
+ " name: data.name,\n",
+ " modelId: data.modelId,\n",
+ " parentPath: data.parentPath,\n",
+ " description: data.description,\n",
+ " fields: data.fields,\n",
+ " }),\n",
+ " });\n",
+ "\n",
+ " if (!responsePut.ok) {\n",
+ " logError(responsePut);\n",
+ " }\n",
+ "\n",
+ " const dataPut = await responsePut.json();\n",
+ " console.log(`After:\\n ${dataPut.fields.filter(f => f.name === 'description')[0].values[0]}`);\n",
+ "}"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Deno",
+ "language": "typescript",
+ "name": "deno"
+ },
+ "language_info": {
+ "codemirror_mode": "typescript",
+ "file_extension": ".ts",
+ "mimetype": "text/x.typescript",
+ "name": "typescript",
+ "nbconvert_exporter": "script",
+ "pygments_lexer": "typescript",
+ "version": "5.7.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/scripts/notebooks/reloadEnv.js b/scripts/notebooks/reloadEnv.js
new file mode 100644
index 000000000..bc3b837e5
--- /dev/null
+++ b/scripts/notebooks/reloadEnv.js
@@ -0,0 +1,27 @@
+// Force reload environment variables by clearing existing ones first
+import { load } from 'jsr:@std/dotenv';
+
+export async function reloadEnv() {
+ const envPath = '../../.env';
+ try {
+ // Read the .env file content directly
+ const envContent = await Deno.readTextFile(envPath);
+ const envLines = envContent.split('\n').filter((line) => line.trim() && !line.startsWith('#'));
+
+ // Clear existing environment variables that are defined in .env
+ for (const line of envLines) {
+ const [key] = line.split('=');
+ if (key) {
+ Deno.env.delete(key.trim());
+ }
+ }
+
+ // Now load fresh from .env file
+ await load({ envPath, export: true, override: true });
+ console.log('Environment variables reloaded successfully');
+ } catch (error) {
+ console.error('Error reloading environment variables:', error);
+ // Fallback to regular load
+ await load({ envPath, export: true, override: true });
+ }
+}