AEM SPA Editor
AEM SPA Editor cho phép content author chỉnh sửa trực quan Single Page Applications (React hoặc Angular) ngay trong môi trường AEM Author — kéo thả component, inline editing, layout mode, đầy đủ tính năng authoring quen thuộc từ AEM Pages truyền thống.
SPA render từ JSON model do AEM cung cấp, và SPA Editor SDK làm cầu nối giữa SPA framework và authoring layer của AEM.
AEM 6.5
SPA Editor được hỗ trợ từ AEM 6.4 SP2 trở đi. Trên AEM 6.5, cần Service Pack 6 trở lên để có đầy đủ tính năng ổn định. Kiến trúc và API ở đây áp dụng cho cả AEM 6.5 và AEMaaCS.
1. Cơ chế hoạt động
Luồng kiến trúc
5 bước cốt lõi:
- AEM serve JSON model của page qua Sling Model Exporter (endpoint
.model.json)
- SPA đọc JSON và render các React/Angular component tương ứng
- SPA Editor SDK bổ sung authoring overlays (selection, drag targets, placeholder) lên trên SPA
- Edits từ Author UI được ghi xuống JCR → JSON model cập nhật → SPA re-render
- Trên Publish, SPA render từ cùng JSON nhưng không có editor overlay nào
Browser (Author)
│
▼
AEM SPA Editor Frame
├── Editor Overlays (authoring SDK)
└── SPA iframe
│
▼
ModelManager → GET /content/myproject/home.model.json
│
▼
React Component Tree (renders from JSON)
Cấu trúc JSON model
Page model là một JSON tree lồng nhau. Mỗi component map với một JCR node:
{
":type": "myproject/components/page",
":path": "/content/myproject/en/home",
":children": {
"root": {
":type": "wcm/foundation/components/responsivegrid",
":items": {
"hero": {
":type": "myproject/components/hero",
"title": "Welcome",
"backgroundImage": "/content/dam/myproject/hero.jpg"
},
"cards": {
":type": "myproject/components/cards",
":items": {}
}
},
":itemsOrder": ["hero", "cards"]
}
}
}
:type là key kết nối JSON node với React component thông qua MapTo.
2. Project Setup
Tạo project từ AEM Archetype
AEM Project Archetype sinh ra project SPA hoàn chỉnh khi chọn frontendModule=react:
mvn -B archetype:generate \
-DarchetypeGroupId=com.adobe.aem \
-DarchetypeArtifactId=aem-project-archetype \
-DarchetypeVersion=49 \
-DappTitle="My SPA Project" \
-DappId="myspa" \
-DgroupId="com.myproject" \
-DfrontendModule=react
Cấu trúc được sinh ra:
myspa/
├── core/ # Java backend (Sling Models, Exporters)
├── ui.apps/ # AEM components, templates, clientlibs
├── ui.content/ # Sample content
├── ui.config/ # OSGi configs
└── ui.frontend/ # React SPA
├── src/
│ ├── components/ # React components mapped tới AEM components
│ ├── App.js
│ └── index.js
├── package.json
└── webpack.config.js
Các npm package quan trọng
npm install @adobe/aem-react-editable-components \
@adobe/aem-spa-page-model-manager \
@adobe/aem-spa-component-mapping
Phiên bản
Trên AEM 6.5, dùng @adobe/aem-react-editable-components@^1.x và React 16/17. Phiên bản 2.x nhắm tới AEMaaCS và React 18. Kiểm tra compatibility matrix trước khi nâng cấp.
3. Component Mapping với MapTo
MapTo() là khái niệm trung tâm: kết nối một React component với một AEM resource type. Khi JSON model có node với :type = "myproject/components/hero", SPA render React component đã được map.
Mapping cơ bản
import React from 'react';
import { MapTo } from '@adobe/aem-react-editable-components';
const HeroEditConfig = {
emptyLabel: 'Hero',
isEmpty: function(props) {
return !props || !props.title;
}
};
const Hero = ({ title, backgroundImage, ctaText, ctaLink }) => (
<section
className="hero"
style={{ backgroundImage: `url(${backgroundImage})` }}
>
<div className="hero-content">
<h1>{title}</h1>
{ctaText && (
<a href={ctaLink} className="hero-cta">{ctaText}</a>
)}
</div>
</section>
);
export default MapTo('myproject/components/hero')(Hero, HeroEditConfig);
Cơ chế hoạt động
- JSON model chứa field
:type cho từng component
MapTo('myproject/components/hero')(HeroComponent, editConfig) đăng ký mapping vào registry
- Khi Page Model Manager gặp resource type đó trong JSON, nó render React component tương ứng
editConfig cho editor biết cách xử lý trạng thái rỗng và placeholder
Các tùy chọn editConfig
4. Container Components
Container component (như Responsive Grid) chứa các child component. Chúng nhận :items và :itemsOrder từ JSON model.
Map Responsive Grid chuẩn
import { MapTo, ResponsiveGrid } from '@adobe/aem-react-editable-components';
MapTo('wcm/foundation/components/responsivegrid')(ResponsiveGrid);
Custom container
Với container tùy chỉnh (tabbed section, accordion...), cần implement phía backend ContainerExporter và frontend nhận :items/:itemsOrder:
import React from 'react';
import { MapTo, withComponentMappingContext } from '@adobe/aem-react-editable-components';
const TabContainer = ({ cqItems, cqItemsOrder }) => {
const [activeTab, setActiveTab] = React.useState(0);
return (
<div className="tab-container">
<div className="tab-nav">
{cqItemsOrder.map((key, idx) => (
<button key={key} onClick={() => setActiveTab(idx)}>
Tab {idx + 1}
</button>
))}
</div>
<div className="tab-content">
{cqItemsOrder[activeTab] && cqItems[cqItemsOrder[activeTab]]}
</div>
</div>
);
};
export default MapTo('myproject/components/tab-container')(
withComponentMappingContext(TabContainer)
);
5. Page Component (Entry Point)
App.js — Root component
import React from 'react';
import { Page, withModel } from '@adobe/aem-react-editable-components';
import './components/Hero/Hero';
import './components/Cards/Cards';
import './components/Text/Text';
function App() {
return <Page />;
}
export default withModel(App);
withModel là HOC tự động fetch page model và truyền xuống dưới dạng props. Import các component file ở đây để đảm bảo MapTo được gọi và đăng ký vào registry trước khi render.
index.js — Entry point
import React from 'react';
import ReactDOM from 'react-dom';
import { ModelManager } from '@adobe/aem-spa-page-model-manager';
import App from './App';
ModelManager.initialize().then((pageModel) => {
ReactDOM.render(
<App
cqChildren={pageModel[':children']}
cqItems={pageModel[':items']}
cqItemsOrder={pageModel[':itemsOrder']}
cqPath={pageModel[':path']}
/>,
document.getElementById('spa-root')
);
});
ModelManager.initialize() fetch JSON model từ AEM và trả về pageModel để boot SPA.
6. Backend: Sling Model Exporter
Mỗi AEM component xuất hiện trong SPA cần một Sling Model Exporter sinh ra JSON representation tương ứng.
import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
@Model(
adaptables = SlingHttpServletRequest.class,
adapters = { HeroModel.class, ComponentExporter.class },
resourceType = HeroModel.RESOURCE_TYPE
)
@Exporter(
name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
public class HeroModel implements ComponentExporter {
static final String RESOURCE_TYPE = "myproject/components/hero";
@ValueMapValue(optional = true)
private String title;
@ValueMapValue(optional = true)
private String backgroundImage;
@ValueMapValue(optional = true)
private String ctaText;
@ValueMapValue(optional = true)
private String ctaLink;
public String getTitle() { return title; }
public String getBackgroundImage() { return backgroundImage; }
public String getCtaText() { return ctaText; }
public String getCtaLink() { return ctaLink; }
@Override
public String getExportedType() { return RESOURCE_TYPE; }
}
Hai điểm quan trọng:
@Exporter + implements ComponentExporter → model được include vào page JSON
getExportedType() trả về resource type → map ngược lại với :type trong JSON
Kiểm tra JSON tại: http://localhost:4502/content/myproject/en/home.model.json
7. SPA Routing và Navigation
SPA xử lý client-side routing. SPA Editor SDK tích hợp với React Router:
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { Page } from '@adobe/aem-react-editable-components';
import { ModelManager } from '@adobe/aem-spa-page-model-manager';
function App() {
return (
<Router>
<Route
path="/:pagePath*"
render={(routeProps) => (
<Page
cqPath={routeProps.match.url}
{...routeProps}
/>
)}
/>
</Router>
);
}
Khi user navigate sang page mới:
- React Router xử lý URL change
ModelManager fetch JSON model cho page mới (nếu chưa cache)
Page component re-render với model mới
- Trong author mode, editor overlays cập nhật tự động
8. Remote SPA
Remote SPA là SPA hosted bên ngoài AEM (Vercel, Netlify, CDN tùy chỉnh) nhưng vẫn có thể edit trong AEM SPA Editor thông qua iframe.
Cấu hình
- Host SPA trên external server
- Cấu hình AEM cho phép remote origin (CORS, CSP headers)
- SPA fetch page model từ AEM JSON endpoint
- SPA Editor load remote SPA trong iframe để authoring
ModelManager.initialize({
modelClient: new ModelClient('https://author.myproject.com')
});
Khi nào dùng Remote SPA
9. SPA vs Headless vs Edge Delivery
10. Authoring Experience
Edit mode
Author thấy SPA với blue overlays trên mỗi editable component:
- Click để select component
- Component toolbar: edit, configure, delete, move
- Kéo component từ side panel vào trang
- Inline-edit text component trực tiếp
Layout mode
Author resize component bên trong responsive grid, giống AEM Pages truyền thống.
Preview mode
Render SPA không có editor overlay — đúng như visitor sẽ thấy.
Placeholder handling
Khi component rỗng, SPA Editor SDK hiển thị placeholder dựa vào emptyLabel từ edit config:
const TextEditConfig = {
emptyLabel: 'Text -- click to edit',
isEmpty: (props) => !props || !props.text || props.text.trim().length === 0
};
11. Common Patterns
Conditional authoring scaffolding
Hiển thị markup bổ sung chỉ trong edit mode để guide author:
import { AuthoringUtils } from '@adobe/aem-spa-page-model-manager';
const MyComponent = (props) => {
const isInEditor = AuthoringUtils.isInEditor();
return (
<div className="my-component">
{isInEditor && (
<div className="author-hint">Kéo items vào đây</div>
)}
{/* Component content */}
</div>
);
};
Map Core Components
Tái sử dụng AEM Core Component React wrappers:
import { TitleV2IsEmptyFn } from '@adobe/aem-core-components-react-base';
import { MapTo } from '@adobe/aem-react-editable-components';
const Title = ({ text, type }) => {
const HeadingTag = type || 'h2';
return <HeadingTag>{text}</HeadingTag>;
};
MapTo('myproject/components/title')(Title, {
emptyLabel: 'Title',
isEmpty: TitleV2IsEmptyFn
});
12. Best Practices
- Map mọi editable component — component thiếu
MapTo sẽ tạo ra vùng trắng trong editor, không có thông báo lỗi
isEmpty() chính xác — placeholder chỉ xuất hiện khi component thực sự rỗng; implement sai gây confuse author
- Test cả hai mode — SPA hoạt động khác nhau giữa author mode (có overlay) và publish mode (render sạch)
- Không thao tác DOM trực tiếp — jQuery hay
document.querySelector xung đột với editor overlay, gây rendering glitch
- Toàn bộ editable content qua JSON model — không fetch data client-side cho content cần authoring; mọi content phải đi qua Sling Model Exporter
13. Common Pitfalls
Tham khảo