Developer Guide to Document Authoring with Edge Delivery Services. Part 8

Author: Tom Cranstoun

Using AI with Edge Delivery Services

So, if you want to use AI with EDS, aka Helix or Franklin


You will end up with an AI agent that writes Edge Delivery Services code for you, generating code from text or screenshots.

I explain how to use AI effectively with Edge Delivery Services.

Previously

I opened up my useful components.

Part 8 - Using AI with Edge Delivery Services

I constructed this series just as AI became popular, especially for developers creating code. So, I was tempted. I tried writing with AI—this was an abysmal experience, so I had to hand-craft these pieces. You might detect the odd bit of wandering, repeats, and typos. I did have Grammarly on hand to help. I used Cursor.ai to create my examples; they were not generated raw by AI, but AI assisted my programming.

Using Claude without training

Let us see what happens if you jump into Claude and ask for programming code from Claude for Franklin.

Now, this is not intelligent. It has no clue what I meant by Franklin. It knows block and accordion and then hallucinates that we want ‘a react component’. No, we don't. We need plain modern JavaScript and CSS3.

Perhaps the word Franklin is not working; try again

Same answer.

Training Claude

Ok, so we need to train Claude. There are three good sources of training material,

  1. Adobe Experience Manager (www.aem.live)
  2. Developer Guide to Document Authoring with Edge Delivery Services - Part 0 (allabout.network)
  3. Your Repo, or perhaps you are starting from scratch GitHub - adobe/aem-boilerplate: Use this repository template for new AEM projects.

When I started writing Part 8 (this part), I realised I had not given enough context for an AI to understand what I was doing; therefore, I wrote Part 0 to fill in the gap. I also needed to provide more background for humans.

The aem.live site is good and covers the topic well, but it is not something one can feed to an AI.

So, I wrote a site scraper that extracts the text elements and links in a site by scraping and caching the web page; it extracts the text elements from a page (ignoring navigation), extracts all links (including navigation), and marks the ones that have been followed. As it goes along, It writes to a text file.

The source code for my scraper, which extracts relevant text pieces from an entire site

NodeJs src for a scraper that constructs a document to train AI.

const puppeteer = require('puppeteer');
const cheerio = require('cheerio');
const winston = require('winston');
const fs = require('fs').promises;
const path = require('path');
const { Command } = require('commander');
const { urlExclusions } = require('./config');

const program = new Command();

// Parse command-line arguments
program
  .requiredOption('-u, --url <url>', 'URL to scrape')
  .requiredOption('-o, --output <directory>', 'Output directory')
  .option('-l, --log-level <level>', 'Log level (error, warn, info, verbose, debug, silly)', 'info')
  .version('1.0.0')
  .addHelpText('after', `
Example:
  $ node scraper.js --url https://example.com --output ./output --log-level info`)
  .showHelpAfterError('(add --help for additional information)')
  .exitOverride((err) => {
    if (err.code === 'commander.missingMandatoryOptionValue') {
      console.error('Error: Missing required option');
      program.outputHelp();
      process.exit(1);
    }
    throw err;
  });

program.parse(process.argv);

const options = program.opts();

// Validate log level
const validLogLevels = ['error', 'warn', 'info', 'verbose', 'debug', 'silly'];
if (!validLogLevels.includes(options.logLevel)) {
  console.error(`Invalid log level: ${options.logLevel}`);
  console.error(`Valid log levels are: ${validLogLevels.join(', ')}`);
  process.exit(1);
}

// Create a logger
const logger = winston.createLogger({
  level: options.logLevel,
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.printf(({ level, message, timestamp }) => {
      return `${timestamp} ${level}: ${message}`;
    })
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: path.join(options.output, 'scraper.log') }),
  ],
});

let browser;

function cleanUrl(url) {
  try {
    const parsedUrl = new URL(url);
    parsedUrl.hash = '';
    const cleanedUrl = parsedUrl.toString();
    
    if (urlExclusions.some(exclusion => exclusion(cleanedUrl))) {
      return null;
    }
    
    return cleanedUrl;
  } catch (error) {
    logger.warn(`Invalid URL: ${url}`);
    return null;
  }
}

