Multi-Site Manager (MSM)
MSM cho phép quản lý nhiều website chia sẻ nội dung chung. Tạo Blueprint (nguồn) và derive Live Copy từ đó. Thay đổi trên Blueprint có thể tự động hoặc thủ công đẩy sang Live Copy qua rollout. Author trên Live Copy có thể override component bằng cách hủy inheritance.
MSM là nền tảng cho multi-language, multi-region, multi-brand sites.
1. Khái Niệm Cốt Lõi
Blueprint
Blueprint là cây nội dung nguồn — master structure mà tất cả Live Copy kế thừa. Đăng ký Blueprint configuration để xuất hiện trong MSM UI:
<!-- apps/msm/myproject/blueprintconfigs/myproject-blueprint/.content.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
jcr:primaryType="cq:Page">
<jcr:content
jcr:primaryType="cq:PageContent"
jcr:title="My Project Blueprint"
sling:resourceType="msm/wcm/components/blueprint/configure"
sitePath="/content/myproject/master"/>
</jcr:root>
Lưu tại /apps/msm/myproject/blueprintconfigs/ (custom) hoặc /libs/msm/wcm/blueprintconfigs/ (product).
Live Copy
Live Copy là bản sao của Blueprint duy trì live relationship với source. Relationship được track qua cq:LiveSyncConfig trong JCR:
/content/myproject/en-gb/jcr:content
├── cq:LiveSyncConfig
│ ├── cq:master = "/content/myproject/master"
│ ├── cq:rolloutConfigs = ["/libs/msm/wcm/rolloutconfigs/default"]
│ └── cq:isDeep = true
Rollout vs Synchronize
Cả hai thực thi cùng rollout configuration actions — khác nhau ở nơi trigger.
Inheritance
Mỗi component trên Live Copy page có 1 trong 2 trạng thái:
- Inherited (xanh): nội dung match Blueprint, rollout sẽ update component này
- Cancelled (vàng): author đã override, rollout bỏ qua component này
- Re-enable: reset component về phiên bản Blueprint (mất local changes)
2. Tạo Live Copy
Qua UI
- Sites → Create → Live Copy
- Chọn source (Blueprint page/subtree)
- Cấu hình destination path, site title
- Chọn rollout configuration (hoặc dùng default)
- Chọn include sub-pages (deep copy)
Qua Java API
package com.myproject.core.services;
import com.day.cq.wcm.api.WCMException;
import com.day.cq.wcm.msm.api.LiveRelationshipManager;
import com.day.cq.wcm.msm.api.RolloutConfig;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@Component(service = LiveCopyService.class)
public class LiveCopyServiceImpl implements LiveCopyService {
@Reference
private LiveRelationshipManager liveRelationshipManager;
@Override
public void createLiveCopy(ResourceResolver resolver,
String blueprintPath,
String liveCopyPath,
String title) throws WCMException {
Resource source = resolver.getResource(blueprintPath);
if (source == null) {
throw new WCMException("Blueprint not found: " + blueprintPath);
}
Resource targetParent = resolver.getResource(liveCopyPath);
if (targetParent == null) {
throw new WCMException("Target parent not found: " + liveCopyPath);
}
// null = dùng default rollout config
RolloutConfig[] rolloutConfigs = null;
liveRelationshipManager.create(
source, // Blueprint source
targetParent, // Parent của live copy mới
title, // Title
true, // deep — include sub-pages
rolloutConfigs // rollout configurations
);
resolver.commit();
}
}
Language Copy
MSM là cơ chế đằng sau language copy. Khi tạo qua Sites → Create → Language Copy, AEM tạo Live Copy với:
- Rollout configuration cho translation
- Integration với Translation Framework
- Property
jcr:language trên language root
/content/myproject/
├── master/ ← Blueprint (source language, vd: English)
│ └── en/
├── de/ ← Live Copy (German translation)
├── fr/ ← Live Copy (French translation)
└── ja/ ← Live Copy (Japanese translation)
3. Rollout Configurations
Rollout configuration định nghĩa actions nào chạy và khi nào trigger. Default config tại /libs/msm/wcm/rolloutconfigs/default.
Built-in configurations
Rollout triggers (cq:trigger)
Built-in rollout actions
Custom rollout configuration
<!-- apps/msm/myproject/rolloutconfigs/custom/.content.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
cq:trigger="rollout"
jcr:primaryType="cq:RolloutConfig"
jcr:title="My Custom Rollout Config"
jcr:description="Standard rollout plus custom notification">
<contentUpdate jcr:primaryType="cq:LiveSyncAction"/>
<contentCopy jcr:primaryType="cq:LiveSyncAction"/>
<contentDelete jcr:primaryType="cq:LiveSyncAction"/>
<referencesUpdate jcr:primaryType="cq:LiveSyncAction"/>
<orderChildren jcr:primaryType="cq:LiveSyncAction"/>
<NotifyOnRollout jcr:primaryType="cq:LiveSyncAction"/>
</jcr:root>
Tên node (NotifyOnRollout) phải match LiveActionFactory.LIVE_ACTION_NAME.
Thứ tự thực thi
Rollout configurations chạy theo thứ tự JCR node. Dùng sling:OrderedFolder để kiểm soát:
<!-- apps/msm/myproject/rolloutconfigs/.content.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
jcr:primaryType="sling:OrderedFolder"
jcr:title="Rollout Configurations">
<pageMove/>
<default/>
<custom/>
</jcr:root>
Assign rollout config cho Live Copy
- UI: Page Properties → tab Live Copy
- JCR: set property
cq:rolloutConfigs trên node cq:LiveSyncConfig
Có thể assign nhiều rollout configurations cho 1 Live Copy — chạy tuần tự.
4. Page Moves và Structural Changes
Standard Rollout Configuration không xử lý page moves. Đây là thiết kế có chủ đích — page move ngầm bao gồm delete, có thể gây hành vi không mong muốn trên publish.
Tạo rollout config riêng cho page moves:
<!-- apps/msm/myproject/rolloutconfigs/pageMove/.content.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
cq:trigger="rollout"
jcr:primaryType="cq:RolloutConfig"
jcr:title="PageMove Rollout Config"
jcr:description="Handles page moves from Blueprint to Live Copy">
<PageMoveAction jcr:primaryType="cq:LiveSyncAction"/>
</jcr:root>
PageMoveAction copy (không move) Live Copy page sang location mới. Page tại location cũ vẫn còn với relationship bị broken. Cần cleanup thủ công hoặc qua custom workflow.
5. Custom Rollout Action (Java)
package com.myproject.core.msm;
import com.day.cq.wcm.api.WCMException;
import com.day.cq.wcm.msm.api.LiveAction;
import com.day.cq.wcm.msm.api.LiveActionFactory;
import com.day.cq.wcm.msm.api.LiveRelationship;
import org.apache.sling.api.resource.Resource;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(
service = LiveActionFactory.class,
immediate = true,
property = {
LiveActionFactory.LIVE_ACTION_NAME + "=" + NotificationRolloutActionFactory.ACTION_NAME
}
)
public class NotificationRolloutActionFactory implements LiveActionFactory<LiveAction> {
static final String ACTION_NAME = "NotifyOnRollout";
private static final Logger LOG = LoggerFactory.getLogger(NotificationRolloutActionFactory.class);
@Reference
private NotificationService notificationService;
@Override
public String createsAction() {
return ACTION_NAME;
}
@Override
public LiveAction createAction(Resource resource) {
return new NotifyAction(notificationService);
}
private static class NotifyAction implements LiveAction {
private final NotificationService notificationService;
NotifyAction(NotificationService notificationService) {
this.notificationService = notificationService;
}
@Override
public String getName() {
return ACTION_NAME;
}
@Override
public void execute(Resource source, Resource target,
LiveRelationship relation, boolean autoSave,
boolean isResetRollout) throws WCMException {
if (source == null || target == null) {
return;
}
String sourcePath = source.getPath();
String targetPath = target.getPath();
LOG.info("Rollout: {} -> {}", sourcePath, targetPath);
try {
notificationService.notifyRollout(sourcePath, targetPath, isResetRollout);
} catch (Exception e) {
LOG.error("Notification failed: {} -> {}", sourcePath, targetPath, e);
// Không throw — notification failure không block rollout
}
}
}
}
LiveAction không phải OSGi component — không dùng @Reference bên trong. Inject service vào factory, truyền qua constructor.
Đăng ký action trong rollout config XML:
<NotifyOnRollout jcr:primaryType="cq:LiveSyncAction"/>
6. MSM Java API
LiveRelationshipManager
@Reference
private LiveRelationshipManager liveRelationshipManager;
Kiểm tra MSM status
import com.day.cq.wcm.msm.api.LiveRelationship;
import com.day.cq.wcm.msm.api.LiveStatus;
public boolean isLiveCopy(Resource resource) throws WCMException {
LiveRelationship rel = liveRelationshipManager
.getLiveRelationship(resource, false);
return rel != null;
}
public String getBlueprintPath(Resource liveCopyResource) throws WCMException {
LiveRelationship rel = liveRelationshipManager
.getLiveRelationship(liveCopyResource, false);
if (rel != null) {
LiveStatus status = rel.getStatus();
if (status != null) {
return status.getSourcePath();
}
}
return null;
}
Trigger rollout programmatically
import com.day.cq.wcm.msm.api.RolloutManager;
@Reference
private RolloutManager rolloutManager;
public void rollout(ResourceResolver resolver, String blueprintPath, boolean deep)
throws WCMException {
Resource blueprint = resolver.getResource(blueprintPath);
if (blueprint == null) {
throw new WCMException("Blueprint not found: " + blueprintPath);
}
rolloutManager.rollout(resolver, blueprint, deep);
resolver.commit();
}
Cancel và re-enable inheritance
// Cancel inheritance — component trở thành local override
LiveRelationship rel = liveRelationshipManager
.getLiveRelationship(componentResource, false);
if (rel != null) {
liveRelationshipManager.cancelRelationship(
resolver, rel, false, false);
resolver.commit();
}
// Re-enable inheritance — reset về Blueprint content (mất local changes)
liveRelationshipManager.reenableRelationship(resolver, rel, false);
resolver.commit();
List tất cả Live Copies
import javax.jcr.RangeIterator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public List<String> getLiveCopyPaths(ResourceResolver resolver, String blueprintPath)
throws WCMException {
Resource blueprint = resolver.getResource(blueprintPath);
if (blueprint == null) {
return Collections.emptyList();
}
List<String> paths = new ArrayList<>();
RangeIterator relationships = liveRelationshipManager
.getLiveRelationships(blueprint, null, null);
while (relationships.hasNext()) {
LiveRelationship rel = (LiveRelationship) relationships.next();
if (rel.getTargetPath() != null) {
paths.add(rel.getTargetPath());
}
}
return paths;
}
7. MSM trong Sling Model
Detect MSM context trong component — kiểm tra page hiện tại có phải Live Copy, inheritance đã bị hủy chưa:
package com.myproject.core.models;
import com.day.cq.wcm.api.WCMException;
import com.day.cq.wcm.msm.api.LiveRelationship;
import com.day.cq.wcm.msm.api.LiveRelationshipManager;
import com.day.cq.wcm.msm.api.LiveStatus;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
@Model(adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class MsmAwareComponent {
private static final Logger LOG = LoggerFactory.getLogger(MsmAwareComponent.class);
@SlingObject
private Resource resource;
@OSGiService
private LiveRelationshipManager liveRelationshipManager;
private boolean liveCopy;
private boolean inheritanceCancelled;
private String blueprintPath;
@PostConstruct
protected void init() {
try {
LiveRelationship rel = liveRelationshipManager
.getLiveRelationship(resource, true);
if (rel != null) {
liveCopy = true;
LiveStatus status = rel.getStatus();
if (status != null) {
blueprintPath = status.getSourcePath();
inheritanceCancelled = status.isCancelled();
}
}
} catch (WCMException e) {
LOG.debug("Cannot determine MSM status for {}", resource.getPath(), e);
}
}
public boolean isLiveCopy() { return liveCopy; }
public boolean isInheritanceCancelled() { return inheritanceCancelled; }
public String getBlueprintPath() { return blueprintPath; }
}
8. Groovy Console Scripts
List tất cả Live Copies của Blueprint
import com.day.cq.wcm.msm.api.LiveRelationshipManager
def lrm = sling.getService(LiveRelationshipManager)
def blueprintPath = '/content/myproject/master/en'
def blueprint = resourceResolver.getResource(blueprintPath)
if (blueprint == null) {
out.println("Blueprint not found: ${blueprintPath}")
return
}
def rels = lrm.getLiveRelationships(blueprint, null, null)
int count = 0
while (rels.hasNext()) {
def rel = rels.next()
out.println("Live Copy: ${rel.targetPath} -> Blueprint: ${rel.sourcePath}")
count++
}
out.println("---")
out.println("Total: ${count} live copies")
Kiểm tra inheritance status
import com.day.cq.wcm.msm.api.LiveRelationshipManager
def lrm = sling.getService(LiveRelationshipManager)
def pagePath = '/content/myproject/en-gb/products'
def resource = resourceResolver.getResource(pagePath + '/jcr:content')
if (resource == null) {
out.println("Resource not found")
return
}
def rel = lrm.getLiveRelationship(resource, true)
if (rel != null) {
def status = rel.status
out.println("Is Live Copy: true")
out.println("Source: ${status?.sourcePath}")
out.println("Cancelled: ${status?.cancelled}")
out.println("Last rolled out: ${status?.lastRolledOut}")
} else {
out.println("Not a Live Copy")
}
Force rollout subtree
import com.day.cq.wcm.msm.api.RolloutManager
final boolean DRY_RUN = true
def rm = sling.getService(RolloutManager)
def blueprintPath = '/content/myproject/master/en/products'
def blueprint = resourceResolver.getResource(blueprintPath)
if (blueprint == null) {
out.println("Blueprint not found: ${blueprintPath}")
return
}
if (!DRY_RUN) {
rm.rollout(resourceResolver, blueprint, true)
out.println("Rolled out: ${blueprintPath} (deep)")
} else {
out.println("DRY RUN — would roll out: ${blueprintPath} (deep)")
}
9. Multi-Site Architecture Patterns
Pattern 1: Multi-region (cùng ngôn ngữ)
/content/myproject/
├── master/en/ ← Blueprint: Global English
├── en-us/ ← Live Copy: US English (pricing, legal riêng)
├── en-gb/ ← Live Copy: UK English (spelling, legal riêng)
└── en-au/ ← Live Copy: Australian English
Phù hợp: global brands có regional variations trong cùng ngôn ngữ.
Pattern 2: Multi-language
/content/myproject/
├── en/ ← Blueprint: English (source)
├── de/ ← Live Copy + Translation: German
├── fr/ ← Live Copy + Translation: French
└── vi/ ← Live Copy + Translation: Vietnamese
Phù hợp: multilingual sites, cấu trúc giống nhau nhưng nội dung được dịch.
Pattern 3: Multi-brand
/content/
├── brand-master/ ← Blueprint: shared structure + content
├── brand-a/ ← Live Copy: Brand A (theme, nội dung riêng)
├── brand-b/ ← Live Copy: Brand B
└── brand-c/ ← Live Copy: Brand C
Phù hợp: holding companies có nhiều brands chia sẻ core content.
Pattern 4: Nested blueprints (multi-level)
Nested blueprints tăng độ phức tạp đáng kể. Rollout ở top level cascade qua intermediate levels. Test kỹ trước khi áp dụng.
10. Conflict Resolution
Rollout Conflict Handler
Cấu hình tại Tools → Operations → Web Console → Day CQ WCM Rollout Conflict Handler. Default dùng chiến lược "Blueprint wins" cho inherited content.
11. Best Practices
Giữ Blueprint sạch
- Blueprint content phải generic, reusable — không chứa nội dung region-specific
- Dùng content policies và Style System cho visual differences thay vì Live Copy override
- Thiết kế Blueprint là superset — Live Copy có thể ẩn pages không cần
Giảm thiểu cancelled inheritance
- Mỗi cancelled inheritance = 1 component không nhận Blueprint updates nữa
- Ưu tiên design dialogs / template policies cho regional differences
- Track cancelled components qua Live Copy Overview trong Sites console
Rollout strategy
- Manual rollout cho content thay đổi quan trọng cần review
- Automatic triggers (
modification, activate) chỉ cho low-risk content (navigation, footer)
- Test rollout trên Author trước khi activate sang Publish
- Schedule large rollout vào thời điểm traffic thấp
Naming và structure
- Naming nhất quán:
en-us, en-gb, de-de
- Mirror Blueprint structure chính xác — divergence gây rollout không dự đoán được
- Document rollout configuration nào được assign cho Live Copy nào
Pitfalls Thường Gặp
Tham Khảo