AI & Automation

Tự Động Hóa Blog trong Optimizely CMS 12 với Google Gemini API

By Ginbok9 min read

Mở rộng nội dung chất lượng cao, chính xác về mặt kỹ thuật là một trong những thách thức lớn nhất đối với các nhóm nền tảng. Việc duy trì tính nhất quán, đảm bảo tuân thủ SEO và giữ cho quy trình nội dung luôn đầy đủ đòi hỏi nỗ lực thủ công đáng kể từ các chuyên gia miền.

Đối với dự án CmsIv, mục tiêu của chúng tôi là loại bỏ hoàn toàn nút thắt cổ chai về nội dung này. Chúng tôi đã thiết kế một giải pháp tận dụng khả năng tự động hóa mạnh mẽ của Optimizely CMS 12 và chức năng đầu ra có cấu trúc (structured output) của Google Gemini API để triển khai quy trình tạo blog tự động 'Set and Forget'.

Bài viết này trình bày chi tiết kiến trúc cần thiết để biến các ý tưởng nội dung đơn giản thành các thể hiện BlogDetailPage được xuất bản hoàn chỉnh, tối ưu hóa SEO mà không cần sự can thiệp của con người.

Giải pháp: Kiến trúc Hàng đợi Prompt CmsIv

Để đảm bảo biên tập viên có thể dễ dàng thêm ý tưởng nội dung mà không kích hoạt các lệnh gọi API đồng bộ, tức thời (có thể dẫn đến hết thời gian chờ của biên tập viên), chúng tôi đã tách rời đầu vào prompt khỏi quá trình tạo bằng cách sử dụng mô hình nội dung dựa trên hàng đợi đơn giản.

Tách Rời bằng Prompt Container

Chúng tôi định nghĩa một PromptIdeaBlock cơ bản, cho phép biên tập viên cung cấp yêu cầu kỹ thuật chi tiết. Các block này được đặt trong một ContentArea trên một trang quản trị được chỉ định (trang "Generator Queue Page").

// CmsIv.Model/Blocks/PromptIdeaBlock.cs
[ContentType(DisplayName = "Prompt Idea", GUID = "153B83F3-E893-4E80-9A82-C93AC3CF2022", 
    Description = "Một ý tưởng duy nhất để Gemini AI xử lý.")]
public class PromptIdeaBlock : BlockData
{
    [CultureSpecific]
    [Display(Name = "Ý tưởng Prompt Kỹ thuật")]
    [UIHint(UIHint.Textarea)]
    public virtual string PromptText { get; set; }

    [ScaffoldColumn(false)]
    public virtual bool IsProcessed { get; set; } = false;
}

Cờ IsProcessed, mặc dù không hoàn toàn cần thiết cho quá trình xử lý hàng đợi, nhưng ngăn công việc xử lý lại các block đã thất bại trong quá trình gọi API hoặc đã được xử lý một phần, đảm bảo hành vi idempotent.

Xác định Đầu ra có Cấu trúc cho Gemini

Thách thức lớn nhất trong việc tạo nội dung bằng AI là độ tin cậy. Chúng tôi không thể chấp nhận việc AI trả về văn bản không có cấu trúc mà sau đó chúng tôi phải phân tích cú pháp (parse) bằng regex. Chúng tôi sử dụng khả năng của SDK Google.GenAI để bắt buộc một schema đầu ra JSON cụ thể.

Chúng tôi đã định nghĩa một lớp C# mục tiêu ánh xạ hoàn hảo với các thuộc tính BlogDetailPage của Optimizely (đặc biệt là đối với các trường SEO).

// CmsIv.Web/Services/Gemini/GeminiPostResult.cs
public class GeminiPostResult
{
    public string SeoTitle { get; set; }
    public string SeoDescription { get; set; }
    public List<string> Keywords { get; set; }
    public string PostContent { get; set; } 
    // Lưu ý: PostContent phải là HTML thuần túy vì chúng tôi ánh xạ nó sang XhtmlString
}

Lớp C# này sau đó được chuyển đổi thành JSON Schema cần thiết được sử dụng trong lệnh gọi API:

