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:
- Truy cập Chế độ Admin > Thẻ Admin.
- Chọn Scheduled Jobs.
- 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).
- Đả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
- Nguyên nhân: Tác vụ đã vượt quá giới hạn thông lượng hoặc tốc độ được định nghĩa bởi API AI bên ngoài (ví dụ: Azure OpenAI tokens mỗi phút).
- Giải pháp: Xác minh hằng số
RateLimitDelayMstrongBlogSeoEnrichmentJob.cslà đủ (chúng ta đã sử dụng 30 giây). Nếu khối lượng tác vụ cao, hãy cân nhắc tăng độ trễ hoặc triển khai cơ chế thử lại (backoff retry) tinh vi hơn trong quá trình triển khaiIGinbokAIService.
Vấn đề: Nhánh Tiếng Việt không được làm giàu hoặc lưu
- Nguyên nhân: Tác vụ không thể truy xuất nhánh nội dung đã bản địa hóa, hoặc nội dung chưa được xuất bản/kiểm tra ở Tiếng Việt.
- Giải pháp: Đảm bảo
_contentLoader.TryGet<BlogDetailPage>(contentLink, culture, out var page)xử lý đúng việc truy xuất nội dung cho các phiên bản đã bản địa hóa chưa được xuất bản nếu cần. Xác minh nhánh Tiếng Việt thực sự tồn tại trước khi cố gắng làm giàu.