CMS & Content Platforms

Tùy biến Editor cho Tag và Category trong Optimizely CMS 12 với Dojo

By Ginbok6 min read

Những người quản trị nội dung làm việc với Optimizely CMS 12 thường gặp phải một điểm yếu lớn trong quy trình làm việc: quản lý hệ thống phân loại (taxonomies). Các thuộc tính mặc định như [SelectMany][SelectOne] chỉ cung cấp danh sách thả xuống hoặc danh sách chọn cơ bản, thiếu sự linh hoạt cho các blog có số lượng tag hoặc category tăng trưởng nhanh. Biên tập viên thường bị mắc kẹt trong những danh sách dài vô tận, và tệ hơn, họ phải rời khỏi trang hiện tại để tạo một tag mới trong công cụ quản lý Category, làm gián đoạn luồng tư duy sáng tạo.

Trong bài phân tích kỹ thuật chuyên sâu này, chúng ta sẽ giải quyết vấn đề này bằng cách xây dựng các widget Dojo tùy chỉnh. Các widget này mang lại trải nghiệm chỉnh sửa hiện đại: chọn nhiều tag bằng checkbox và chọn một category bằng radio button, cả hai đều có tính năng thêm mới trực tiếp (inline add). Điều này có nghĩa là biên tập viên có thể tạo các mục phân loại mới ngay trong thuộc tính trang mà không cần rời khỏi trình chỉnh sửa.

Kiến trúc Hệ thống

