Developer Guide to Document Authoring with Edge Delivery Services.
Appendix

Adobe Edge Delivery Services (EDS) Development Reference Guide
Introduction
To ensure effective and efficient development within the EDS environment, it is essential to adhere to established best practices. These practices encompass a wide range of considerations, including document structuring, content optimization, metadata utilization, and adherence to platform-specific requirements.
Document Structuring: Clear and logical organization of content is paramount. Utilize headings, subheadings, and consistent formatting to enhance readability and navigation. Consider the target audience and their information needs when structuring documents.
Content Optimization: Craft concise and informative content that is tailored to the context of EDS delivery. Employ plain language and avoid jargon whenever possible. Incorporate multimedia elements strategically to enhance engagement and understanding.
Metadata Utilization: Leverage metadata effectively to improve content discoverability and searchability. Assign relevant keywords, descriptions, and tags to facilitate efficient information retrieval.
Platform-Specific Requirements: Adhere to any guidelines or specifications dictated by the EDS platform. This may include restrictions on file formats, image sizes, or other technical considerations.
Accessibility: Ensure that content is accessible to users of all abilities. Follow accessibility guidelines and provide alternative formats when necessary.
Version Control: Maintain version control of documents to track changes and facilitate collaboration among team members.
Testing and Validation: Thoroughly test and validate content prior to publication to ensure accuracy and functionality within the EDS environment.
User Feedback: Actively seek and incorporate user feedback to continuously improve the quality and relevance of EDS content.
By following these best practices, developers can create high-quality, user-centric content that maximizes the potential of the EDS platform.
Configuration and Structure
Configuration Constants Pattern
When developing blocks for EDS, it's recommended to use configuration constants at the top of your JavaScript files. This pattern improves code readability, maintainability, and prevents "magic numbers" or strings from being scattered throughout your code:
const BLOCK_CONFIG = {
// Visual appearance
ANIMATION_DURATION: 300,
COPY_BUTTON_RESET_DELAY: 2000,
// Content thresholds
MAX_ITEMS: 12,
LONG_DOCUMENT_THRESHOLD: 40,
SUMMARY_LENGTH: 150,
// Content labels
ERROR_MESSAGE: 'Error loading content. Please try again.',
LOADING_MESSAGE: 'Loading content...',
COPY_TEXT: 'Copy',
COPIED_TEXT: 'Copied!',
EXPAND_TEXT: 'Expand',
COLLAPSE_TEXT: 'Collapse',
// API endpoints
API_ENDPOINT: '/query-index.json',
CONTENT_ENDPOINT: '/content/pages.json',
// Feature toggles
ENABLE_TRACKING: true,
SHOW_TIMESTAMPS: true
};
Using this pattern makes it easier for other developers to find and modify configuration values without digging through implementation details. It also centralizes configuration, making it clear what aspects of the component are customizable.
Standard File Organization
EDS projects follow a consistent file organization pattern. Each block or component has its own directory with standardized files:
/blocks/{blockname}/
├── {blockname}.js # Core block functionality
├── {blockname}.css # Block styles
├── README.md # Documentation
├── example.md # Usage examples for content authors
├── demo.md # More comprehensive usage examples
├── example.json # Sample data (if needed)
└── example.csv # CSV version of sample data
This structure ensures that all blocks have complete documentation, clear examples for authors, and consistent organization that makes it easy for developers to understand how the block works.
JavaScript Patterns
ESLint Integration
EDS follows the Airbnb JavaScript Style Guide, which discourages console output in production code. When you need to use console logging for debugging, use the following pattern:
// eslint-disable-next-line no-console
console.log('Debug information:', data);
This approach prevents ESLint errors while maintaining the ability to use console logging for debugging. Remember to:
- Keep the comment on the line immediately before the console statement
- Be specific about which rule you're disabling
- Remove or comment out debug logging before committing to production
Asynchronous Data Fetching
When fetching data from APIs or the EDS query index, follow this pattern for clean, error-handled asynchronous requests:
export default async function decorate(block) {
try {
// Show loading state
block.innerHTML = '<div class="loading">Loading content...</div>';
// Fetch data with proper error handling
const response = await fetch('/query-index.json');
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}
const data = await response.json();
// Process successful response
block.innerHTML = ''; // Clear loading state
// Create and append content elements
const container = document.createElement('div');
container.className = 'content-container';
// Add content to container
// ...
block.appendChild(container);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error fetching content:', error);
// Show user-friendly error state
block.innerHTML = `
<div class="error-state">
<p>We couldn't load this content. Please try again later.</p>
</div>
`;
}
}
This pattern includes:
- Clear loading states to improve user experience
- Proper error handling with try/catch blocks
- Response validation with specific error messages
- User-friendly error states when things go wrong
- Clean DOM manipulation to update the UI
Standard Data Structure
When integrating with external data sources or the EDS query-index.json, follow this consistent pattern for JSON data structures:
{
"total": 100, // Total items available
"offset": 0, // Starting position for pagination
"limit": 10, // Maximum items returned in this response
"data": [ // Array of actual content items
{
"path": "/example-path",
"title": "Example Title",
"image": "/path/to/image.jpg",
"description": "Example description",
"lastModified": "1724942455",
"tags": ["tag1", "tag2"],
"author": "Author Name"
}
],
"type": "sheet" // Content type identifier
}
This structure works well with EDS's built-in query functionality and makes it easier to create consistent data handling patterns across your site.
Event Handling
For event handling, use this pattern to maintain clean separation and avoid memory leaks:
export default function decorate(block) {
// Create elements
const button = document.createElement('button');
button.textContent = 'Click me';
button.className = 'action-button';
// Define handler functions separately for clarity
function handleClick(event) {
// Handle the click event
console.log('Button clicked', event);
// Example: Toggle state
button.classList.toggle('active');
}
// Add event listeners
button.addEventListener('click', handleClick);
// Append elements to the block
block.appendChild(button);
// Optional: Return a cleanup function for SPA environments
return () => {
button.removeEventListener('click', handleClick);
};
}
Key benefits of this pattern:
- Handler functions are defined separately for better readability
- Event listeners are clearly attached in one section
- Optional cleanup function helps prevent memory leaks in single-page applications
CSS Best Practices
CSS Variables for Theming
Define configuration through CSS variables to create consistent, flexible theming:
:root {
/* Color palette */
--color-primary: #1473e6;
--color-secondary: #2680eb;
--color-background: #ffffff;
--color-text: #2c2c2c;
--color-text-light: #707070;
--color-border: #e1e1e1;
--color-error: #d7373f;
--color-success: #268e6c;
/* Typography */
--font-family-heading: 'Adobe Clean', sans-serif;
--font-family-body: 'Adobe Clean', sans-serif;
--font-weight-normal: 400;
--font-weight-bold: 700;
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--line-height-loose: 1.8;
/* Spacing system */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-xxl: 48px;
/* UI elements */
--border-radius-small: 4px;
--border-radius-medium: 8px;
--border-radius-large: 16px;
--shadow-small: 0 1px 3px rgba(0,0,0,0.12);
--shadow-medium: 0 4px 6px rgba(0,0,0,0.12);
--shadow-large: 0 10px 20px rgba(0,0,0,0.12);
/* Transitions */
--transition-fast: 0.15s ease;
--transition-normal: 0.3s ease;
--transition-slow: 0.5s ease;
/* Layout */
--container-width-small: 600px;
--container-width-medium: 900px;
--container-width-large: 1200px;
--content-width: 72ch;
}
/* Using variables in components */
.button {
background-color: var(--color-primary);
color: white;
padding: var(--spacing-s) var(--spacing-m);
border-radius: var(--border-radius-small);
font-family: var(--font-family-body);
font-weight: var(--font-weight-bold);
transition: background-color var(--transition-fast);
}
.button:hover {
background-color: var(--color-secondary);
}
.card {
background-color: var(--color-background);
border-radius: var(--border-radius-medium);
padding: var(--spacing-m);
box-shadow: var(--shadow-medium);
max-width: var(--container-width-small);
}
This comprehensive approach to CSS variables provides several benefits:
- Consistent visual design across the site
- Easy updates to the design system by changing variable values
- Better organization of design tokens
- Simplified maintenance and theme switching
Standard Breakpoints
EDS follows a common set of breakpoints for responsive design:
/* Mobile (default) - 0-599px */
.component {
/* Mobile styles */
padding: var(--spacing-m);
font-size: 16px;
}
/* Tablet - 600-899px */
@media (min-width: 600px) {
.component {
/* Tablet styles */
padding: var(--spacing-l);
font-size: 18px;
}
}
/* Desktop - 900-1199px */
@media (min-width: 900px) {
.component {
/* Desktop styles */
padding: var(--spacing-xl);
font-size: 20px;
}
}
/* Large Desktop - 1200px+ */
@media (min-width: 1200px) {
.component {
/* Large desktop styles */
padding: var(--spacing-xxl);
max-width: var(--container-width-large);
margin: 0 auto;
}
}
Key principles for responsive design in EDS:
- Always use
min-width
queries for consistency - Design mobile-first, adding complexity for larger screens
- Use standard breakpoints (600px, 900px, 1200px) for consistency
- Test all components at each breakpoint
Block Variation Pattern
For blocks that support variations, follow this pattern:
/* Base block styling */
.blockname {
/* Default styles */
padding: var(--spacing-m);
background: var(--color-background);
color: var(--color-text);
}
/* Size variations */
.blockname.small {
/* Small variation */
padding: var(--spacing-s);
font-size: 0.9em;
}
.blockname.large {
/* Large variation */
padding: var(--spacing-xl);
font-size: 1.2em;
}
/* Color theme variations */
.blockname.dark {
/* Dark theme */
background: #333333;
color: #ffffff;
}
.blockname.light {
/* Light theme */
background: #f5f5f5;
color: #333333;
}
/* Layout variations */
.blockname.centered {
/* Centered layout */
text-align: center;
margin-left: auto;
margin-right: auto;
}
.blockname.split {
/* Split layout */
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-m);
}
/* Combined variations */
.blockname.dark.centered {
/* Special styles for dark+centered combination */
border: 1px solid rgba(255, 255, 255, 0.1);
}
This approach allows authors to specify variations in the document (e.g., Blockname (dark, centered)
) without requiring additional JavaScript logic. Content authors can easily mix and match variations to achieve their desired appearance.
Documentation Standards
Block Documentation Template
When documenting a block, include the following sections in your README.md:
# Block Name
## Overview
Brief description of what the block does and its primary use cases.
## Content Structure
Explain how authors should structure content in Google Docs to use this block.
### Example Table Structure
| BlockName | |
| --------- | --------- |
| Content A | Content B |
| More here | More here |
## Variations
List all supported variations with examples:
- **dark**: Applies a dark color scheme
- **wide**: Expands the block to use more horizontal space
- **centered**: Centers the content within the block
### Variation Examples
| BlockName (dark) | |
| ---------------- | ------- |
| Content | Content |
## Configuration Options
Any custom configuration options available for developers.
## Accessibility Considerations
Notes on accessibility features and considerations.
## Performance Impact
Any notable performance considerations or optimizations.
## Dependencies
List any dependencies or requirements.
## Known Limitations
Document any known issues or limitations.
This comprehensive documentation structure ensures that both developers and content authors can effectively use your blocks. Always include:
- Clear examples that content authors can copy and paste
- Visual examples of variations when possible
- Accessibility information to ensure inclusive design
- Performance considerations to maintain site speed
Code Comments
For complex JavaScript functions, use JSDoc-style comments:
/**
* Processes a collection of content items and generates HTML elements
*
* This function takes raw data from the query index, filters based on
* the provided criteria, and creates DOM elements for each item.
*
* @param {Array} items - Collection of content items from query index
* @param {Object} options - Configuration options
* @param {number} options.limit - Maximum number of items to display
* @param {string} options.layout - Layout style ('grid', 'list', etc.)
* @param {boolean} options.showDate - Whether to display dates
* @returns {DocumentFragment} Fragment containing generated elements
*/
function processItems(items, options = {}) {
// Implementation details...
}
For CSS, include section comments to organize your stylesheet:
/* ---------------------------------------------------------------------
* CARD COMPONENT
*
* Core styles for card components including base layout, variations,
* and responsive behaviors.
* --------------------------------------------------------------------*/
.card {
/* Card base styles */
}
/* Card media section */
.card-media {
/* Media styles */
}
/* Card content section */
.card-content {
/* Content styles */
}
/* Card variations */
.card.featured {
/* Featured card styles */
}
Resource Loading Patterns
CSS Loading
/**
* Loads a CSS file asynchronously
* @param {string} href - Path to the CSS file
* @return {HTMLLinkElement} - The created link element
*/
function loadCSS(href) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
document.head.appendChild(link);
return link;
}
// Usage
loadCSS('/blocks/myblock/myblock.css');
Lazy JavaScript Loading
/**
* Loads a JavaScript file asynchronously
* @param {string} src - Path to the JavaScript file
* @return {Promise} - Resolves when the script is loaded
*/
async function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Usage with async/await
try {
await loadScript('/scripts/feature.js');
// Script is now loaded and can be used
initFeature();
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to load script:', error);
showErrorState();
}
Delayed Functionality
/**
* Delays execution of non-critical functionality
* @param {Function} func - Function to execute after delay
* @param {number} delay - Delay in milliseconds (default: 3000)
*/
function delayFunction(func, delay = 3000) {
setTimeout(func, delay);
}
// Usage
delayFunction(() => {
// Initialize non-critical features
loadChatWidget();
setupAnalytics();
initFeedbackForm();
});
Common Implementation Patterns
Analytics Implementation
A clean pattern for implementing analytics without modifying core files:
// In delayed.js
(function loadAnalytics() {
// Skip in development environments
if (window.location.hostname === 'localhost') {
return;
}
// Create script element with async loading
const script = document.createElement('script');
script.async = true;
script.src = 'https://www.googletagmanager.com/gtag/js?id=UA-XXXXXXXX-X';
document.head.appendChild(script);
// Initialize analytics
window.dataLayer = window.dataLayer || [];
function gtag() { window.dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'UA-XXXXXXXX-X', {
'anonymize_ip': true,
'page_title': document.title,
'page_path': window.location.pathname
});
// Add custom event tracking with event delegation
document.addEventListener('click', e => {
const target = e.target.closest('a, button');
if (!target) return;
// Gather tracking data
const trackingData = {
event_category: target.tagName.toLowerCase(),
event_label: target.innerText || target.textContent,
};
// Check for outbound links
if (target.href) {
const url = new URL(target.href);
trackingData.outbound = url.hostname !== window.location.hostname;
// Track downloads
const fileExtension = url.pathname.split('.').pop();
if (['pdf', 'docx', 'xlsx', 'zip'].includes(fileExtension)) {
trackingData.event_category = 'download';
trackingData.file_extension = fileExtension;
trackingData.file_name = url.pathname.split('/').pop();
}
}
// Send event to analytics
gtag('event', 'click', trackingData);
});
})();
Cookie Consent Block
A reusable block for implementing cookie consent:
export default function decorate(block) {
// Configuration - can be customized via block configuration
const config = {
cookieName: 'cookie-consent',
cookieExpiry: 365, // days
showOnce: true,
position: 'bottom', // 'bottom', 'top', 'modal'
text: 'This website uses cookies to ensure you get the best experience.',
learnMoreText: 'Learn more',
learnMoreUrl: '/privacy-policy',
acceptText: 'Accept',
declineText: 'Decline'
};
// Only show if consent not yet given
if (getCookie(config.cookieName)) {
return;
}
// Create consent banner
const banner = document.createElement('div');
banner.className = `cookie-consent-banner ${config.position}`;
banner.innerHTML = `
<div class="cookie-content">
<p>${config.text}
<a href="${config.learnMoreUrl}">${config.learnMoreText}</a></p>
<div class="cookie-buttons">
<button class="accept-button">${config.acceptText}</button>
<button class="decline-button">${config.declineText}</button>
</div>
</div>
`;
// Add event listeners
banner.querySelector('.accept-button').addEventListener('click', () => {
setCookie(config.cookieName, 'accepted', config.cookieExpiry);
banner.remove();
enableTracking();
});
banner.querySelector('.decline-button').addEventListener('click', () => {
setCookie(config.cookieName, 'declined', config.cookieExpiry);
banner.remove();
});
// Add to page
document.body.appendChild(banner);
// Helper functions
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = `; expires=${date.toUTCString()}`;
document.cookie = `${name}=${value}${expires}; path=/; SameSite=Lax`;
}
function getCookie(name) {
const nameEQ = `${name}=`;
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function enableTracking() {
// Load analytics and other tracking scripts
delayFunction(() => {
const script = document.createElement('script');
script.src = '/scripts/tracking.js';
document.head.appendChild(script);
});
}
}
Personalization Block
A reusable block for implementing simple personalization:
export default function decorate(block) {
// Get or create user profile
let userProfile = JSON.parse(localStorage.getItem('user-profile')) || {};
// Track page visits
userProfile.pageVisits = userProfile.pageVisits || [];
userProfile.pageVisits.push({
path: window.location.pathname,
timestamp: Date.now(),
});
// Limit history length
if (userProfile.pageVisits.length > 20) {
userProfile.pageVisits = userProfile.pageVisits.slice(-20);
}
// Determine interests based on page visits
const interests = determineInterests(userProfile.pageVisits);
userProfile.interests = interests;
// Save updated profile
localStorage.setItem('user-profile', JSON.stringify(userProfile));
// Personalize content
personalizeContent(block, userProfile);
/**
* Analyzes user behavior to determine interests
* @param {Array} pageVisits - Collection of page visit records
* @return {Object} - Map of interest categories and strength
*/
function determineInterests(pageVisits) {
const interests = {};
const categories = {
products: ['/products/', '/shop/', '/store/'],
services: ['/services/', '/solutions/'],
blog: ['/blog/', '/articles/', '/news/'],
support: ['/support/', '/help/', '/faq/']
};
// Count visits in each category
pageVisits.forEach(visit => {
Object.entries(categories).forEach(([category, paths]) => {
if (paths.some(path => visit.path.includes(path))) {
interests[category] = (interests[category] || 0) + 1;
}
});
});
// Convert counts to relative strength (0-1)
const total = pageVisits.length || 1;
Object.keys(interests).forEach(category => {
interests[category] = Math.min(interests[category] / total, 1);
});
return interests;
}
/**
* Creates personalized content based on user profile
* @param {HTMLElement} container - Element to populate
* @param {Object} profile - User profile data
*/
function personalizeContent(container, profile) {
// Clear container
container.textContent = '';
// Create personalized content heading
const heading = document.createElement('h2');
// Determine strongest interest
let strongestInterest = '';
let maxStrength = 0;
Object.entries(profile.interests || {}).forEach(([interest, strength]) => {
if (strength > maxStrength) {
maxStrength = strength;
strongestInterest = interest;
}
});
// Set heading based on interests
if (strongestInterest === 'products') {
heading.textContent = 'Products You Might Like';
} else if (strongestInterest === 'services') {
heading.textContent = 'Services For Your Needs';
} else if (strongestInterest === 'blog') {
heading.textContent = 'Articles You Might Enjoy';
} else if (strongestInterest === 'support') {
heading.textContent = 'Support Resources';
} else {
heading.textContent = 'Recommended For You';
}
container.appendChild(heading);
// Create content container for recommendations
const content = document.createElement('div');
content.className = 'personalized-content';
// Fetch recommended content based on interest
fetchRecommendations(strongestInterest)
.then(items => {
items.forEach(item => {
const card = createContentCard(item);
content.appendChild(card);
});
})
.catch(error => {
// eslint-disable-next-line no-console
console.error('Error fetching recommendations:', error);
content.innerHTML = '<p>Unable to load recommendations at this time.</p>';
});
container.appendChild(content);
}
/**
* Fetches content recommendations based on interest
* @param {string} interest - Interest category
* @return {Promise<Array>} - Promise resolving to content items
*/
async function fetchRecommendations(interest) {
try {
const response = await fetch('/query-index.json');
if (!response.ok) throw new Error('Failed to fetch recommendations');
const data = await response.json();
// Filter by interest category
if (interest === 'products') {
filtered = filtered.filter(item => item.path.includes('/products/'));
} else if (interest === 'services') {
filtered = filtered.filter(item => item.path.includes('/services/'));
} else if (interest === 'blog') {
filtered = filtered.filter(item => item.path.includes('/blog/'));
} else if (interest === 'support') {
filtered = filtered.filter(item => item.path.includes('/support/'));
}
// Limit to 3 items
return filtered.slice(0, 3);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error in fetchRecommendations:', error);
return [];
}
}
/**
* Creates a content card element
* @param {Object} item - Content item data
* @return {HTMLElement} - Card element
*/
function createContentCard(item) {
const card = document.createElement('div');
card.className = 'content-card';
// Add image if available
if (item.image) {
const img = document.createElement('img');
img.src = item.image;
img.alt = item.title || '';
card.appendChild(img);
}
// Add title
const title = document.createElement('h3');
const link = document.createElement('a');
link.href = item.path;
link.textContent = item.title;
title.appendChild(link);
card.appendChild(title);
// Add description if available
if (item.description) {
const desc = document.createElement('p');
desc.textContent = item.description;
card.appendChild(desc);
}
return card;
}
}
About the Author: Tom Cranstoun is a seasoned AEM expert with over 13 years of experience, including roles as a Global Architecture Director and solution architect for major projects like Nissan and Renault's Helios initiative. Now running his own consultancy, Tom continues to work with top brands and agencies, bringing his expertise in AEM and emerging technologies to the forefront of digital experiences.
Thanks for reading.
Digital Domain Technologies provides expert Adobe Experience Manager (AEM) consultancy. We have collaborated with some of the world’s leading brands across various AEM platforms, including AEM Cloud, on-premise solutions, Adobe Managed Services, and Edge Delivery Services.
Related Articles