AI & Automation

Tự động hóa Metadata SEO bằng AI trong Optimizely CMS 12

By Ginbok7 min read

Trong các dự án CMS quy mô lớn như CmsIv, việc kiểm định nội dung là một thách thức không ngừng. Khi đội ngũ biên tập thay đổi theo thời gian, các bài viết cũ thường thiếu siêu dữ liệu (metadata) SEO chuẩn hóa, chẳng hạn như phân loại chủ đề và thẻ hash tag. Việc cập nhật thủ công hàng trăm hoặc hàng nghìn trang cũ là không thực tế. Bài viết này chi tiết cách tự động hóa quy trình này trong Optimizely CMS 12 bằng một công việc lập lịch (scheduled job) quét nội dung và tích hợp dịch vụ AI mạnh mẽ để tạo metadata. Bằng cách tận dụng .NET 8 và các API AI hiện đại, chúng ta có thể giảm đáng kể công sức thủ công trong khi cải thiện khả năng tìm thấy trên các công cụ tìm kiếm.

Điều kiện tiên quyết: Chuẩn bị Content Model

Trước khi triển khai job, hãy đảm bảo loại nội dung mục tiêu—trong trường hợp này là BlogItemPage—được cấu hình để lưu trữ metadata do AI tạo ra. Chúng ta giả định một cấu trúc đơn giản được định nghĩa trong CmsIv.Model/Pages/BlogItemPage.cs. Việc chuẩn bị này rất quan trọng vì hệ thống model định kiểu mạnh của Optimizely cho phép dịch vụ AI ánh xạ trực tiếp kết quả đầu ra vào các thuộc tính CMS.

using EPiServer.DataAnnotations;
using EPiServer.SpecializedProperties;
using System.ComponentModel.DataAnnotations;

[ContentType(
    DisplayName = "Blog Item Page",
    GUID = "8e1f5b72-2a4d-4c8e-9f3a-123456789abc",
    Description = "Trang bài viết hoặc blog cụ thể."
)]
public class BlogItemPage : PageData
{
    [CultureSpecific]
    [Display(Name = "Nội dung chính", GroupName = SystemTabNames.Content)]
    public virtual XhtmlString MainBody { get; set; }

    // Các thuộc tính mục tiêu để AI cập nhật
    [Display(Name = "Danh mục", GroupName = SystemTabNames.Content)]
    public virtual CategoryList Categories { get; set; }

    [Display(Name = "Tags do AI tạo (CSV)", GroupName = SystemTabNames.Content)]
    public virtual string AiTags { get; set; }
}

Thiết kế Abstraction cho AI Service

Để giữ cho logic CMS sạch sẽ và dễ kiểm thử, tương tác thực tế với các API AI bên ngoài (như OpenAI, Azure AI Services) nên được đóng gói trong một lớp dịch vụ (service layer) riêng biệt. Dịch vụ này nhận nội dung thô và trả về metadata có cấu trúc. Việc tách rời logic AI giúp bạn có thể chuyển đổi nhà cung cấp (ví dụ từ OpenAI sang Claude) mà không cần sửa đổi các scheduled job cốt lõi của CMS.

Giao diện AI Metadata Generator

Định nghĩa hợp đồng cho dịch vụ trong CmsIv.Web/Services/IMetadataGeneratorService.cs:

using EPiServer.SpecializedProperties;

public interface IMetadataGeneratorService
{
    /// <summary>
    /// Tạo metadata có cấu trúc (tags và categories) từ nội dung.
    /// </summary>
    /// <param name="contentBodyText">Nội dung văn bản thuần của bài viết.</param>
    /// <returns>Một tuple chứa tags (chuỗi CSV) và danh mục.</returns>
    Task<(string Tags, CategoryList Categories)> GenerateMetadataAsync(string contentBodyText);
}

Lưu ý: Việc triển khai MetadataGeneratorService bao gồm các lệnh gọi API, xử lý khóa bảo mật và phân tích cú pháp JSON. Để tương thích với Optimizely, AI phải trả về ID danh mục hoặc tên khớp chính xác với phân loại (taxonomy) đã định nghĩa trong CMS của bạn.

Triển khai Scheduled Cleanup Job

Logic cốt lõi nằm trong một Scheduled Job của Optimizely. Job này lặp qua cây nội dung, xác định các trang thiếu metadata, lấy dữ liệu cần thiết bằng dịch vụ AI và xuất bản nội dung đã cập nhật. Sử dụng lớp ScheduledJobBase cho phép job được theo dõi và kích hoạt thủ công từ giao diện Admin.

Định nghĩa Scheduled Job

Tạo lớp job trong CmsIv.Web/Jobs/AiMetadataCleanupJob.cs:

using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.Logging;
using EPiServer.PlugIn;
using EPiServer.Scheduler;
using CmsIv.Web.Services;
using CmsIv.Model.Pages;

[ScheduledJob(
    DisplayName = "AI Metadata Cleanup Job",
    DefaultEnabled = false,
    IntervalLength = 3600000, 
    SortIndex = 1000
)]
public class AiMetadataCleanupJob : ScheduledJobBase
{
    private readonly IContentLoader _contentLoader;
    private readonly IContentRepository _contentRepository;
    private readonly IMetadataGeneratorService _metadataGenerator;
    private static readonly ILogger Logger = LogManager.GetLogger();

