Development

Optimizely: Tự động hóa SEO bằng AI qua Tác vụ Định kỳ

By Ginbok6 min read

Việc duy trì siêu dữ liệu SEO (SEO metadata) chất lượng cao trên hàng trăm hoặc hàng nghìn trang nội dung trong hệ thống Optimizely đa ngôn ngữ có thể là một gánh nặng lớn cho biên tập viên. Họ thường bỏ qua việc điền các trường ít quan trọng hơn như Từ khóa Meta hoặc Thẻ Blog, dẫn đến hiệu suất tìm kiếm không đồng nhất.

Hướng dẫn này trình bày chi tiết cách tận dụng khung tác vụ định kỳ của Optimizely cùng với một dịch vụ AI nội bộ (ví dụ: GPT thông qua Azure OpenAI hoặc tương tự) để tự động kiểm tra và làm giàu các thuộc tính SEO bị thiếu ở cả nhánh nội dung chính (Tiếng Anh) và nhánh phụ (Tiếng Việt).

Điều kiện Tiên quyết và Thiết lập

Chúng tôi giả định bạn có một giải pháp Optimizely CMS 12 tiêu chuẩn trên nền tảng .NET 8 và một dịch vụ tích hợp AI hiện có được định nghĩa bằng giao diện IGinbokAIService.

1. Mở rộng Giao diện Dịch vụ AI

Để xử lý cụ thể việc tạo SEO, chúng ta định nghĩa một hợp đồng chuyên biệt trả về dữ liệu SEO có cấu trúc, đảm bảo mô hình AI chỉ tập trung vào các thuộc tính cần thiết thay vì tạo ra toàn bộ bản nháp nội dung.

// MyProject.Model/Services/IGinbokAIService.cs
    public interface IGinbokAIService
    {
        Task<SeoData> GenerateSeoMetadataAsync(string contentBody, string languageCode);
    }

    public class SeoData
    {
        public string MetaTitle { get; set; }
        public string MetaDescription { get; set; }
        public string MetaKeywords { get; set; }
        public string BlogCategory { get; set; }
        public string BlogTags { get; set; }
    }

2. Xác định Mô hình Nội dung

Tác vụ định kỳ nhắm mục tiêu đến các đối tượng BlogDetailPage, chúng phải có các thuộc tính SEO cần thiết được định nghĩa là CultureSpecific để hỗ trợ phân nhánh ngôn ngữ.

// MyProject.Model/Content/BlogDetailPage.cs
    [ContentType(DisplayName = "Blog Detail Page", GUID = "5D24D857-...", GroupName = "Blog")]
    public class BlogDetailPage : PageData
    {
        [CultureSpecific]
        [Display(Name = "Nội dung Trang", GroupName = SystemTabNames.Content)]
        public virtual XhtmlString MainBody { get; set; }

        [CultureSpecific]
        [Display(Name = "Tiêu đề Meta (SEO)", GroupName = SystemTabNames.SEO)]
        public virtual string MetaTitle { get; set; }
        
        // ... Các thuộc tính MetaDescription, MetaKeywords, BlogCategory, BlogTags tương tự
    }

Tạo Tác vụ Định kỳ BlogSeoEnrichmentJob

Tác vụ định kỳ phải kế thừa từ ScheduledJobBase và được cấu hình để hỗ trợ dừng lại, vì các lệnh gọi API AI có thể kéo dài.

