AEM Edge Delivery - Claude AEM AI Agent Skill to Create JCR Package of Assets in a Page


Following steps explains the process of creating a Custom Claude AEM Skill eaem-create-assets-package-from-page-skill which in turn uses the scrape-webpage skill of Experience Production Agent -> Site Migration Skill to Download Images from a web page and Create a JCR Package

AI Assistant Claude Sonnet 4.5 integrated into Cursor IDE was used for the skill execution...

Entered following Prompt in AI Assistant to Run the Skill...


create jcr package of assets from "https://wknd.site/us/en.html",
run it at folder "eaem-dev-eds",
before running if the folder "eaem-dev-eds/import-work" exists, delete it


Documentation:

            AEM Coding Agents : https://www.aem.live/developer/ai-coding-agents



Skill Execution in Cursor



Package Created




Solution


1) Install Github cli, gh-upskill for getting the Claude AEM Skills from your Github Repo into EDS Project...

                      winget install --id GitHub.cli
                      gh extension install trieloff/gh-upskill


2) Install available AEM Skills...

                      gh upskill adobe/helix-website --all


3) Steps below explain the creation of Custom AEM Skill eaem-create-assets-package-from-page-skill which can be installed using following command...

                      gh upskill schoudry/claude-aem-skills --skill eaem-create-assets-package-from-page-skill


4)  Create folder eaem-create-assets-package-from-page-skill and Markdown file eaem-create-assets-package-from-page-skill\SKILL.md with following content. It gives step by step instructions (prompts) for the AI assistant to follow, executing necessary tasks (scrape webpage -> images only confirmation > install dependencies if necessary > run package creation script)  

---
name: eaem-create-assets-package-from-page-skill
description: Creates a AEM JCR Package with the images downloaded by scrape-webpage skill
---

# Create Assets Package from Page Skill

Collect the images created by Scrape webpage skill and create a JCR Package

## Prerequisites

Before using this skill, ensure:
- ✅ scrape-webpage skill is available

## Asset Package Workflow

### Step 1: Scrape Webpage

**Invoke:** scrape-webpage skill

**Provide:**
- Target URL
- Output directory: `./import-work`

**Success criteria:**
- ✅ images/ folder with all downloaded images from the URL provided

### Step 2: Confirm JCR Package is created only with Images

**Before proceeding, confirm with the user only images are copied and not for example pdfs:**

"This skill only creates a package with images, proceed?"

### Step 3: Ask for the JCR package name

**Ask user for the JCR package name, give default as 'my-site-assets':**

"What would you like the package name to be? eg. my-site-assets"

### Step 4: Install package dependencies

**Command:**
```bash
npm install --prefix ./.skills/eaem-create-assets-package-from-page-skill/scripts
```

### Step 5: Run the copy images and package creation script

**Command:**
```bash
node .skills/eaem-create-assets-package-from-page-skill/scripts/create-jcr-package.js 'my-site-assets'
```

### Step 6: Verify package created as .zip

**Success criteria:**
- ✅ package created as zip with images


5) In folder eaem-create-assets-package-from-page-skill\resources\my-site-assets provide a template structure of the JCR Package



6) For the Package Creation Step of Skill Execution, add code in eaem-create-assets-package-from-page-skill\scripts\create-jcr-package.js. It copies the template structure to eds-project\import-work, makes necessary changes to .content.xml files, copies the images from eds-project\import-work\images (downloaded by the scrape-webpage skill execution) and finally zips it and places in eds-project\import-work\

import { mkdir, access, cp, rename, readdir, readFile, writeFile, rm, stat } from 'fs/promises';
import { createWriteStream } from 'fs';
import { randomUUID } from 'crypto';
import { constants } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import archiver from 'archiver';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

function getMimeType(filename) {
    const ext = path.extname(filename).toLowerCase();
    const mimeTypes = {
        '.jpg': 'image/jpeg',
        '.jpeg': 'image/jpeg',
        '.png': 'image/png',
        '.gif': 'image/gif',
        '.webp': 'image/webp',
        '.svg': 'image/svg+xml'
    };
    return mimeTypes[ext] || 'application/octet-stream';
}

async function updateOriginalMimeType(renditionsFolder, imageFile) {
    const contentXmlPath = path.join(renditionsFolder, 'original.dir', '.content.xml');
    const mimeType = getMimeType(imageFile);
    let xmlContent = await readFile(contentXmlPath, 'utf8');
    xmlContent = xmlContent.replace(/jcr:mimeType="[^"]*"/, `jcr:mimeType="${mimeType}"`);
    await writeFile(contentXmlPath, xmlContent, 'utf8');
}

