1. Tag Trong AEM Là Gì
Tag là node JCR cấp cao (cq:Tag) nằm dưới /content/cq:tags/. Tag được gán cho pages, assets, Content Fragments, hoặc bất kỳ resource nào có property cq:tags. Tag phục vụ: navigation, search facets, analytics segmentation, personalisation.
2. Cấu Trúc Tag
Namespaces
Mỗi tag thuộc một namespace — container cấp cao nhất, là direct children của /content/cq:tags/:
/content/cq:tags/
├── marketing/ ← namespace
│ ├── campaigns/
│ │ ├── 2024/
│ │ └── 2025/
│ └── channels/
│ ├── email
│ ├── social
│ └── web
├── products/ ← namespace
│ ├── shoes/
│ │ ├── running
│ │ └── hiking
│ └── clothing/
└── regions/ ← namespace
├── emea
├── americas
└── apac
Node type cq:Tag
Tag ID
Tag được định danh bằng tag ID — path relative so với /content/cq:tags/, dùng dấu : phân tách namespace khỏi path:
marketing:campaigns/2025
products:shoes/running
regions:emea
Property cq:tags trên resource lưu dạng String array:
cq:tags = ["marketing:campaigns/2025", "products:shoes/running"]
Luôn dùng tag ID (namespace:path), không dùng JCR path (/content/cq:tags/namespace/path). Tag ID ổn định qua các thao tác move và export.
3. Tạo và Quản Lý Tag
Tagging Console (UI)
Tools > General > Tagging (/aem/tagging):
- Tạo namespaces và child tags
- Sửa title, description
- Move, merge tags
- Dịch tag title sang nhiều ngôn ngữ
- Xóa tags không dùng
TagManager API (Java)
TagManager là interface chính để thao tác tag trong Java code.
TagManager tagManager = resourceResolver.adaptTo(TagManager.class);
Tạo tag
Tag newTag = tagManager.createTag(
"marketing:campaigns/2025/summer", // tag ID
"Summer 2025 Campaign", // title
"Tags for the summer 2025 campaign", // description
true // auto-save
);
createTag tự tạo parent tags nếu chưa tồn tại.
Resolve tag
Tag tag = tagManager.resolve("marketing:campaigns/2025");
// Title theo locale
String titleDe = tag.getTitle(Locale.GERMAN); // "Kampagnen 2025"
String titleDefault = tag.getTitle(); // "2025"
String titleLocalized = tag.getLocalizedTitle(locale); // fallback chain
// Thông tin tag
String path = tag.getPath(); // /content/cq:tags/marketing/campaigns/2025
String tagId = tag.getTagID(); // marketing:campaigns/2025
String ns = tag.getNamespace().getName(); // marketing
// Qua TagManager
Tag[] tags = tagManager.getTags(resource);
// Hoặc đọc trực tiếp property
String[] tagIds = resource.getValueMap().get("cq:tags", String[].class);
Tag campaignTag = tagManager.resolve("marketing:campaigns/2025");
Tag channelTag = tagManager.resolve("marketing:channels/email");
// Thay thế toàn bộ tags hiện có
tagManager.setTags(resource, new Tag[]{ campaignTag, channelTag }, true);
Tìm resources theo tag
Iterator<Resource> resources = tagManager.find(
"/content/mysite", // search root
"marketing:campaigns/2025" // tag ID
);
while (resources.hasNext()) {
Resource taggedResource = resources.next();
// xử lý resource
}
TagManager API tham khảo
@Model(
adaptables = Resource.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class TaggedContentModel {
@Self
private Resource resource;
@ValueMapValue(name = "cq:tags")
private String[] cqTags;
private List<TagInfo> tags;
@PostConstruct
protected void init() {
tags = new ArrayList<>();
if (cqTags == null || cqTags.length == 0) return;
TagManager tagManager = resource.getResourceResolver()
.adaptTo(TagManager.class);
if (tagManager == null) return;
for (String tagId : cqTags) {
Tag tag = tagManager.resolve(tagId);
if (tag != null) {
tags.add(new TagInfo(
tag.getTagID(),
tag.getTitle(),
tag.getPath()
));
}
}
}
public List<TagInfo> getTags() {
return Collections.unmodifiableList(tags);
}
public static class TagInfo {
private final String id;
private final String title;
private final String path;
public TagInfo(String id, String title, String path) {
this.id = id;
this.title = title;
this.path = path;
}
public String getId() { return id; }
public String getTitle() { return title; }
public String getPath() { return path; }
}
}
Tag title theo locale
Khi site hỗ trợ nhiều ngôn ngữ, dùng SlingHttpServletRequest để lấy locale:
@Model(
adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class LocalisedTagModel {
@Self
private SlingHttpServletRequest request;
@ValueMapValue(name = "cq:tags")
private String[] tagIds;
public List<String> getTagTitles() {
if (tagIds == null) return Collections.emptyList();
Locale pageLocale = request.getLocale();
TagManager tagManager = request.getResourceResolver()
.adaptTo(TagManager.class);
if (tagManager == null) return Collections.emptyList();
List<String> titles = new ArrayList<>();
for (String id : tagIds) {
Tag tag = tagManager.resolve(id);
if (tag != null) {
titles.add(tag.getLocalizedTitle(pageLocale));
}
}
return titles;
}
}
Fallback chain của getLocalizedTitle(locale):
- Exact locale match (
de_DE)
- Language match (
de)
- Default title (không có locale suffix)
5. Tag Field Trong Component Dialog
Tag picker chuẩn
<tags
jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/coral/common/form/tagfield"
fieldLabel="Tags"
fieldDescription="Select one or more tags"
name="./cq:tags"
multiple="{Boolean}true"/>
Giới hạn theo namespace
Dùng rootPath để restrict tag picker chỉ hiện tags trong namespace cụ thể:
<tags
jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/coral/common/form/tagfield"
fieldLabel="Product Tags"
name="./productTags"
multiple="{Boolean}true"
rootPath="/content/cq:tags/products"/>
Single tag (không multi-select)
<category
jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/coral/common/form/tagfield"
fieldLabel="Primary Category"
name="./primaryCategory"
multiple="{Boolean}false"/>
<sly data-sly-use.model="com.myproject.core.models.TaggedContentModel"/>
<div class="tag-list" data-sly-test="${model.tags.size > 0}">
<ul>
<li data-sly-repeat="${model.tags}">
<span class="tag">${item.title}</span>
</li>
</ul>
</div>
<sly data-sly-use.model="com.myproject.core.models.TaggedContentModel"/>
<nav class="tags" data-sly-test="${model.tags.size > 0}">
<a data-sly-repeat="${model.tags}"
href="/content/mysite/en/tags.html?tag=${item.id}"
class="tag-link">
${item.title}
</a>
</nav>
7. Tìm Kiếm Theo Tag Bằng QueryBuilder
Tìm pages có tag cụ thể
path=/content/mysite/en
type=cq:Page
1_property=jcr:content/cq:tags
1_property.value=marketing:campaigns/2025
p.limit=20
p.guessTotal=true
Tìm pages có BẤT KỲ tag nào trong danh sách (OR)
path=/content/mysite/en
type=cq:Page
group.p.or=true
group.1_property=jcr:content/cq:tags
group.1_property.value=marketing:channels/email
group.2_property=jcr:content/cq:tags
group.2_property.value=marketing:channels/social
p.limit=20
p.guessTotal=true
Dùng predicate tagid (khuyến nghị)
Predicate tagid chuyên dùng cho tag query, đơn giản hơn property predicate:
path=/content/mysite/en
type=cq:Page
tagid=marketing:campaigns/2025
tagid.property=jcr:content/cq:tags
p.limit=20
tagid tự động match child tags. Query dưới đây trả về pages tag với marketing:campaigns, marketing:campaigns/2024, marketing:campaigns/2025...:
path=/content/mysite/en
type=cq:Page
tagid=marketing:campaigns
tagid.property=jcr:content/cq:tags
p.limit=20
Đây là lợi thế lớn của tagid so với property predicate — hỗ trợ hierarchical tag matching mà không cần viết nhiều điều kiện OR.
8. Taxonomy Design Patterns
Flat vs Hierarchical
Multi-site tag governance
Closed namespace — kiểm soát vocabulary
Ngăn author tự tạo tags mới, buộc dùng vocabulary đã định nghĩa:
- Set namespace thành closed trong Tagging Console
- Hoặc set property
cq:isContainer = false trên namespace node
- Chỉ user thuộc group
tag-administrators mới tạo được tags trong closed namespace
9. Dịch Tag Title (i18n)
Thêm property jcr:title.<locale> trên tag node:
/content/cq:tags/products/shoes
├── jcr:title = "Shoes"
├── jcr:title.de = "Schuhe"
├── jcr:title.fr = "Chaussures"
├── jcr:title.es = "Zapatos"
└── jcr:title.ja = "靴"
Trong Java, tag.getLocalizedTitle(locale) tự xử lý fallback:
- Exact locale (
de_DE)
- Language (
de)
- Default title
10. Groovy Console Scripts
import com.day.cq.tagging.TagManager
def tagManager = resourceResolver.adaptTo(TagManager.class)
def namespace = tagManager.resolve("products")
def printTag(tag, depth) {
println " " * depth + tag.tagID + " -> " + tag.title
tag.listChildren().each { child ->
printTag(child, depth + 1)
}
}
printTag(namespace, 0)
import com.day.cq.tagging.TagManager
def tagManager = resourceResolver.adaptTo(TagManager.class)
def namespace = tagManager.resolve("marketing")
def checkTag(tag) {
def count = 0
def iter = tagManager.find("/content", tag.tagID)
while (iter.hasNext()) {
iter.next()
count++
}
if (count == 0) {
println "UNUSED: ${tag.tagID}"
}
tag.listChildren().each { child -> checkTag(child) }
}
checkTag(namespace)
Bulk rename tag (dry-run pattern)
def DRY_RUN = true
import com.day.cq.tagging.TagManager
def tagManager = resourceResolver.adaptTo(TagManager.class)
def oldTag = tagManager.resolve("marketing:campaigns/old-name")
if (oldTag != null) {
if (DRY_RUN) {
println "Would move ${oldTag.tagID} to marketing:campaigns/new-name"
} else {
tagManager.moveTag(oldTag, "marketing:campaigns/new-name")
resourceResolver.commit()
println "Tag moved successfully"
}
} else {
println "Tag not found"
}
11. Best Practices
Thiết kế taxonomy trước khi build. Lên kế hoạch namespaces, độ sâu hierarchy, governance rules trước khi tạo tags. Tái cấu trúc taxonomy sau khi content đã gắn tag rất tốn công.
Dùng closed namespace cho controlled vocabulary. Ngăn tag sprawl bằng cách đóng namespace, chỉ giao tag-administrators cho taxonomy owners.
Giữ hierarchy nông. Hai đến ba level thường đủ. Tags nest quá sâu khó navigate và ít mang lại giá trị phân loại thực tế.
Luôn dùng tag ID, không dùng JCR path. Tag ID ổn định qua move và export. JCR path (/content/cq:tags/...) sẽ hỏng khi tag được di chuyển.
Dịch tag title nếu site multi-language. Thêm jcr:title.<locale> trên tag node. Framework tự xử lý locale resolution.
Activate tags lên Publish. Tags phải được replicate, nếu không publish instance sẽ không resolve được tag ID. Kiểm tra replication status trong Tagging Console.
12. Lỗi Thường Gặp
Tham Khảo