    public AiMetadataCleanupJob(
        IContentLoader contentLoader,
        IContentRepository contentRepository,
        IMetadataGeneratorService metadataGenerator)
    {
        _contentLoader = contentLoader;
        _contentRepository = contentRepository;
        _metadataGenerator = metadataGenerator;
    }

    public override string Execute()
    {
        OnStatusChanged("Bắt đầu dọn dẹp metadata bằng AI...");
        var processedCount = 0;

        var blogRoot = new ContentReference(100); // ID của trang danh sách Blog

        var descendants = _contentLoader.GetDescendents(blogRoot)
            .Select(id => _contentLoader.Get<IContent>(id))
            .OfType<BlogItemPage>();

        foreach (var blogPage in descendants)
        {
            if (IsStopSignalled)
            {
                return $"Job bị dừng thủ công. Đã xử lý {processedCount} trang.";
            }

            bool metadataMissing = string.IsNullOrWhiteSpace(blogPage.AiTags) || 
                                   (blogPage.Categories == null || blogPage.Categories.Count == 0);

            if (metadataMissing)
            {
                OnStatusChanged($"Đang xử lý: {blogPage.Name}");

                var contentText = blogPage.MainBody.ToHtmlString().StripHtml(); 
                
                try
                {
                    var (tags, categories) = _metadataGenerator.GenerateMetadataAsync(contentText).GetAwaiter().GetResult();

                    if (!string.IsNullOrEmpty(tags) || (categories != null && categories.Count > 0))
                    {
                        var clone = (BlogItemPage)blogPage.CreateWritableClone();
                        clone.AiTags = tags;
                        clone.Categories = categories;

                        _contentRepository.Save(clone, EPiServer.DataAccess.SaveAction.Publish, EPiServer.Security.AccessLevel.NoAccess);
                        processedCount++;
                    }
                }
                catch (Exception ex)
                {
                    Logger.Error($"Lỗi khi xử lý trang {blogPage.Name}", ex);
                    continue; 
                }
            }
        }

        return $"Job hoàn tất. Đã cập nhật {processedCount} bài viết.";
    }
}

Tối ưu hóa quan trọng: Loại bỏ mã HTML

Các dịch vụ AI thường yêu cầu đầu vào là văn bản sạch để xử lý ngữ nghĩa chính xác. Bạn phải đảm bảo nội dung XhtmlString được chuyển đổi sang văn bản thuần trước khi gửi đến API. Sử dụng Regex là cách tiếp cận phổ biến, nhưng với HTML phức tạp, hãy cân nhắc sử dụng HtmlAgilityPack.

public static class XhtmlStringExtensions
{
    public static string StripHtml(this string html)
    {
        if (string.IsNullOrEmpty(html)) return string.Empty;
        var stripped = System.Text.RegularExpressions.Regex.Replace(html, "<[^>]*>", string.Empty);
        return System.Net.WebUtility.HtmlDecode(stripped).Trim();
    }
}

Lưu ý về Bảo mật và Quyền hạn

Khi chạy các công việc lập lịch có chỉnh sửa nội dung, job thực thi dưới ngữ cảnh hệ thống. Vì job sử dụng IContentRepository.Save(..., SaveAction.Publish), nó sẽ bỏ qua quy trình biên tập thông thường. Chúng ta sử dụng AccessLevel.NoAccess để đảm bảo job không bị lỗi do kiểm tra quyền hạn, nhưng điều này có nghĩa là logic phải cực kỳ chính xác để tránh làm hỏng dữ liệu thực tế. Luôn kiểm tra trên môi trường staging trước.

Giải quyết các vấn đề thường gặp

Vấn đề 1: Suy giảm hiệu suất khi quét dữ liệu

Nguyên nhân: IContentLoader.GetDescendents lấy mọi tham chiếu nội dung dưới gốc. Trong các dự án có hơn 10.000 trang, điều này tiêu tốn rất nhiều bộ nhớ và CPU.

Giải pháp: Sử dụng Optimizely Search & Navigation (Find) để lọc ban đầu. Điều này cho phép bạn chỉ lấy các trang thực sự thiếu metadata, giảm tải cho máy chủ ứng dụng.

Vấn đề 2: Giới hạn Token và Chi phí AI

Nguyên nhân: Các bài blog dài có thể vượt quá giới hạn "Context Window" của các model AI như GPT-4, dẫn đến lỗi API hoặc chi phí cao.

Giải pháp: Triển khai cắt bớt văn bản hoặc tóm tắt trước khi gửi dữ liệu cho AI. Thông thường, 1.000 từ đầu tiên là đủ để AI xác định chính xác danh mục và thẻ.

Tự động hóa vệ sinh nội dung bằng AI là một bước tiến đáng kể trong việc duy trì tiêu chuẩn SEO cao trên các hệ thống Optimizely lớn. Bằng cách tận dụng sự mạnh mẽ của scheduled jobs trong .NET 8, các dự án như CmsIv có thể đảm bảo mọi nội dung cũ đều có thể tìm thấy và được phân loại đúng cách.

#ai#automation#optimizely#seo#workflow#llm
← Back to Articles