/* Được định nghĩa ngầm hoặc tường minh khi gọi Gemini API */
{
  "type": "object",
  "properties": {
    "SeoTitle": { "type": "string", "description": "Một tiêu đề được tối ưu hóa cho công cụ tìm kiếm." },
    "SeoDescription": { "type": "string", "description": "Một mô tả meta, 150-160 ký tự." },
    "Keywords": { "type": "array", "items": { "type": "string" } },
    "PostContent": { "type": "string", "description": "Nội dung bài viết blog đầy đủ, được định dạng hoàn toàn bằng HTML (sử dụng các thẻ h2, p, ul, pre)." }
  },
  "required": ["SeoTitle", "SeoDescription", "PostContent"]
}

Động Cơ: Triển khai BlogPostGeneratorJob

Công việc nặng nhọc được thực hiện bởi một Optimizely Scheduled Job tiêu chuẩn, được đặt lịch chạy cứ sau bốn giờ.

// CmsIv.Web/ScheduledJobs/BlogPostGeneratorJob.cs
[ScheduledPlugIn(DisplayName = "Trình tạo Bài viết Blog", 
    Description = "Xử lý các prompt đã xếp hàng thông qua Google Gemini và xuất bản các bài viết blog mới.")]
public class BlogPostGeneratorJob : ScheduledJobBase
{
    private readonly IContentRepository _contentRepository;
    private readonly IContentLoader _contentLoader;
    private readonly ILogger<BlogPostGeneratorJob> _logger;
    private readonly GenerativeModel _geminiModel;

    private const int GeneratorQueuePageId = 123; // ID của trang chứa ContentArea

    public BlogPostGeneratorJob(
        IContentRepository contentRepository, 
        IContentLoader contentLoader,
        ILogger<BlogPostGeneratorJob> logger)
    {
        _contentRepository = contentRepository;
        _contentLoader = contentLoader;
        _logger = logger;
        
        // Khởi tạo mô hình Gemini (Khóa API thường được tải từ Configuration/Secrets)
        _geminiModel = new GenerativeModel(
            model: "gemini-2.5-pro", 
            apiKey: Environment.GetEnvironmentVariable("GEMINI_API_KEY"));
    }

    public override string Execute()
    {
        var blocks = GetUnprocessedPromptBlocks();
        if (!blocks.Any())
        {
            return "Không tìm thấy prompt mới nào trong hàng đợi.";
        }

        int publishedCount = 0;
        foreach (var block in blocks)
        {
            try
            {
                // Sử dụng .Result để xử lý bất đồng bộ trong bối cảnh đồng bộ của ScheduledJobBase
                var result = Task.Run(() => GenerateAndPublishPost(block)).Result;
                if (result)
                {
                    MarkBlockAsProcessed(block);
                    publishedCount++;
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"Lỗi xử lý prompt: {block.PromptText}");
            }
        }

        return $"Công việc hoàn thành. Đã xuất bản thành công {publishedCount} bài viết blog mới.";
    }
}

Tích hợp Google.GenAI SDK

Logic cốt lõi liên quan đến việc xây dựng prompt, bao gồm các hướng dẫn chi tiết về chất lượng kỹ thuật mong muốn và yêu cầu nghiêm ngặt về đầu ra JSON có cấu trúc.

