> 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.

# Seo Ld Json

## Phương pháp triển khai chung

1. **Backend (Java / Sling Model):** Đọc các thuộc tính (properties) từ trang hiện tại (`currentPage`) hoặc cấu hình (Configuration), sau đó xây dựng một object đại diện cho Schema (ví dụ: `Article`, `BreadcrumbList`, `WebPage`).
2. **Serialization:** Chuyển đổi Object thành chuỗi JSON (sử dụng thư viện như `Gson` hoặc `Jackson`).
3. **Frontend (HTL / Sightly):** Gọi Sling Model từ component Page (ví dụ file `customheaderlibs.html` hoặc trực tiếp trong `<head>`) và in chuỗi JSON ra bên trong thẻ `<script type="application/ld+json">`.

***

## Bước 1: Tạo Data Transfer Object (DTO) cho Schema

Đầu tiên, tạo các lớp Java (POJO) để cấu trúc hóa dữ liệu theo chuẩn [Schema.org](https://schema.org/). Ví dụ cho loại `Article`.

```java
package com.yourproject.core.models.seo;

import com.google.gson.annotations.SerializedName;
import java.util.List;

public class ArticleSchema {

    @SerializedName("@context")
    private String context = "https://schema.org";

    @SerializedName("@type")
    private String type = "Article";

    private String headline;
    private String image;
    private String datePublished;
    private String dateModified;
    private Author author;

    // Getters and Setters ...
    
    public static class Author {
        @SerializedName("@type")
        private String type = "Person";
        private String name;
        
        // Getters and Setters ...
    }
}
```

## Bước 2: Xây dựng Sling Model lấy dữ liệu từ Page

Tạo Sling Model gắn vào Page component để đọc properties và map vào Object `ArticleSchema`.

```java
package com.yourproject.core.models;

import com.google.gson.Gson;
import com.yourproject.core.models.seo.ArticleSchema;
import com.day.cq.wcm.api.Page;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ScriptVariable;

import javax.annotation.PostConstruct;

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

    @ScriptVariable
    private Page currentPage;

    private String jsonLdString;

    @PostConstruct
    protected void init() {
        if (currentPage != null) {
            ArticleSchema schema = new ArticleSchema();
            
            // Lấy dữ liệu từ Page Properties của AEM
            String title = currentPage.getProperties().get("jcr:title", String.class);
            String publishDate = currentPage.getProperties().get("cq:lastModified", String.class);
            // ... lấy các property khác như author, image, v.v.

            schema.setHeadline(title != null ? title : currentPage.getName());
            schema.setDatePublished(publishDate);
            
            ArticleSchema.Author author = new ArticleSchema.Author();
            author.setName(currentPage.getProperties().get("authorName", "Admin"));
            schema.setAuthor(author);

            // Chuyển đổi Object thành JSON String
            Gson gson = new Gson();
            this.jsonLdString = gson.toJson(schema);
        }
    }

    public String getJsonLdString() {
        return jsonLdString;
    }
}
```

## Bước 3: Inject chuỗi JSON vào Frontend (HTL)

Trong cấu trúc Page component của bạn (ví dụ: `apps/yourproject/components/page`), tìm đến vị trí render phần `<head>` (có thể là `customheaderlibs.html`, `head.html` hoặc `headlibs.html`).

Sử dụng HTL để gọi Sling Model và in dữ liệu ra một cách an toàn (tránh bị escape các ký tự JSON).

```html
<!--/* /apps/yourproject/components/page/head.html */-->

<sly data-sly-use.seoModel="com.yourproject.core.models.SeoJsonLdModel"></sly>

<!--/* 
  Sử dụng context='unsafe' để HTL không escape chuỗi JSON (ví dụ các dấu ngoặc kép ""), 
  vì đây là thẻ script yêu cầu định dạng JSON nguyên bản.
*/-->
<sly data-sly-test="${seoModel.jsonLdString}">
    <script type="application/ld+json">
        ${seoModel.jsonLdString @ context='unsafe'}
    </script>
</sly>
```

## Cơ chế hoạt động: JSON-LD được sinh ra động hay lưu trữ (Persisted)?

Trong cách triển khai ở trên, chuỗi JSON-LD **KHÔNG** được lưu nguyên khối (persisted) trong JCR (Page Properties). Thay vào đó, nó được **sinh ra động (dynamic)**:

1. **Sinh ra động (On-the-fly):** Mỗi khi trang được yêu cầu render, Sling Model sẽ được gọi. Hàm `@PostConstruct` sẽ đọc các properties nhỏ lẻ đã lưu trong JCR của page (như `jcr:title`, `cq:lastModified`, tên tác giả, ảnh), sau đó tự động lắp ráp chúng thành Object Schema và parse ra chuỗi JSON.
2. **Tối ưu hiệu năng bằng Dispatcher:** Dù Sling Model gen code tự động, nhưng bạn không phải lo về hiệu năng. Mã HTML của trang (bao gồm thẻ `<script>` đã chứa chuỗi JSON-LD) sẽ được **AEM Dispatcher cache lại** thành file HTML tĩnh trên Web Server (như Apache/Nginx). Sling Model thực chất chỉ chạy 1 lần lúc trang bị "miss cache".
3. **Trường hợp muốn nhập tay (Manual):** Nếu business yêu cầu cho phép Content Author tự copy/paste đoạn script JSON-LD tùy ý, bạn có thể tạo thêm một trường Text Area trong Page Dialog (ví dụ lưu thành property `customJsonLd` trong JCR). Khi đó, HTML chỉ việc in trực tiếp cái property `customJsonLd` đó ra thẻ `<script>` mà không cần qua bước convert của Gson.

## Xử lý yêu cầu nghiệp vụ nâng cao (Inherit, Auto-fill Edit, Manual Inject)

Nếu thiết kế yêu cầu một trường `seoText` trong Page Properties thỏa mãn 3 điều kiện:

1. Nhập `"inherit"`: Tự động kế thừa JSON-LD từ page cha.
2. Tự động sinh khi mở Dialog (Auto-fill): Page thuộc loại schema được support, khi tạo page không sinh ra, nhưng lần đầu mở page lên edit thì sẽ lấy JSON cấu trúc từ model điền vào.
3. Nhập tùy ý (Manual Inject): Dùng luôn giá trị nhập vào mà không query thêm.

Dưới đây là giải pháp kiến trúc để đáp ứng requirement này:

### 1. Xử lý Logic render ở tầng Sling Model

Sling Model sẽ đọc trường `seoText` và quyết định output:

```java
@Model(adaptables = SlingHttpServletRequest.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SeoJsonLdModel {
    
    @ScriptVariable
    private Page currentPage;
    private String jsonLdString;

    @PostConstruct
    protected void init() {
        String seoText = currentPage.getProperties().get("seoText", String.class);

        if (org.apache.commons.lang3.StringUtils.isNotBlank(seoText)) {
            if ("inherit".equalsIgnoreCase(seoText.trim())) {
                // Yêu cầu 1: Kế thừa từ Parent Page
                Page parentPage = currentPage.getParent();
                if (parentPage != null) {
                    // Logic tìm jsonLdString của cha (có thể viết hàm đệ quy nếu cha cũng là "inherit")
                    this.jsonLdString = parentPage.getProperties().get("seoText", String.class);
                }
            } else {
                // Yêu cầu 3: User tự inject (Manual Inject) -> Dùng luôn không query
                this.jsonLdString = seoText;
            }
        } 
        // Nếu seoText rỗng -> Trả về rỗng (vì requirement bảo chỉ gen ra khi User mở Edit và Save lại).
    }

    public String getJsonLdString() {
        return jsonLdString;
    }
}
```

### 2. Giải pháp cho yêu cầu "Tự động gen JSON khi mở Page Edit lần đầu"

Vì JSON cần được lưu lại (persisted) vào `seoText` sau khi User tương tác với Page Properties, cách tốt nhất là kết hợp **Clientlibs (JavaScript)** và **Sling Servlet**:

- **Tạo một Custom Sling Servlet:** Viết một Servlet (phương thức GET) nhận đường dẫn page, sau đó chạy logic sinh JSON theo model (giống Bước 1 ở phần đầu) và trả về kết quả dạng String JSON.
- **Viết Clientlib bắt sự kiện Dialog:** Tạo một JS Clientlib thuộc category `cq.authoring.dialog`. JS này sẽ lắng nghe sự kiện khi Dialog Page Properties mở ra (`foundation-contentloaded`).
- **Logic trong JS:**
  - Tìm ô textarea của trường `seoText`. Kiểm tra nếu nó đang **trống** (chưa có data) và Page này thuộc loại Schema được hỗ trợ (có thể kiểm tra qua template path hoặc resourceType).
  - Gọi AJAX (GET) tới Servlet vừa tạo ở trên.
  - Lấy chuỗi JSON từ response và **tự động điền (val)** vào ô textarea `seoText`.
- **Kết quả:** Ngay khi Author mở Page Properties, họ sẽ thấy ô Seo Text tự động hiện ra một cấu trúc JSON chuẩn. Họ có thể kiểm tra, chỉnh sửa (Manual Inject) hoặc gõ chữ "inherit". Sau đó nhấn "Save & Close", chuỗi JSON này chính thức được lưu thành property `seoText` trong JCR.

Đây là phương pháp thân thiện với người dùng (UX) nhất vì Author luôn biết chính xác cái gì sắp được lưu vào page.

## Một số lưu ý quan trọng

1. **Context = 'unsafe' trong Sightly:** Mặc định HTL sẽ escape các thẻ HTML và ký tự đặc biệt để chống XSS. Khi in output JSON vào thẻ script, bạn phải sử dụng `${variable @ context='unsafe'}` để nội dung JSON được giữ nguyên vẹn.
2. **Sử dụng Core Components:** Nếu bạn sử dụng AEM Core Components mới nhất, class `Page` model (`com.adobe.cq.wcm.core.components.models.Page`) đã hỗ trợ sẵn việc render một số meta tags và có thể customize thông qua tính năng [Data Layer](https://github.com/adobe/aem-core-wcm-components/tree/main/content/src/content/jcr_root/apps/core/wcm/components/page/v2/page). Tuy nhiên, đối với các loại schema phức tạp (như Product, FAQPage, Event), việc tự viết custom Sling Model là cần thiết.
3. **Thư viện Gson / Jackson:** Trong AEM OSGi environment, `Gson` hoặc `Jackson` thường được dùng để convert object sang json. Jackson thường có hiệu năng cao hơn và được đi kèm trong các version AEM mới thông qua bundle của Sling.
4. **Validation:** Sau khi publish trang, hãy sử dụng công cụ [Rich Results Test](https://search.google.com/test/rich-results) hoặc [Schema Markup Validator](https://validator.schema.org/) của Google để kiểm tra xem đoạn script LD+JSON được generate ra có hợp lệ hay không.
