For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/aem/articles/aem-externalizer-guide.md.

Guide to configuring and using AEM Externalizer service to build absolute URLs across environments.

AEM Externalizer — Hướng dẫn sử dụng trong dự án mysite

1. Externalizer là gì?

com.day.cq.commons.Externalizer là OSGi service của AEM dùng để chuyển đổi đường dẫn JCR nội bộ thành URL tuyệt đối có schema (http:// / https://) và domain thật.

Khi nào PHẢI dùng Externalizer?

Tình huốngVí dụ
URL trong email / newsletterLink unsubscribe, link xem bài viết
SEO structured data (JSON-LD)"url": "https://mysite.com/content/..."
Sitemap XML generation<loc>https://mysite.com/content/...</loc>
Open Graph / Social meta tagsog:url, og:image
Link trong PDF exportKhông thể dùng relative path
Background job / Workflow không có HTTP request contextKhông có request.getServerName()

Quy tắc vàng: Bất cứ khi nào code chạy ngoài request context (workflow, scheduler, email service) hoặc cần URL tuyệt đối cho external system → luôn dùng Externalizer, không hardcode domain.


2. Cấu hình OSGi

Cấu trúc thư mục config theo môi trường

ui.config/src/main/content/jcr_root/apps/mysite/osgiconfig/
├── config/                          ← Áp dụng cho TẤT CẢ môi trường (dev local)
│   └── com.day.cq.commons.impl.ExternalizerImpl.cfg.json
├── config.author/                   ← Chỉ áp dụng cho Author instance
│   └── (không cần Externalizer riêng vì dùng chung với config/)
├── config.stage/                    ← Staging environment
│   └── com.day.cq.commons.impl.ExternalizerImpl.cfg.json
└── config.prod/                     ← Production environment
    └── com.day.cq.commons.impl.ExternalizerImpl.cfg.json

AEM chọn config theo thứ tự ưu tiên: config.prod > config.stage > config.author > config
RunMode được set khi khởi động AEM bằng -Dsling.run.modes=author,prod


2.1. Local Development

File: ui.config/.../osgiconfig/config/com.day.cq.commons.impl.ExternalizerImpl.cfg.json

{
    "externalizer.domains": [
        "local http://localhost:4502",
        "author http://localhost:4502",
        "publish http://localhost:4503"
    ],
    "externalizer.encodedpath": true
}

2.2. Staging Environment

File: ui.config/.../osgiconfig/config.stage/com.day.cq.commons.impl.ExternalizerImpl.cfg.json

{
    "externalizer.domains": [
        "local https://stage-author.mysite.com",
        "author https://stage-author.mysite.com",
        "publish https://stage.mysite.com"
    ],
    "externalizer.encodedpath": true
}

2.3. Production Environment

File: ui.config/.../osgiconfig/config.prod/com.day.cq.commons.impl.ExternalizerImpl.cfg.json

{
    "externalizer.domains": [
        "local https://author.mysite.com",
        "author https://author.mysite.com",
        "publish https://www.mysite.com"
    ],
    "externalizer.encodedpath": true
}

2.4. Giải thích từng property

externalizer.domains

Format mỗi entry: "<tên-domain> <scheme>://<host>[:<port>][/<contextPath>]"

Tên domainDùng với methodÝ nghĩa
localexternalizer.externalLink(...)URL của instance hiện tại (author hoặc publish)
authorexternalizer.authorLink(...)URL của Author instance
publishexternalizer.publishLink(...)URL của Publish instance

Context path (/contextPath) chỉ thêm khi AEM được deploy dưới sub-path.
Ví dụ deploy AEM trong Docker với nginx proxy: "publish https://www.mysite.com/site" → URL ra sẽ là https://www.mysite.com/site/content/...
Môi trường AEM mặc định (root path) → không thêm context path.

externalizer.encodedpath

  • true — Encode các ký tự đặc biệt trong path (space → %20, v.v.). Luôn dùng true trên production.
  • false — Giữ nguyên path, có thể gây broken URL nếu path chứa ký tự đặc biệt.

3. Các method của Externalizer API

// 1. publishLink — Tạo URL absolute trỏ đến Publish instance
//    Dùng cho: Email links, SEO, Sitemap, Social meta
String url = externalizer.publishLink(resourceResolver, "/content/mysite/page.html");
// → https://www.mysite.com/content/mysite/page.html

// 2. authorLink — Tạo URL absolute trỏ đến Author instance
//    Dùng cho: Notification email gửi đến content editor, approval workflow
String url = externalizer.authorLink(resourceResolver, "/content/mysite/page.html");
// → https://author.mysite.com/content/mysite/page.html

// 3. externalLink — Dùng domain tùy chỉnh
//    Dùng cho: Multi-domain setup, custom domain mapping
String url = externalizer.externalLink(resourceResolver, "publish", "/content/mysite/page.html");
// → https://www.mysite.com/content/mysite/page.html

// 4. absoluteLink — Dùng scheme+host của request hiện tại (KHÔNG dùng config domain)
//    Dùng cho: Khi có HTTP request context và chỉ cần absolute của request đó
String url = externalizer.absoluteLink(request, request.getScheme(), "/content/mysite/page.html");

Lưu ý: Method publishLink và authorLink luôn ưu tiên hơn absoluteLink trong production vì chúng lấy domain từ OSGi config, không phụ thuộc vào host của HTTP request (có thể là internal load balancer IP).


4. Cách dùng trong Sling Model

Pattern chuẩn trong dự án

// File: core/src/main/java/com/mysite/core/models/NewsletterModel.java

@Getter
@Model(
    adaptables = SlingHttpServletRequest.class,
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class NewsletterModel {

    @OSGiService
    private Externalizer externalizer;          // Inject OSGi service

    @SlingObject
    private ResourceResolver resourceResolver;  // Cần thiết cho Externalizer

    @ScriptVariable
    private Page currentPage;                   // Trang hiện tại

    @ValueMapValue(name = "articlePath")
    private String articlePath;                 // Path được author chọn qua dialog

    private String absolutePublishUrl;

    @PostConstruct
    protected void init() {
        // Ưu tiên dùng articlePath nếu được set, fallback về path trang hiện tại
        String targetPath = (articlePath != null && articlePath.startsWith("/content"))
                ? articlePath
                : (currentPage != null ? currentPage.getPath() : null);

        if (targetPath != null) {
            // Append .html extension trước khi externalize
            String pathWithExtension = targetPath + ".html";
            this.absolutePublishUrl = externalizer.publishLink(resourceResolver, pathWithExtension);
        }
    }
}

Các lưu ý khi dùng trong Sling Model

// ✅ ĐÚNG: adaptables = SlingHttpServletRequest.class cho phép inject @ScriptVariable
@Model(adaptables = SlingHttpServletRequest.class, ...)

// ❌ SAI: adaptables = Resource.class không hỗ trợ inject @ScriptVariable (currentPage)
@Model(adaptables = Resource.class, ...)

// ✅ ĐÚNG: Luôn append extension trước khi externalize
String url = externalizer.publishLink(resolver, path + ".html");
// → https://www.mysite.com/content/mysite/page.html

// ❌ SAI: Externalize path không có extension → AEM Dispatcher có thể block
String url = externalizer.publishLink(resolver, path);
// → https://www.mysite.com/content/mysite/page  (Dispatcher thường block non-extension URL)

// ✅ ĐÚNG: Validate path trước khi externalize
if (path != null && path.startsWith("/content")) {
    url = externalizer.publishLink(resolver, path + ".html");
}

// ❌ SAI: Externalize path ngoài /content (external URL, fragment path...)
url = externalizer.publishLink(resolver, "https://external.com"); // → sẽ ra URL sai

5. Dùng trong workflow ngoài request context

Khi chạy trong OSGi Service, Scheduler, Workflow — không có HTTP request → không thể dùng absoluteLink. Chỉ dùng publishLink / authorLink:

@Component(service = NewsletterEmailService.class)
public class NewsletterEmailService {

    @Reference
    private Externalizer externalizer;

    @Reference
    private ResourceResolverFactory resolverFactory;

    public String buildEmailLink(String contentPath) {
        // Mở service ResourceResolver (không có request)
        Map<String, Object> param = new HashMap<>();
        param.put(ResourceResolverFactory.SUBSERVICE, "newsletter-service");

        try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(param)) {
            return externalizer.publishLink(resolver, contentPath + ".html");
            // → https://www.mysite.com/content/mysite/en/article.html
        } catch (LoginException e) {
            log.error("Cannot open service resolver", e);
            return null;
        }
    }
}

Cần cấu hình Service User Mapping trong OSGi để newsletter-service có quyền đọc /content.


6. Dùng trong HTL (Sightly)

<!--/* Khởi tạo model */-->
<sly data-sly-use.newsletter="com.mysite.core.models.NewsletterModel"/>

<!--/* Hiển thị URL — dùng data-sly-test để tránh render khi null */-->
<div data-sly-test="${newsletter.absolutePublishUrl}">
    Publish URL: ${newsletter.absolutePublishUrl}
</div>

<!--/* Dùng trong href — context='uri' để encode đúng */-->
<a href="${newsletter.absolutePublishUrl @ context='uri'}">Read article</a>

<!--/* Dùng trong JSON-LD — context='unsafe' vì là JSON trong script tag */-->
<script type="application/ld+json">${schema.jsonLd @ context='unsafe'}</script>

HTL Context quan trọng:

  • @ context='uri' — Dùng cho href, src, action. Encode URL theo RFC 3986.
  • @ context='unsafe' — Dùng cho JSON trong <script> tag. Không encode.
  • Mặc định (không có context) — HTML-encode, an toàn cho text content.

7. Checklist production deployment

  • Không hardcode domain trong code Java hoặc HTL. Luôn dùng Externalizer.
  • Tạo file config.prod/com.day.cq.commons.impl.ExternalizerImpl.cfg.json với domain thật.
  • Dùng https:// cho tất cả domain trên production.
  • Kiểm tra AEM RunMode: -Dsling.run.modes=author,prod (author) / -Dsling.run.modes=publish,prod (publish).
  • Kiểm tra output URL tại: AEM → Tools → Operations → Web Console → Configuration → Day CQ Link Externalizer.
  • externalizer.encodedpath = true trên tất cả môi trường production.
  • Nếu dùng CDN / custom domain, publish domain phải là CDN domain (không phải IP nội bộ).

8. Kiểm tra nhanh tại AEM Console

  1. Truy cập: http://localhost:4502/system/console/configMgr
  2. Tìm: "Day CQ Link Externalizer"
  3. Xác nhận giá trị Domains đúng với môi trường.

Hoặc test qua Groovy Console (nếu đã cài AEM Groovy Console):

def externalizer = getService(com.day.cq.commons.Externalizer)
def resolver = resourceResolver
println externalizer.publishLink(resolver, "/content/mysite/en.html")
// Expected: https://www.mysite.com/content/mysite/en.html