async function updateAssetMetadata(assetFolderPath, imageFile, imageSourcePath) {
    const contentXmlPath = path.join(assetFolderPath, '.content.xml');
    const mimeType = getMimeType(imageFile);
    const fileExt = path.extname(imageFile).toLowerCase().substring(1); // Remove the dot
    const fileStat = await stat(imageSourcePath);
    const fileSize = fileStat.size;
    const lastModified = new Date().toISOString();
    const newUuid = randomUUID();

    let xmlContent = await readFile(contentXmlPath, 'utf-8');
   
    xmlContent = xmlContent.replace(/jcr:uuid="[^"]*"/, `jcr:uuid="${newUuid}"`);
    xmlContent = xmlContent.replace(/jcr:lastModified="{Date}[^"]*"/, `jcr:lastModified="{Date}${lastModified}"`);
    xmlContent = xmlContent.replace(/dam:Fileformat="[^"]*"/, `dam:Fileformat="${fileExt.toUpperCase()}"`);
    xmlContent = xmlContent.replace(/dam:MIMEtype="[^"]*"/, `dam:MIMEtype="${mimeType}"`);
    xmlContent = xmlContent.replace(/dam:size="{Long}[^"]*"/, `dam:size="{Long}${fileSize}"`);
    xmlContent = xmlContent.replace(/dc:format="[^"]*"/, `dc:format="${mimeType}"`);
   
    await writeFile(contentXmlPath, xmlContent, 'utf-8');
}

async function createPackageTempFolder(folderName) {
    const importWorkFolder = path.join(__dirname, '..', '..', '..', 'import-work');
    const packageFolder = path.join(importWorkFolder, folderName);
   
    try {
        await access(packageFolder, constants.F_OK);
    } catch (error) {
        await mkdir(packageFolder, { recursive: true });
    }
   
    return packageFolder;
}

async function copyTemplateFiles(destinationFolder) {
    const templateSource = path.join(__dirname, '..', 'resources', 'my-site-assets');
   
    await cp(templateSource, destinationFolder, {
        recursive: true,
        force: true
    });
   
    return destinationFolder;
}

async function renameDamFolder(packageFolderPath, folderName) {
    const oldPath = path.join(packageFolderPath, 'jcr_root', 'content', 'dam', 'my-site');
    const newPath = path.join(packageFolderPath, 'jcr_root', 'content', 'dam', folderName);
   
    await rename(oldPath, newPath);
   
    return packageFolderPath;
}

async function updateDamFolderTitle(packageFolderPath, folderName) {
    const contentXmlPath = path.join(packageFolderPath, 'jcr_root', 'content', 'dam', folderName, '.content.xml');
   
    let xmlContent = await readFile(contentXmlPath, 'utf-8');
   
    // Replace jcr:title="My Site" with jcr:title="{folderName}"
    xmlContent = xmlContent.replace(/jcr:title="My Site"/, `jcr:title="${folderName}"`);
   
    await writeFile(contentXmlPath, xmlContent, 'utf-8');
   
    return packageFolderPath;
}

async function updateManifest(packageFolderPath, folderName) {
    const manifestPath = path.join(packageFolderPath, 'META-INF', 'MANIFEST.MF');
   
    let manifestContent = await readFile(manifestPath, 'utf-8');
   
    // Update Content-Package-Id: my_packages:my-site-assets -> my_packages:{folderName}
    manifestContent = manifestContent.replace(
        /Content-Package-Id: my_packages:my-site-assets/,
        `Content-Package-Id: my_packages:${folderName}`
    );
   
    // Update Content-Package-Roots: /content/dam/my-site -> /content/dam/{folderName}
    manifestContent = manifestContent.replace(
        /Content-Package-Roots: \/content\/dam\/my-site/,
        `Content-Package-Roots: /content/dam/${folderName}`
    );
   
    await writeFile(manifestPath, manifestContent, 'utf-8');
   
    return packageFolderPath;
}

async function updateFilterXml(packageFolderPath, folderName) {
    const filterXmlPath = path.join(packageFolderPath, 'META-INF', 'vault', 'filter.xml');
   
    let filterContent = await readFile(filterXmlPath, 'utf-8');
   
    // Update filter root: /content/dam/my-site -> /content/dam/{folderName}
    filterContent = filterContent.replace(
        /root="\/content\/dam\/my-site"/,
        `root="/content/dam/${folderName}"`
    );
   
    await writeFile(filterXmlPath, filterContent, 'utf-8');
   
    return packageFolderPath;
}

async function updatePropertiesXml(packageFolderPath, folderName) {
    const propertiesXmlPath = path.join(packageFolderPath, 'META-INF', 'vault', 'properties.xml');
   
    let propertiesContent = await readFile(propertiesXmlPath, 'utf-8');
   
    // Update name entry: <entry key="name">my-site-assets</entry> -> <entry key="name">{folderName}</entry>
    propertiesContent = propertiesContent.replace(
        /<entry key="name">my-site-assets<\/entry>/,
        `<entry key="name">${folderName}</entry>`
    );
   
    await writeFile(propertiesXmlPath, propertiesContent, 'utf-8');
   
    return packageFolderPath;
}