Để triển khai giải pháp này, chúng ta cần một cầu nối giữa giao diện Optimizely CMS (Dojo/JavaScript) và Backend (C#/.NET 8). Kiến trúc của chúng ta dựa trên bốn lớp riêng biệt:

Bước 1: Triển khai REST API cho Phân loại

Bước đầu tiên là tạo một backend mạnh mẽ để xử lý các yêu cầu GET (lấy tag hiện có) và POST (tạo tag mới). Trong Optimizely CMS 12, chúng ta sử dụng ASP.NET Core Web API tiêu chuẩn. Chúng ta phải đảm bảo controller được bảo mật để chỉ các biên tập viên có quyền mới có thể truy cập.


[ApiController]
[Route("api/blog-taxonomy")]
[Authorize(Roles = "CmsEditors, CmsAdmins, WebEditors, WebAdmins, Administrators")]
public class BlogTaxonomyApiController : ControllerBase
{
    private readonly CategoryRepository _categoryRepository;

    public BlogTaxonomyApiController(CategoryRepository categoryRepository)
    {
        _categoryRepository = categoryRepository;
    }

    [HttpGet("tags")]
    public IActionResult GetTags()
    {
        var root = _categoryRepository.Get("Blog Tags");
        if (root == null) return Ok(new string[0]);

        return Ok(root.Categories
            .Where(c => c.Selectable)
            .Select(c => c.Name)
            .OrderBy(n => n).ToList());
    }

    [HttpPost("tags")]
    public IActionResult CreateTag([FromBody] CreateTaxonomyRequest request)
    {
        if (string.IsNullOrWhiteSpace(request?.Name))
            return BadRequest("Tên tag là bắt buộc.");

        var root = _categoryRepository.Get("Blog Tags");
        if (root == null) return NotFound("Không tìm thấy root 'Blog Tags'.");

        var existing = root.Categories
            .FirstOrDefault(c => c.Name.Equals(request.Name.Trim(), StringComparison.OrdinalIgnoreCase));

        if (existing != null)
            return Ok(new { name = existing.Name, message = "Đã tồn tại." });

        var newCat = new Category(root, request.Name.Trim())
        {
            Description = request.Name.Trim(),
            Selectable = true
        };
        _categoryRepository.Save(newCat);

        return Ok(new { name = newCat.Name, message = "Đã tạo." });
    }
}
    

Bước 2: Phát triển Tag Widget Chọn Nhiều (Dojo)

Dojo widget là cốt lõi của trải nghiệm người dùng. Chúng ta sử dụng dijit/_TemplatedMixin để định nghĩa cấu trúc HTML và epi/shell/widget/_ValueRequiredMixin để tích hợp mượt mà với hệ thống validation của CMS. Widget cần xử lý tải dữ liệu không đồng bộ và cập nhật giá trị thuộc tính theo định dạng mà CMS hiểu (thường là chuỗi phân cách bằng dấu phẩy cho multi-select).


define([
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/_base/xhr",
    "dojo/on",
    "dojo/query",
    "dijit/_Widget",
    "dijit/_TemplatedMixin",
    "epi/shell/widget/_ValueRequiredMixin"
], function (declare, lang, xhr, on, query, _Widget, _TemplatedMixin, _ValueRequiredMixin) {

    return declare("app/Editors/BlogTagEditor", [_Widget, _TemplatedMixin, _ValueRequiredMixin], {
        templateString:
            '<div class="blog-tag-editor">' +
                '<div style="display:flex;gap:6px;margin-bottom:10px;">' +
                    '<input data-dojo-attach-point="newTagInput" type="text" placeholder="Thêm tag mới..." ' +
                    'style="flex:1;padding:6px;border:1px solid #ccc;border-radius:4px;" />' +
                    '<button data-dojo-attach-point="addBtn" type="button" ' +
                    'style="padding:6px 12px;background:#0078d4;color:#fff;border:none;border-radius:4px;cursor:pointer;">+ Thêm</button>' +
                '</div>' +
                '<div data-dojo-attach-point="tagListContainer" style="max-height:220px;overflow-y:auto;border:1px solid #e0e0e0;padding:6px;background:#fafafa;">' +
                    '<span>Đang tải tag...</span>' +
                '</div>' +
            '</div>',

        value: null,

        postCreate: function () {
            this.inherited(arguments);
            on(this.addBtn, "click", lang.hitch(this, "_onAddTag"));
            this._loadTags();
        },

        _loadTags: function () {
            var self = this;
            xhr.get({
                url: "/api/blog-taxonomy/tags",
                handleAs: "json",
                load: function (data) {
                    self._tags = data || [];
                    self._renderCheckboxes();
                }
            });
        },

        _renderCheckboxes: function () {
            var selected = (this.value || "").split(",").map(s => s.trim()).filter(s => s);
            var html = "";
            this._tags.forEach(function(tag) {
                var checked = selected.indexOf(tag) >= 0 ? "checked" : "";
                html += '<label style="display:flex;align-items:center;gap:6px;padding:2px 0;cursor:pointer;">' +
                        '<input type="checkbox" class="bte-cb" value="' + tag + '" ' + checked + ' />' +
                        '<span>' + tag + '</span></label>';
            });
            this.tagListContainer.innerHTML = html;
            
            query(".bte-cb", this.tagListContainer).on("change", lang.hitch(this, "_onCheckboxChange"));
        },

        _onCheckboxChange: function () {
            var vals = [];
            query(".bte-cb:checked", this.tagListContainer).forEach(function(cb) {
                vals.push(cb.value);
            });
            this._set("value", vals.join(","));
            this.onChange(this.value);
        },

        _onAddTag: function () {
            var name = this.newTagInput.value.trim();
            if (!name) return;
            var self = this;
            xhr.post({
                url: "/api/blog-taxonomy/tags",
                postData: JSON.stringify({ Name: name }),
                handleAs: "json",
                headers: { "Content-Type": "application/json" },
                load: function (data) {
                    self.newTagInput.value = "";
                    var current = (self.value || "").split(",").filter(s => s.trim());
                    if (current.indexOf(data.name) < 0) current.push(data.name);
                    self._set("value", current.join(","));
                    self._loadTags();
                    self.onChange(self.value);
                }
            });
        },

        _setValueAttr: function (v) {
            this._set("value", v);
            if (this.tagListContainer) this._renderCheckboxes();
        },
        _getValueAttr: function () { return this.value; },
        onChange: function (v) {}
    });
});
    

Bước 3: Đăng ký với EditorDescriptors

Để các widget này xuất hiện trong CMS, chúng ta cần đăng ký qua EditorDescriptor. Điều này liên kết một UIHint với đường dẫn JS của widget.


[EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "BlogTagEditor")]
public class BlogTagEditorDescriptor : EditorDescriptor
{
    public BlogTagEditorDescriptor()
    {
        ClientEditingClass = "app/Editors/BlogTagEditor";
    }
}
    

Sau đó, áp dụng vào Model của trang:


[UIHint("BlogTagEditor")]
public virtual string BlogTags { get; set; }
    

Xử lý lỗi thường gặp

Kết luận

Việc thay thế các danh sách thả xuống tĩnh bằng các widget Dojo tương tác hỗ trợ REST API đã thay đổi hoàn toàn trải nghiệm quản lý phân loại blog. Biên tập viên giờ đây có thể quản lý tag và category ngay tại chỗ—không còn phải chuyển đổi ngữ cảnh hay điều hướng trang phức tạp. Đây là một mẫu thiết kế mạnh mẽ và có thể mở rộng cho bất kỳ thuộc tính phức tạp nào trong Optimizely CMS 12.

#optimizely#backend#frontend#workflow
← Back to Articles
Tùy biến Editor cho Tag và Category trong Optimizely CMS 12 với Dojo - Ginbok