// Phương thức Helper trong BlogPostGeneratorJob
private async Task<GeminiPostResult> GeneratePostContent(string userPrompt)
{
    var systemInstruction = "Bạn là một chuyên gia viết blog kỹ thuật .NET 8 cho Optimizely CMS. Phản hồi của bạn PHẢI là một đối tượng JSON duy nhất tuân thủ schema bắt buộc. Đảm bảo PostContent được định dạng tốt, là HTML hoàn chỉnh.";

    var config = new GenerateContentConfig
    {
        SystemInstruction = systemInstruction,
        ResponseMimeType = "application/json",
        ResponseSchema = Schema.FromType<GeminiPostResult>() // Bắt buộc cấu trúc
    };

    var response = await _geminiModel.GenerateContentAsync(
        new List<Content> { new UserContent(userPrompt) },
        config);

    if (string.IsNullOrWhiteSpace(response.Text))
    {
        throw new InvalidOperationException("Gemini đã trả về nội dung rỗng.");
    }

    // Quá trình Deserialization diễn ra tại đây
    return JsonSerializer.Deserialize<GeminiPostResult>(response.Text, 
        new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}

Tạo và Xuất bản Nội dung Optimizely

Sau khi chúng tôi có GeminiPostResult được cấu trúc đáng tin cậy, việc ánh xạ nó tới BlogDetailPage mới và xuất bản trở nên đơn giản bằng cách sử dụng IContentRepository.

// Phương thức Helper trong BlogPostGeneratorJob
private bool GenerateAndPublishPost(PromptIdeaBlock block)
{
    var postData = GeneratePostContent(block.PromptText).GetAwaiter().GetResult();
    var parentLink = new ContentReference(124); // Tham chiếu đến trang Blog Landing Page

    // 1. Tạo một bản sao có thể ghi (writable clone) của instance trang mới
    var newPost = _contentRepository.GetDefault<BlogDetailPage>(parentLink);
    
    // Đặt các thuộc tính Optimizely bắt buộc
    newPost.Name = postData.SeoTitle; 
    newPost.URLSegment = postData.SeoTitle.ToLowerInvariant().Replace(" ", "-");

    // 2. Ánh xạ kết quả AI sang các thuộc tính Optimizely
    newPost.SeoTitle = postData.SeoTitle;
    newPost.SeoDescription = postData.SeoDescription;
    newPost.SeoKeywords = string.Join(", ", postData.Keywords);
    newPost.MainBody = new XhtmlString(postData.PostContent);
    newPost.Author = "CmsIv Automation"; // Đặt tác giả tiêu chuẩn

    // 3. Lưu và Xuất bản trong một giao dịch
    _contentRepository.Save(newPost, SaveAction.Publish);
    _logger.LogInformation($"Đã xuất bản bài viết mới thành công: {newPost.Name}");

    return true;
}

Khắc phục sự cố & Thực tiễn tốt nhất

Nguyên nhân: Giới hạn Tốc độ API (Rate Limiting) hoặc Timeouts

Nếu hàng đợi công việc lớn (ví dụ: 50 prompt) và mỗi lần gọi Gemini mất 20-30 giây, tổng thời gian thực thi công việc có thể vượt quá giới hạn được khuyến nghị, khiến công việc thất bại hoặc timeout, có khả năng để lại kết quả một phần.

Giải pháp: Phân lô (Batching) và Xử lý Bất đồng bộ

Thay vì dựa vào phương thức đồng bộ tiêu chuẩn Execute() cho toàn bộ công việc, hãy đảm bảo logic gọi API được xử lý bất đồng bộ đúng cách (như đã thấy ở trên bằng cách sử dụng Task.Run().Result, thừa nhận các ràng buộc đồng bộ của ngữ cảnh thực thi ScheduledJobBase). Điều quan trọng là triển khai kích thước lô tối đa (ví dụ: chỉ xử lý 5 prompt mỗi lần thực thi) và dựa vào tần suất theo lịch để xử lý phần còn lại.

Nguyên nhân: Đầu ra JSON không hợp lệ

Mặc dù schema đầu ra có cấu trúc giúp giảm đáng kể lỗi, các yêu cầu phức tạp hoặc dài đôi khi khiến AI thêm văn bản giới thiệu bên ngoài khối JSON (ví dụ: "Here is the content:\n{...}").

Giải pháp: Tiền xử lý Mạnh mẽ (Robust Pre-Processing)

Trước khi cố gắng JsonSerializer.Deserialize, hãy thêm các bước xác thực và làm sạch để cô lập chuỗi JSON thuần túy. Điều này thường bao gồm việc loại bỏ các hàng rào mã markdown (`json`) hoặc cắt bỏ khoảng trắng trước dấu ngoặc nhọn đầu tiên { và sau dấu ngoặc nhọn cuối cùng }.

Kết luận: Sức mạnh của Tự động hóa 'Set and Forget'

Bằng cách tận dụng Optimizely Scheduled Jobs và khả năng bắt buộc dữ liệu có cấu trúc của Google Gemini, CmsIv đã đạt được một quy trình nội dung không cần nỗ lực. Biên tập viên chỉ cần thêm các ý tưởng cấp cao vào Content Area, và hệ thống sẽ xử lý việc tạo kỹ thuật, tối ưu hóa SEO, ánh xạ thuộc tính và xuất bản ngay lập tức.

Cách tiếp cận tự động này mang lại sự gia tăng nhất quán 5 lần về khả năng xuất bản hàng ngày, đồng thời đảm bảo rằng mọi nội dung đều đáp ứng các yêu cầu kỹ thuật và SEO tối thiểu khi tạo, giải phóng thời gian quý báu của nhà phát triển cho các cải tiến khung cốt lõi.

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