// MyProject.Web/ScheduledJobs/BlogSeoEnrichmentJob.cs
    using EPiServer.PlugIn;
    using EPiServer.Scheduler;
    // ... Khai báo using cần thiết

    [ScheduledPlugIn(DisplayName = "Blog SEO Enrichment Job", 
                     Default = false, 
                     SortIndex = 100, 
                     IsStoppable = true)]
    public class BlogSeoEnrichmentJob : ScheduledJobBase
    {
        private readonly IContentLoader _contentLoader;
        private readonly IContentRepository _contentRepository;
        private readonly IGinbokAIService _aiService;
        
        private const int RateLimitDelayMs = 30000; // Giới hạn 30 giây giữa các lệnh gọi AI

        public BlogSeoEnrichmentJob(IContentLoader contentLoader, IContentRepository contentRepository, IGinbokAIService aiService) : base(contentLoader)
        {
            _contentLoader = contentLoader;
            _contentRepository = contentRepository;
            _aiService = aiService;
            Is  StopSignalled = false;
        }

        public override string Execute()
        {
            Set		Status("Bắt đầu tác vụ làm giàu SEO blog...");
            var blogRoot = ContentReference.StartPage; // Thay thế bằng tham chiếu gốc Blog List thực tế
            var blogs = _contentLoader.GetChildren<BlogDetailPage>(blogRoot);
            int pagesProcessed = 0;

            foreach (var blogPage in blogs)
            {
                if (IsStopSignalled)
                {
                    return "Tác vụ đã dừng thủ công.";
                }

                // 1. Xử lý Ngôn ngữ Chính (English)
                Task.WaitAll(ProcessLanguageBranch(blogPage.ContentLink, "en", true));
                
                // 2. Xử lý Ngôn ngữ Tiếng Việt (Vi)
                Task.WaitAll(ProcessLanguageBranch(blogPage.ContentLink, "vi", false));

                pagesProcessed++;
                SetStatus($"Đã xử lý {pagesProcessed} blog. Gần nhất: {blogPage.Name}. Đang chờ giới hạn tốc độ...");
                
                // Tuân thủ giới hạn tốc độ
                Task.Delay(RateLimitDelayMs).Wait(); 
            }

            return $"Tác vụ hoàn thành. Đã xử lý thành công {pagesProcessed} blog.";
        }
        
        public override void Stop()
        {
            IsStopSignalled = true;
        }

        private async Task ProcessLanguageBranch(ContentReference contentLink, string languageCode, bool isMaster)
        {
            try
            {
                var culture = new System.Globalization.CultureInfo(languageCode);
                if (!_contentLoader.TryGet<BlogDetailPage>(contentLink, culture, out var page))
                {
                    if (!isMaster) return; 
                    page = _contentLoader.Get<BlogDetailPage>(contentLink); 
                }
                
                var clone = (BlogDetailPage)page.CreateWritableClone();
                bool needsSave = false;
                
                if (string.IsNullOrEmpty(clone.MetaTitle) || string.IsNullOrEmpty(clone.BlogTags))
                {
                    string contentBody = clone.MainBody?.ToHtmlString() ?? string.Empty;

                    if (!string.IsNullOrEmpty(contentBody))
                    {
                        var seoData = await _aiService.GenerateSeoMetadataAsync(contentBody, languageCode);

                        // Chỉ điền vào các trường bị thiếu
                        if (string.IsNullOrEmpty(clone.MetaTitle)) clone.MetaTitle = seoData.MetaTitle;
                        if (string.IsNullOrEmpty(clone.MetaDescription)) clone.MetaDescription = seoData.MetaDescription;
                        if (string.IsNullOrEmpty(clone.MetaKeywords)) clone.MetaKeywords = seoData.MetaKeywords;
                        if (string.IsNullOrEmpty(clone.BlogCategory)) clone.BlogCategory = seoData.BlogCategory;
                        if (string.IsNullOrEmpty(clone.BlogTags)) clone.BlogTags = seoData.BlogTags;

                        needsSave = true;
                    }
                }

                if (needsSave)
                {
                    // Sử dụng SaveAction.Publish để đảm bảo nội dung mới được áp dụng ngay lập tức
                    _contentRepository.Save(clone, SaveAction.Publish, AccessLevel.NoAccess);
                    ReportProgress(1, 1, $"Đã lưu thay đổi cho {clone.Name} ({languageCode}).");
                }
            }
            catch (Exception ex)
            {
                ReportProgress(0, 1, $"Lỗi xử lý trang {contentLink} ({languageCode}): {ex.Message}");
                // Quan trọng: Bắt lỗi để tác vụ có thể tiếp tục với trang tiếp theo
            }
        }
    }

Triển khai và Cấu hình

Sau khi biên dịch và triển khai BlogSeoEnrichmentJob.cs vào MyProject.Web/bin, tác vụ sẽ xuất hiện trong giao diện Quản trị Optimizely:

  1. Truy cập Chế độ Admin > Thẻ Admin.
  2. Chọn Scheduled Jobs.
  3. Tìm "Blog SEO Enrichment Job" và cấu hình lịch trình mong muốn (ví dụ: Hàng tuần, hoặc Giờ thấp điểm hàng ngày).
  4. Đảm bảo Tài khoản chạy tác vụ có quyền Xuất bản cần thiết trên cây nội dung.

Khắc phục Sự cố Thường gặp

Vấn đề: Lệnh gọi AI thất bại với lỗi 429 Too Many Requests

Vấn đề: Nhánh Tiếng Việt không được làm giàu hoặc lưu

#OptimizelyCMS#ScheduledJobs#AIService#SEOAutomation#DotNet8
← Back to Articles
Optimizely: Tự động hóa SEO bằng AI qua Tác vụ Định kỳ - Ginbok