async function updateDefinitionXml(packageFolderPath, folderName) {
    const definitionXmlPath = path.join(packageFolderPath, 'META-INF', 'vault', 'definition', '.content.xml');
    const lastModified = new Date().toISOString();
   
    let definitionContent = await readFile(definitionXmlPath, 'utf-8');
   
    // Update jcr:lastModified (line 6)
    definitionContent = definitionContent.replace(
        /jcr:lastModified="{Date}[^"]*"/,
        `jcr:lastModified="{Date}${lastModified}"`
    );
   
    // Update name (line 16)
    definitionContent = definitionContent.replace(
        /name="my-site-assets"/,
        `name="${folderName}"`
    );
   
    // Update root (line 23)
    definitionContent = definitionContent.replace(
        /root="\/content\/dam\/my-site"/,
        `root="/content/dam/${folderName}"`
    );
   
    await writeFile(definitionXmlPath, definitionContent, 'utf-8');
   
    return packageFolderPath;
}

async function createZipPackage(packageFolderPath, folderName) {
    const importWorkFolder = path.join(__dirname, '..', '..', '..', 'import-work');
    const zipFilePath = path.join(importWorkFolder, `${folderName}.zip`);
   
    return new Promise((resolve, reject) => {
        const output = createWriteStream(zipFilePath);
        const archive = archiver('zip', {
            zlib: { level: 9 } // Maximum compression
        });
       
        output.on('close', () => {
            console.log(`✅ Package created: ${zipFilePath}`);
            console.log(`📦 Total size: ${(archive.pointer() / 1024 / 1024).toFixed(2)} MB`);
            resolve(zipFilePath);
        });
       
        archive.on('error', (err) => {
            reject(err);
        });
       
        archive.pipe(output);
        archive.directory(packageFolderPath, false);
        archive.finalize();
    });
}

async function createAssetFoldersForImages(packageFolderPath, folderName) {
    const imagesSource = path.join(__dirname, '..', '..', '..', 'import-work', 'images');
    const damFolder = path.join(packageFolderPath, 'jcr_root', 'content', 'dam', folderName);
    const assetTemplatePath = path.join(damFolder, 'asset.jpg');
   
    const imageFiles = await readdir(imagesSource);
   
    // For each image, copy the asset.jpg folder and rename it
    for (const imageFile of imageFiles) {
        const newAssetFolderPath = path.join(damFolder, imageFile);
       
        // Copy asset.jpg folder to new image name folder
        await cp(assetTemplatePath, newAssetFolderPath, {
            recursive: true,
            force: true
        });
       
        // Copy the actual image binary to renditions folder as "original"
        const imageSourcePath = path.join(imagesSource, imageFile);
        const renditionsFolder = path.join(newAssetFolderPath, '_jcr_content', 'renditions');
        const imageDestPath = path.join(renditionsFolder, 'original');
       
        await cp(imageSourcePath, imageDestPath);
       
        // Update the jcr:mimeType in original.dir/.content.xml
        await updateOriginalMimeType(renditionsFolder, imageFile);
       
        // Update asset metadata in the main .content.xml
        await updateAssetMetadata(newAssetFolderPath, imageFile, imageSourcePath);
    }
   
    await rm(assetTemplatePath, { recursive: true, force: true });
   
    return packageFolderPath;
}

async function main() {
    const args = process.argv.slice(2);
   
    const folderName = args[0];
   
    if (!folderName) {
        console.error('Error: Please provide a folder name');
        process.exit(1);
    }
   
    try {
        const packageFolderPath = await createPackageTempFolder(folderName);
        await copyTemplateFiles(packageFolderPath);
        await renameDamFolder(packageFolderPath, folderName);
        await updateDamFolderTitle(packageFolderPath, folderName);
        await updateManifest(packageFolderPath, folderName);
        await updateFilterXml(packageFolderPath, folderName);
        await updatePropertiesXml(packageFolderPath, folderName);
        await updateDefinitionXml(packageFolderPath, folderName);
        await createAssetFoldersForImages(packageFolderPath, folderName);
        await createZipPackage(packageFolderPath, folderName);
       
    } catch (error) {
        console.error(`Failed to create package: ${error.message}`);
        process.exit(1);
    }
}

main();


7) Add the necessary dependencies for the above script create-jcr-package.js in eaem-create-assets-package-from-page-skill\scripts\package.json

{
  "type": "module",
  "name": "create-jcr-package",
  "version": "1.0.0",
  "description": "Create JCR package from scraped assets",
  "dependencies": {
    "archiver": "^7.0.1"
  }
}



No comments:

Post a Comment