async function ensureDirectories() {
  try {
    await fs.rm(options.output, { recursive: true, force: true });
    logger.info(`Cleared output directory: ${options.output}`);

    await fs.mkdir(options.output, { recursive: true });
    logger.info(`Recreated output directory: ${options.output}`);

    const cacheDir = path.join('./cache');
    await fs.mkdir(cacheDir, { recursive: true });
    logger.info(`Ensured cache directory exists: ${cacheDir}`);
  } catch (error) {
    logger.error('Error managing directories:', { error: error.message });
    process.exit(1);
  }
}

async function scrapeWebsite(url, visitedUrls = new Set()) {
  const cleanedUrl = cleanUrl(url);
  if (!cleanedUrl) {
    logger.debug(`Skipping invalid, excluded, or community URL: ${url}`);
    return;
  }
  logger.info(`Starting to scrape: ${cleanedUrl}`);

  const cacheFile = path.join('./cache', `${Buffer.from(cleanedUrl).toString('base64')}.html`);
  const resultFile = path.join(options.output, 'result.txt');

  if (visitedUrls.has(cleanedUrl)) {
    logger.debug(`URL already visited, skipping: ${cleanedUrl}`);
    return;
  }

  visitedUrls.add(cleanedUrl);
  logger.debug(`Added to visited URLs: ${cleanedUrl}`);

  try {
    let content;
    let isCached = false;
    try {
      content = await fs.readFile(cacheFile, 'utf-8');
      logger.debug(`Using cached content for: ${cleanedUrl}`);
      isCached = true;
    } catch (error) {
      logger.debug(`Cache miss for: ${cleanedUrl}, fetching content`);
      browser = await puppeteer.launch();
      const page = await browser.newPage();
      await page.goto(cleanedUrl, { waitUntil: 'networkidle0' });
      if (!isCached) {
        logger.debug(`Waiting 3 seconds for: ${cleanedUrl}`);
        await new Promise(resolve => setTimeout(resolve, 3000));
      }
      content = await page.content();
      await browser.close();
      browser = null;
      await fs.writeFile(cacheFile, content);
      logger.debug(`Cached content for: ${cleanedUrl}`);
    }
    let $;
    try {
      $ = cheerio.load(content);
      logger.debug(`Loaded content into cheerio for: ${cleanedUrl}`);
    } catch (error) {
      logger.error(`Error loading content into cheerio for ${cleanedUrl}: ${error.message}`);
      logger.debug(`Error details: ${error.stack}`);
      throw error; // Re-throw to be caught by the outer try-catch
    }
    const rootElement = $('main').length > 0 ? 'main' : 'body';
    const relevantElements = $(rootElement).find('h1, h2, h3, h4, h5, h6, p, li')
      .not('header *, footer *, aside *, [class*="navigation"], [class*="pagination"], .navigation, .pagination, .pagination-wrapper *, .breadcrumb *, form *, .bloglist *, .bg-dark *');
    const mainElementStatus = rootElement === 'main' ? 'Found main element' : 'No main element found';
    logger.debug(`${mainElementStatus}, extracting content from ${rootElement}`);

    logger.debug(`Found ${relevantElements.length} relevant elements in: ${cleanedUrl}`);

    let structuredContent = `\n *** New Page *** \n${cleanedUrl}\n\n`;
    relevantElements.each((_, element) => {
      const $element = $(element);
        const text = $element.text().trim().replace(/^\s+/gm, '').replace(/\n\s+/g, '\n');
        if (text) {
          structuredContent += `${text}\n`;
        }
    });

    structuredContent = structuredContent + "\n *** End of Page *** \n";
    structuredContent = structuredContent.replace(/\n{3,}/g, '\n\n');
    logger.debug(`Prepared structured content for: ${cleanedUrl}`);

    await fs.appendFile(resultFile, structuredContent);
    logger.debug(`Appended content to result file for: ${cleanedUrl}`);

    const baseUrl = new URL(cleanedUrl);
    const internalLinks = new Set();

    $('a').each((_, element) => {
      const href = $(element).attr('href');
      if (href) {
        try {
          const linkUrl = new URL(href, baseUrl);
          const cleanedLink = cleanUrl(linkUrl.href);
          if (cleanedLink && linkUrl.hostname === baseUrl.hostname) {
            internalLinks.add(cleanedLink);
          }
        } catch (error) {
          logger.warn(`Invalid URL: ${href}`);
        }
      }
    });

    logger.debug(`Extracted ${internalLinks.size} internal links from: ${cleanedUrl}`);

    for (const link of internalLinks) {
      logger.debug(`Recursively scraping: ${link}`);
      await scrapeWebsite(link, visitedUrls);
    }

    logger.debug(`Completed scraping: ${cleanedUrl}`);
  } catch (error) {
    logger.error('Error scraping website:', { error: error.message, stack: error.stack, url: cleanedUrl });
    throw error;
    if (browser) {
      try {
        await browser.close();
      } catch (closeError) {
        logger.warn('Error closing browser:', { error: closeError.message });
      } finally {
        browser = null;
      }
    }
    // Add the URL to a list of failed URLs for potential retry
    failedUrls.add(cleanedUrl);
    // Implement exponential backoff for retrying
    if (retryCount < MAX_RETRIES) {
      const delay = Math.pow(2, retryCount) * 1000; // Exponential backoff
      logger.info(`Retrying ${cleanedUrl} in ${delay}ms (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
      await new Promise(resolve => setTimeout(resolve, delay));
      await scrapeWebsite(cleanedUrl, visitedUrls, retryCount + 1);
    } else {
      logger.warn(`Max retries reached for ${cleanedUrl}`);
    }
  }
}

async function gracefulShutdown(signal) {
  logger.info(`Received ${signal}. Starting graceful shutdown...`);

  if (browser) {
    logger.info('Closing browser...');
    await browser.close();
    browser = null;
  }

  logger.info('Graceful shutdown completed');
  process.exit(0);
}

async function main() {
  logger.debug('Starting main function');
  try {
    process.on('SIGINT', () => gracefulShutdown('SIGINT'));
    process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));

    logger.debug('Clearing output directory and ensuring directories exist');
    await ensureDirectories();

    const startUrl = cleanUrl(options.url);
    logger.debug(`Starting scrape with URL: ${startUrl}`);
    await scrapeWebsite(startUrl);

    logger.info('Scraping process completed');
    logger.debug('Main function completed');
  } catch (error) {
    logger.error('An error occurred during the scraping process:', error);
    process.exit(1);
  }
}

main();

And the config.js that informs exclusions

// URL exclusion patterns
const urlExclusions = [
  url => url.endsWith('.mp4'),
  url => url.endsWith('.pdf'),
  url => new URL(url).pathname.startsWith('/community'),
  url => new URL(url).pathname.includes('/chat'),
  url => new URL(url).pathname.includes('docs/admin.html'),
];

module.exports = {
  urlExclusions
};

I ran this code against aem.live and Part 0 of this tutorial; it diligently created files that contained the site's essence.

Adding the correct parts of your repo to the AI training

The final part of the training is the work already done, your repo.

The AI should be interested in the blocks you have added to your repo and the base style sheets.

I wrote another nodejs app that scans your folders and builds instructable text from CSS, JS, and MD. I believe in using MD files next to blocks to keep documentation within the implementation.

NodeJs src for a scanner that collects JS, CSS, and MD.

import { Command } from 'commander';
import fs from 'fs';
import path from 'path';
import logger from './logger.js';

const program = new Command();

program
  .option('-i, --input <directory>', 'input directory')
  .option('-o, --output <directory>', 'output directory', './output')
  .option('-f, --file <filename>', 'output file', 'result.txt')
  .option('-l, --log-level <level>', 'logging level', 'info')
  .parse(process.argv);

const options = program.opts();

logger.level = options.logLevel;

if (!options.input) {
  console.error('Error: Input directory is required.');
  program.help();
}

const processFiles = async (dir, outputDir, outputFile, isStylesFolder = false) => {
  const files = await fs.promises.readdir(dir, { withFileTypes: true });
  for (const file of files) {
    const fullPath = path.join(dir, file.name);
    if (file.isDirectory()) {
      await processFiles(fullPath, outputDir, outputFile, isStylesFolder);
    } else if (file.name.endsWith('.js') || file.name.endsWith('.css') || file.name.endsWith('.md')) {
      const content = await fs.promises.readFile(fullPath, 'utf-8');
      const blockName = path.basename(file.name, path.extname(file.name));
      let fileType;
      if (file.name.endsWith('.js')) {
        fileType = 'JS';
      } else if (file.name.endsWith('.css')) {
        fileType = 'CSS';
      } else {
        fileType = 'Markdown';
      }
      let message;
      if (isStylesFolder) {
        message = `This is a ${fileType} file that contains the overarching styles. Full path: ${fullPath}`;
      } else {
        message = `This is the ${fileType} file that generates a fraction of the block named ${blockName}. Full path: ${fullPath}`;
      }
      logger.info(message);
      await fs.promises.appendFile(path.join(outputDir, outputFile), `${message}\n${content}\n`);
    }
  }
};

const main = async () => {
  try {
    await fs.promises.mkdir(options.output, { recursive: true });

    let inputDir = options.input;
    if (inputDir.endsWith('blocks')) {
      inputDir = path.dirname(inputDir);
      const blocksDir = path.join(inputDir, 'blocks');
      const stylesDir = path.join(inputDir, 'styles');

      // Process blocks
      await processFiles(blocksDir, options.output, options.file, false);

      // Process styles
      await processFiles(stylesDir, options.output, options.file, true);
    } else {
      await processFiles(inputDir, options.output, options.file);
    }
  } catch (error) {
    logger.error(error.message);
  }
};

main();

I ran this

node index.js -i ~/Documents/GitHub/{myrepname} -o ./output -f result.txt -l debug

Creating the final training file

I have five more manually created files, which provide context for the machine-generated files.

developer-front.txt

This part of the instructions is intended to train an AI in the coding style for an Adobe product. The product uses CSS3,  modern javascript and DOM manipulation instead of libraries.

Instructions

Edge Delivery Services (EDS) Overview:
EDS is Adobe's serverless platform for fast, scalable web content delivery. Also known as Franklin or Helix. It allows content creators to use familiar tools like Google Docs/Microsoft Word for authoring. The system converts documents to HTML/JSON and serves them via CDN. EDS supports blocks/components for layout and functionality, enabling rapid development and publishing.

Key Components:
1. Document-based authoring
2. Auto-blocking and metadata handling
3. JSON generation from spreadsheets
4. Redirects and configuration options

Development Process:
1. Content Creation:
   - Use Google Docs or Microsoft Word
   - Add metadata tables for page properties
   - Define blocks using tables in the document

2. Preview and Publish:
   - Use Sidekick tool in document
   - Preview generates temporary URL
   - Publish pushes content to production

3. Block Development:
   - Create folder in /blocks with block-name.css and block-name.js files, inside a folder called block-name.  e.g. /blocks/example/example.js and /blocks/example/example.css and /blocks/example/README.md
   - Use vanilla JavaScript and DOM manipulation
   - Access block content via classes and data attributes, never ID

4. Spreadsheet Usage:
   - Create spreadsheets for dynamic content or configuration
   - System automatically converts to JSON
   - Access via /path/to/spreadsheet.json

5. Advanced Development:
   - Use query-index.json for dynamic content across site
   - Configure headers, redirects, authentication in .helix folder

Block Development Details:
1. CSS:
   - Use CSS3 features including variables
   - Create variations with additional classes

2. JavaScript:
   - Export a default function that decorates the block
   - Use modern browser APIs (fetch, Intersection Observer, etc.)
   - Avoid external libraries to maintain performance

3. HTML Structure:
   - Blocks wrapped in <div class="blockname-wrapper">
   - Inner content in <div class="blockname block">

Best Practices:
1. Performance:
   - Leverage EDS features for optimal page speed
   - Minimize external dependencies
   - Use lazy loading for images and heavy content

2. Modularity:
   - Keep blocks self-contained and reusable
   - Use block variations for different use cases
   - Separate concerns (HTML structure, styling, behavior)

3. Content Author Experience:
   - Make block usage intuitive in documents
   - Provide clear documentation for custom blocks
   - Use sensible defaults with options for customization

4. SEO and Accessibility:
   - Ensure proper heading structure
   - Use semantic HTML elements
   - Provide alt text for images

5. Responsive Design:
   - Design mobile-first, then enhance for larger screens
   - Use CSS Grid and Flexbox for layouts
   - Implement responsive images with picture element

Configuration and Advanced Features:
1. .helix folder:
   - Store configuration files (headers, redirects, etc.)
   - Not publicly accessible

2. Spreadsheets and JSON:
   - query-index for site-wide content index
   - Use for dynamic content, site configuration
   - GitHub fallback for spreadsheets

3. Authentication:
   - Configure in .helix/config spreadsheet

4. Redirects:
   - Manage in redirects spreadsheet or GitHub fallback

5. Custom HTTP Headers:
   - Define in headers spreadsheet or GitHub fallback

system-block-ai-front.txt

# Guide to Creating Blocks in Edge Delivery Services (EDS) or Franklin, Helix

Blocks are modular components used in development to create reusable and customizable webpage sections.

## Block Structure
- Each block typically consists of two main files and an optional third:
  1. A CSS file (e.g., `blockname.css`) for styling
  2. A JavaScript file (e.g., `blockname.js`) for functionality
  3. a README.md for documentation
If asked to create a block, one should always make a README.md alongside it.

## Key Components of a Block

### CSS File
- Contains styles specific to the block
- Often includes responsive design for different screen sizes
- May use CSS variables for theming

### JavaScript File
- Exports a `decorate` function as the default
- The `decorate` function typically:
  - Manipulates the DOM to create the block's structure
  - Adds event listeners for interactivity
  - Fetches data if needed (e.g., from JSON files)
  - Applies dynamic styling
  
### Markdown File
- Contains documentation of use to the developer, maintainer or content author

### Common Patterns
- Use of `document.createElement` to create elements
- Applying classes for styling
- Creating nested structures (e.g., for carousels or grids)
- Fetching and processing data (often using `fetch` API)
- Implementing responsive behavior

## Best Practices
1. Keep blocks modular and reusable
2. Use semantic HTML where possible
3. Implement accessibility features (ARIA attributes, keyboard navigation)
4. Optimize for performance (lazy loading, efficient DOM manipulation)
5. Follow a consistent naming convention for classes and IDs

## Advanced Features
- Some blocks may use external libraries or APIs
- Complex blocks may implement lazy loading or infinite scrolling
- Interactive blocks often use event delegation for efficiency

## Integration
- Blocks are typically integrated into a larger project structure
- They may interact with a content management system or data layer
- Often used in conjunction with a main JavaScript file that handles overall page behavior

This guide provides a high-level overview of creating blocks in web development. Each block type has its specific implementation details and requirements.

What follows are sample blocks from my GitHub repository.

system-block-ai-foot.txt

##### This is the end of the examples

When asked to create anything, if the result is a block, remember to create a README.md

All js, cs and md files should obey the air-bnb linting style guide

use await rather than .then() for asynch code

always list every line and function in code

lib-franklin.js has been renamed aem.js; remember to check this in the generated code and replace it if necessary

Finally, my Readme.txt places them all in the correct order; I have named the result file all.out to remind me it is not just a plain text file. It contains magic.

Cat.sh and ReadMe.txt

cat aem-live-result.txt system-developer-front.txt developer-guide.txt system-block-ai-front.txt block-ai-result.txt system-block-ai-foot.txt >all.out

This Unix/Linux shell command combines the contents of multiple text files into a single output file. Let's break it down:

1. `cat`: This command concatenates (combines) and displays the contents of files.

2. The files being combined are:

- `aem-live-result.txt`

- `system-developer-front.txt`

- `developer-guide.txt`

- `system-block-ai-front.txt`

- `block-ai-result.txt`

- `system-block-ai-foot.txt`

3. `>`: This is an output redirection operator. It takes the output that would normally be displayed on the screen and instead writes it to a file.

4. `all.out` is the name of the output file to which the combined contents of all the input files will be written.

So, when you run this command, it will:

1. Read the contents of the files in the correct order.

2. Concatenating all of their contents.

3. Write the combined content to a new file named `all.out`.

If `all.out` exists, its contents will be overwritten. If it doesn't exist, it will be created.

This takes all of the files Tom Cranstoun created and turns them into an `all.out` file for Claude or other AI.

When added as context, this turns Claude into an agent that can create blocks for Adobe Edge Delivery Services (AKA Franklin or Helix)

If you want your repo (`block-ai.txt`) or training, you can replace any files except 'system-XXXX--xxx.txt' and rerun.


The file all.out is uploaded to Claude, and the question is given again.

Claude does the trick, including a README.md for the component.

To create an accordion block for Franklin (Adobe Experience Manager Edge Delivery Services), you'll need to create three files:

accordion.js
accordion.css
README.md (optional, but recommended for documentation)

Here's how you can implement an accordion block:

accordion.js:

javascriptCopyexport default function decorate(block) {
  const accordionItems = block.querySelectorAll(':scope > div');
  
  accordionItems.forEach((item, index) => {
    const [header, content] = item.children;
    
    // Create accordion item structure
    const accordionItem = document.createElement('div');
    accordionItem.className = 'accordion-item';
    
    const accordionHeader = document.createElement('button');
    accordionHeader.className = 'accordion-header';
    accordionHeader.textContent = header.textContent;
    accordionHeader.setAttribute('aria-expanded', 'false');
    accordionHeader.setAttribute('aria-controls', `accordion-content-${index}`);
    
    const accordionContent = document.createElement('div');
    accordionContent.className = 'accordion-content';
    accordionContent.id = `accordion-content-${index}`;
    accordionContent.hidden = true;
    accordionContent.innerHTML = content.innerHTML;
    
    // Add click event listener
    accordionHeader.addEventListener('click', () => {
      const isExpanded = accordionHeader.getAttribute('aria-expanded') === 'true';
      accordionHeader.setAttribute('aria-expanded', !isExpanded);
      accordionContent.hidden = isExpanded;
    });
    
    // Append elements
    accordionItem.appendChild(accordionHeader);
    accordionItem.appendChild(accordionContent);
    block.appendChild(accordionItem);
  });
  
  // Remove original content
  block.querySelectorAll(':scope > div').forEach(div => div.remove());
}

accordion.css:

cssCopy.accordion-item {
  border: 1px solid #ddd;
  margin-bottom: 10px;
}

.accordion-header {
  width: 100%;
  padding: 15px;
  background-color: #f4f4f4;
  border: none;
  text-align: left;
  font-weight: bold;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.accordion-header:hover {
  background-color: #e4e4e4;
}

.accordion-header[aria-expanded="true"] {
  background-color: #e4e4e4;
}

.accordion-content {
  padding: 15px;
}

.accordion-content[hidden] {
  display: none;
}

README.md:

markdownCopy# Accordion Block

This block creates an accessible, interactive accordion component.

## Usage

To use the accordion block in your content:

1. Create a new table in your document.
2. In the first cell of the first row, type "Accordion".
3. For each accordion item:
   - Create a new row
   - In the first column, add the accordion header text
   - In the second column, add the accordion content

Example:

| Accordion |
|-----------|
| Section 1 | This is the content for section 1. |
| Section 2 | This is the content for section 2. |
| Section 3 | This is the content for section 3. |

## Styling

You can customize the appearance of the accordion by modifying the CSS in `accordion.css`.

## Accessibility

This accordion implementation follows accessibility best practices:
- Uses appropriate ARIA attributes
- Can be operated with keyboard navigation
To use this accordion block in your Franklin project:

Create a new folder named accordion in your blocks directory.
Add the accordion.js and accordion.css files to this folder.
Optionally, add the README.md file for documentation.

Now, content authors can create accordions in their documents by using a table with "Accordion" in the first cell, followed by rows where the first column is the header and the second column is the content for each accordion item.
This implementation creates an accessible, interactive accordion that can be easily styled and customized as needed.

You do not need to create your training files; you can use mine.

Here are the links to the Training texts.

Training Texts

You can cherry-pick, download the whole lot, or use the file all.out, which contains the magic.

With this training, you can also upload images and screenshots into Claude; Claude will then generate complex blocks for you.

I took this screenshot from my home screen on Edge (The browser, gosh, don’t you hate the way this industry names things)


I pasted into my trained Claude,

The first pass was not complete; it said things like

​​function createFeaturedStories(stories) {

// Implementation for featured stories carousel

}

function createTopStories(stories) {

// Implementation for top stories list

}

function createNewsGrid(items) {

// Implementation for news items grid

}

I said, show me the code, and voila.

Note that I have trained Claude to use airbnb style guide, which is Adobe’s Linting in the GitHub pipeline; this code will pass the linting test.

New task

Create a rotating cube display with images as a hero for franklin

To create a rotating cube display with images as a hero for Franklin (Adobe Experience Manager), you'll need to create a custom block. Here's how you can implement this:

First, create a new block folder called "rotating-cube-hero" in your project's blocks directory.
Create two files in this folder: rotating-cube-hero.css and rotating-cube-hero.js
Here's the CSS for the rotating cube (rotating-cube-hero.css):
css

.rotating-cube-hero {
  width: 100%;
  height: 400px;
  perspective: 1000px;
  overflow: hidden;
}

.cube {
  width: 100%;
  height: 100%;
  position: relative;
  transform-style: preserve-3d;
  animation: rotate 20s infinite linear;
}

.cube-face {
  position: absolute;
  width: 100%;
  height: 100%;
  background-size: cover;
  background-position: center;
  opacity: 0.8;
}

.cube-face:nth-child(1) { transform: rotateY(0deg) translateZ(200px); }
.cube-face:nth-child(2) { transform: rotateY(90deg) translateZ(200px); }
.cube-face:nth-child(3) { transform: rotateY(180deg) translateZ(200px); }
.cube-face:nth-child(4) { transform: rotateY(-90deg) translateZ(200px); }

@keyframes rotate {
  0% { transform: rotateY(0); }
  100% { transform: rotateY(360deg); }
}
Now, here's the JavaScript for the rotating cube (rotating-cube-hero.js):

export default function decorate(block) {
  const cube = document.createElement('div');
  cube.className = 'cube';

  // Get all images from the block
  const images = block.querySelectorAll('img');

  // Create cube faces
  images.forEach((img, index) => {
    if (index < 4) { // We only need 4 faces for our cube
      const face = document.createElement('div');
      face.className = 'cube-face';
      face.style.backgroundImage = `url(${img.src})`;
      cube.appendChild(face);
    }
  });

  // Clear the block and add the cube
  block.innerHTML = '';
  block.appendChild(cube);
}
To use this block in your Franklin project, you would add a table to your document with the following structure:

Copy
| rotating-cube-hero |
|---------------------|
| [Image 1]           |
| [Image 2]           |
| [Image 3]           |
| [Image 4]           |
Replace [Image 1], [Image 2], etc. with actual images in your document.

This will create a rotating cube hero with four faces, each displaying one of the images you've provided.
Note: This implementation assumes that the block will be full-width. If you need it to be contained within a certain width, you may need to adjust the CSS accordingly.

Also, keep in mind that 3D transforms can be computationally expensive, especially on mobile devices. You might want to consider adding a media query to disable the rotation on smaller screens for better performance.

Remember to test thoroughly across different devices and browsers to ensure compatibility and performance.

You now have an agent that can create Edge Delivery Services Blocks with a prompt and maybe a picture; this is a very handy tool. Remember that it is AI-generated, so mistakes can happen; check the code.

A reminder

You do not need to create your training files; you can use mine.

Here are the links to the Training texts.

Training Texts

You can cherry-pick, download the whole lot, or use the file all.out, which contains the magic.

/fragments/ddt/proposition

Related Articles

guide
Back to Top