Skip to main content
Glama
page-operations.js25.5 kB
/** * Page Operations Module * Handles all AEM page-related operations including CRUD, activation, and content extraction */ import { createAEMError, handleAEMHttpError, safeExecute, createSuccessResponse, AEM_ERROR_CODES, isValidContentPath } from '../error-handler.js'; export class PageOperations { httpClient; logger; config; constructor(httpClient, logger, config) { this.httpClient = httpClient; this.logger = logger; this.config = config; } /** * Create a new page in AEM with proper template handling */ async createPage(request) { return safeExecute(async () => { const { parentPath, title, template, name, properties = {} } = request; if (!isValidContentPath(parentPath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid parent path: ${String(parentPath)}`, { parentPath }); } // Auto-select template if not provided let selectedTemplate = template; if (!selectedTemplate) { const templatesResponse = await this.getTemplates(parentPath); const availableTemplates = templatesResponse.data.availableTemplates; if (availableTemplates.length === 0) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'No templates available for this path', { parentPath }); } selectedTemplate = availableTemplates[0].path; this.logger.info(`Auto-selected template: ${selectedTemplate}`); } // Validate template exists try { await this.httpClient.get(`${selectedTemplate}.json`); } catch (error) { if (error.response?.status === 404) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Template not found: ${selectedTemplate}`, { template: selectedTemplate }); } throw handleAEMHttpError(error, 'createPage'); } const pageName = name || title.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase(); const newPagePath = `${parentPath}/${pageName}`; // Create page with proper structure const pageData = { 'jcr:primaryType': 'cq:Page', 'jcr:content': { 'jcr:primaryType': 'cq:PageContent', 'jcr:title': title, 'cq:template': selectedTemplate, 'sling:resourceType': 'foundation/components/page', 'cq:lastModified': new Date().toISOString(), 'cq:lastModifiedBy': 'admin', ...properties } }; // Create the page using Sling POST servlet const formData = new URLSearchParams(); formData.append('jcr:primaryType', 'cq:Page'); // Create page first await this.httpClient.post(newPagePath, formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); // Then create jcr:content node const contentFormData = new URLSearchParams(); Object.entries(pageData['jcr:content']).forEach(([key, value]) => { if (key === 'jcr:created' || key === 'jcr:createdBy') { return; } if (typeof value === 'object') { contentFormData.append(key, JSON.stringify(value)); } else { contentFormData.append(key, String(value)); } }); await this.httpClient.post(`${newPagePath}/jcr:content`, contentFormData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); // Verify page creation const verificationResponse = await this.httpClient.get(`${newPagePath}.json`); const hasJcrContent = verificationResponse.data['jcr:content'] !== undefined; // Check if page is accessible in author mode let pageAccessible = false; try { const authorResponse = await this.httpClient.get(`${newPagePath}.html`, { validateStatus: (status) => status < 500 }); pageAccessible = authorResponse.status === 200; } catch (error) { pageAccessible = false; } return createSuccessResponse({ pagePath: newPagePath, title, templateUsed: selectedTemplate, jcrContentCreated: hasJcrContent, pageAccessible, errorLogCheck: { hasErrors: false, errors: [] }, creationDetails: { timestamp: new Date().toISOString(), steps: [ 'Template validation completed', 'Page node created', 'jcr:content node created', 'Page structure verified', 'Accessibility check completed' ] }, pageStructure: verificationResponse.data }, 'createPage'); }, 'createPage'); } /** * Delete a page from AEM */ async deletePage(request) { return safeExecute(async () => { const { pagePath, force = false } = request; if (!isValidContentPath(pagePath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid page path: ${String(pagePath)}`, { pagePath }); } let deleted = false; try { await this.httpClient.delete(pagePath); deleted = true; } catch (err) { if (err.response && err.response.status === 405) { try { await this.httpClient.post('/bin/wcmcommand', { cmd: 'deletePage', path: pagePath, force: force.toString(), }); deleted = true; } catch (postErr) { try { await this.httpClient.post(pagePath, { ':operation': 'delete' }); deleted = true; } catch (slingErr) { this.logger.error('Sling POST delete failed', { error: slingErr.response?.status, data: slingErr.response?.data }); throw slingErr; } } } else { this.logger.error('DELETE failed', { status: err.response?.status, data: err.response?.data }); throw err; } } return createSuccessResponse({ success: deleted, deletedPath: pagePath, timestamp: new Date().toISOString(), }, 'deletePage'); }, 'deletePage'); } /** * List all cq:Page nodes under a site root */ async listPages(siteRoot, depth = 1, limit = 20) { return safeExecute(async () => { // First try direct JSON API approach for better performance try { const response = await this.httpClient.get(`${siteRoot}.${depth}.json`); const pages = []; const processNode = (node, currentPath, currentDepth) => { if (currentDepth > depth || pages.length >= limit) return; Object.entries(node).forEach(([key, value]) => { if (pages.length >= limit) return; // Skip JCR system properties if (key.startsWith('jcr:') || key.startsWith('sling:') || key.startsWith('cq:') || key.startsWith('rep:') || key.startsWith('oak:')) { return; } if (value && typeof value === 'object') { const childPath = `${currentPath}/${key}`; const primaryType = value['jcr:primaryType']; // Only include cq:Page nodes if (primaryType === 'cq:Page') { pages.push({ name: key, path: childPath, primaryType: 'cq:Page', title: value['jcr:content']?.['jcr:title'] || key, template: value['jcr:content']?.['cq:template'], lastModified: value['jcr:content']?.['cq:lastModified'], lastModifiedBy: value['jcr:content']?.['cq:lastModifiedBy'], resourceType: value['jcr:content']?.['sling:resourceType'], type: 'page' }); } // Recursively process child nodes if within depth limit if (currentDepth < depth) { processNode(value, childPath, currentDepth + 1); } } }); }; if (response.data && typeof response.data === 'object') { processNode(response.data, siteRoot, 0); } return createSuccessResponse({ siteRoot, pages, pageCount: pages.length, depth, limit, totalChildrenScanned: pages.length }, 'listPages'); } catch (error) { // Fallback to QueryBuilder if JSON API fails if (error.response?.status === 404 || error.response?.status === 403) { const response = await this.httpClient.get('/bin/querybuilder.json', { params: { path: siteRoot, type: 'cq:Page', 'p.nodedepth': depth.toString(), 'p.limit': limit.toString(), 'p.hits': 'full' }, }); const pages = (response.data.hits || []).map((hit) => ({ name: hit.name || hit.path?.split('/').pop(), path: hit.path, primaryType: 'cq:Page', title: hit['jcr:content/jcr:title'] || hit.title || hit.name, template: hit['jcr:content/cq:template'], lastModified: hit['jcr:content/cq:lastModified'], lastModifiedBy: hit['jcr:content/cq:lastModifiedBy'], resourceType: hit['jcr:content/sling:resourceType'], type: 'page' })); return createSuccessResponse({ siteRoot, pages, pageCount: pages.length, depth, limit, totalChildrenScanned: response.data.total || pages.length, fallbackUsed: 'QueryBuilder' }, 'listPages'); } throw error; } }, 'listPages'); } /** * Get complete page content including Experience Fragments and Content Fragments */ async getPageContent(pagePath) { return safeExecute(async () => { const response = await this.httpClient.get(`${pagePath}.infinity.json`); return createSuccessResponse({ pagePath, content: response.data, }, 'getPageContent'); }, 'getPageContent'); } /** * Get page properties and metadata */ async getPageProperties(pagePath) { return safeExecute(async () => { const response = await this.httpClient.get(`${pagePath}/jcr:content.json`); const content = response.data; const properties = { title: content['jcr:title'], description: content['jcr:description'], template: content['cq:template'], lastModified: content['cq:lastModified'], lastModifiedBy: content['cq:lastModifiedBy'], created: content['jcr:created'], createdBy: content['jcr:createdBy'], primaryType: content['jcr:primaryType'], resourceType: content['sling:resourceType'], tags: content['cq:tags'] || [], properties: content, }; return createSuccessResponse({ pagePath, properties }, 'getPageProperties'); }, 'getPageProperties'); } /** * Activate (publish) a single page */ async activatePage(request) { return safeExecute(async () => { const { pagePath, activateTree = false } = request; if (!isValidContentPath(pagePath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid page path: ${String(pagePath)}`, { pagePath }); } try { // Use the correct AEM replication servlet endpoint const formData = new URLSearchParams(); formData.append('cmd', 'Activate'); formData.append('path', pagePath); formData.append('ignoredeactivated', 'false'); formData.append('onlymodified', 'false'); if (activateTree) { formData.append('deep', 'true'); } const response = await this.httpClient.post('/bin/replicate.json', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); return createSuccessResponse({ success: true, activatedPath: pagePath, activateTree, response: response.data, timestamp: new Date().toISOString(), }, 'activatePage'); } catch (error) { // Fallback to alternative replication methods try { const wcmResponse = await this.httpClient.post('/bin/wcmcommand', { cmd: 'activate', path: pagePath, ignoredeactivated: false, onlymodified: false, }); return createSuccessResponse({ success: true, activatedPath: pagePath, activateTree, response: wcmResponse.data, fallbackUsed: 'WCM Command', timestamp: new Date().toISOString(), }, 'activatePage'); } catch (fallbackError) { throw handleAEMHttpError(error, 'activatePage'); } } }, 'activatePage'); } /** * Deactivate (unpublish) a single page */ async deactivatePage(request) { return safeExecute(async () => { const { pagePath, deactivateTree = false } = request; if (!isValidContentPath(pagePath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid page path: ${String(pagePath)}`, { pagePath }); } try { // Use the correct AEM replication servlet endpoint const formData = new URLSearchParams(); formData.append('cmd', 'Deactivate'); formData.append('path', pagePath); formData.append('ignoredeactivated', 'false'); formData.append('onlymodified', 'false'); if (deactivateTree) { formData.append('deep', 'true'); } const response = await this.httpClient.post('/bin/replicate.json', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); return createSuccessResponse({ success: true, deactivatedPath: pagePath, deactivateTree, response: response.data, timestamp: new Date().toISOString(), }, 'deactivatePage'); } catch (error) { // Fallback to alternative replication methods try { const wcmResponse = await this.httpClient.post('/bin/wcmcommand', { cmd: 'deactivate', path: pagePath, ignoredeactivated: false, onlymodified: false, }); return createSuccessResponse({ success: true, deactivatedPath: pagePath, deactivateTree, response: wcmResponse.data, fallbackUsed: 'WCM Command', timestamp: new Date().toISOString(), }, 'deactivatePage'); } catch (fallbackError) { throw handleAEMHttpError(error, 'deactivatePage'); } } }, 'deactivatePage'); } /** * Get all text content from a page including titles, text components, and descriptions */ async getAllTextContent(pagePath) { return safeExecute(async () => { const response = await this.httpClient.get(`${pagePath}.infinity.json`); const textContent = []; const processNode = (node, nodePath) => { if (!node || typeof node !== 'object') return; if (node['text'] || node['jcr:title'] || node['jcr:description']) { textContent.push({ path: nodePath, title: node['jcr:title'], text: node['text'], description: node['jcr:description'], }); } Object.entries(node).forEach(([key, value]) => { if (typeof value === 'object' && value !== null && !key.startsWith('rep:') && !key.startsWith('oak:')) { const childPath = nodePath ? `${nodePath}/${key}` : key; processNode(value, childPath); } }); }; if (response.data['jcr:content']) { processNode(response.data['jcr:content'], 'jcr:content'); } else { processNode(response.data, pagePath); } return createSuccessResponse({ pagePath, textContent, }, 'getAllTextContent'); }, 'getAllTextContent'); } /** * Get text content from a specific page (alias for getAllTextContent) */ async getPageTextContent(pagePath) { return this.getAllTextContent(pagePath); } /** * Get all images from a page, including those within Experience Fragments */ async getPageImages(pagePath) { return safeExecute(async () => { const response = await this.httpClient.get(`${pagePath}.infinity.json`); const images = []; const processNode = (node, nodePath) => { if (!node || typeof node !== 'object') return; if (node['fileReference'] || node['src']) { images.push({ path: nodePath, fileReference: node['fileReference'], src: node['src'], alt: node['alt'] || node['altText'], title: node['jcr:title'] || node['title'], }); } Object.entries(node).forEach(([key, value]) => { if (typeof value === 'object' && value !== null && !key.startsWith('rep:') && !key.startsWith('oak:')) { const childPath = nodePath ? `${nodePath}/${key}` : key; processNode(value, childPath); } }); }; if (response.data['jcr:content']) { processNode(response.data['jcr:content'], 'jcr:content'); } else { processNode(response.data, pagePath); } return createSuccessResponse({ pagePath, images, }, 'getPageImages'); }, 'getPageImages'); } /** * Helper method to get available templates for a parent path */ async getTemplates(parentPath) { // This is a simplified version - in a full implementation, this would call the template service try { let confPath = '/conf'; const pathParts = parentPath.split('/'); if (pathParts.length >= 3 && pathParts[1] === 'content') { const siteName = pathParts[2]; confPath = `/conf/${siteName}`; } const templatesPath = `${confPath}/settings/wcm/templates`; const response = await this.httpClient.get(`${templatesPath}.json`, { params: { ':depth': '3' } }); const templates = []; if (response.data && typeof response.data === 'object') { Object.entries(response.data).forEach(([key, value]) => { if (key.startsWith('jcr:') || key.startsWith('sling:')) return; if (value && typeof value === 'object' && value['jcr:content']) { const templatePath = `${templatesPath}/${key}`; const content = value['jcr:content']; templates.push({ name: key, path: templatePath, title: content['jcr:title'] || key, description: content['jcr:description'] || '', status: content['status'] || 'enabled', ranking: content['ranking'] || 0, }); } }); } return { data: { parentPath, templatesPath, templates, availableTemplates: templates.filter(t => t.status === 'enabled') } }; } catch (error) { // Fallback to global templates const globalTemplatesPath = '/libs/wcm/foundation/templates'; const globalResponse = await this.httpClient.get(`${globalTemplatesPath}.json`, { params: { ':depth': '2' } }); const globalTemplates = []; if (globalResponse.data && typeof globalResponse.data === 'object') { Object.entries(globalResponse.data).forEach(([key, value]) => { if (key.startsWith('jcr:') || key.startsWith('sling:')) return; if (value && typeof value === 'object') { globalTemplates.push({ name: key, path: `${globalTemplatesPath}/${key}`, title: value['jcr:title'] || key, description: value['jcr:description'] || 'Global template', status: 'enabled', ranking: 0, isGlobal: true }); } }); } return { data: { parentPath, templatesPath: globalTemplatesPath, templates: globalTemplates, availableTemplates: globalTemplates, fallbackUsed: true } }; } } } //# sourceMappingURL=page-operations.js.map

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/indrasishbanerjee/aem-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server