Bạn muốn bài viết Blogger của mình nổi bật trên Google với những ngôi sao đánh giá lấp lánh? ⭐⭐⭐⭐⭐ Bạn muốn thu thập phản hồi realtime từ độc giả nhưng loay hoay vì schema đánh giá sao không được Google nhận diện? Đừng lo lắng! Trong bài viết này, TruongDevs sẽ hướng dẫn bạn từ A đến Z cách tạo đánh giá sao cho Blogger bằng Firebase, kết hợp với sức mạnh của Cloud Function để tối ưu hiển thị sao trên Google (rich snippets) một cách hiệu quả và chuẩn SEO nhất.

Việc tích hợp đánh giá động và đảm bảo Googlebot đọc được schema là một thách thức lớn. Googlebot thường chỉ quét HTML tĩnh và không đợi JavaScript kết nối Firebase xong. Giải pháp chúng ta sẽ thực hiện bằng cách sử dụng Cloud Function để tạo file cache, đây là phương pháp tối ưu nhất hiện nay để giải quyết vấn đề này, đảm bảo Google thấy được dữ liệu đánh giá gần như thật ngay lập tức.

Hãy cùng bắt tay vào làm nhé!

danh gia sao blogger
Cách tạo đánh giá sao Blogger hiển thị trên Google
{getToc} $title={Table of Contents}

Tại sao cần dùng Cloud Function & Cache cho đánh giá sao Blogger?

Nhiều hướng dẫn tạo sao đánh giá Blogger chỉ dừng lại ở việc dùng JavaScript kết nối trực tiếp Firebase. Cách này hoạt động tốt cho người dùng, nhưng thường thất bại với Google vì:

  • Googlebot quét nhanh: Nó đọc HTML gốc trước khi JavaScript kịp chạy và lấy dữ liệu từ Firebase.
  • Firebase bị chặn: File robots.txt của Firebase có thể chặn Googlebot truy cập trực tiếp để lấy dữ liệu.

Giải pháp Cloud Function + Cache khắc phục điều này:

  • Cloud Function (Trợ lý ảo): Tự động tổng hợp dữ liệu rating từ Firebase Realtime Database và lưu vào một file cache (ví dụ: ratings_cache.json) trên Firebase Storage.
  • JavaScript trên Blogger: Thay vì kết nối Firebase trực tiếp (chậm), nó chỉ cần tải file cache (nhanh) và tạo schema JSON-LD ngay lập tức.

Kết quả: Googlebot đọc được schema với dữ liệu gần như thật ngay trong lần quét đầu tiên, tăng tối đa khả năng hiển thị sao trên Google.

Chuẩn bị cần thiết

Trước khi bắt đầu tạo đánh giá sao cho Blogger, bạn cần:

  • Kiến thức cơ bản về chỉnh sửa theme Blogger (HTML/CSS/JS).
  • Tài khoản Google.
  • Node.js và npm cài đặt trên máy tính (Tải tại nodejs.org).
  • Firebase CLI: Cài bằng Command Prompt/PowerShell:
    npm install -g firebase-tools
  • Google Cloud SDK (gcloud CLI): Cài đặt theo hướng dẫn tại đây.

Thiết lập Firebase Realtime Database

Tạo dự án Firebase

  1. Truy cập Firebase Console.
  2. Nhấn "Create a New Firebase project" và làm theo hướng dẫn để tạo dự án mới (ví dụ: myblog-ratings).
    Giao diện tạo dự án Firebase với tên myblog-ratings cho hệ thống sao đánh giá Blogger
    Đặt tên cho dự án Firebase của bạn, ví dụ: myblog-ratings

Tạo Realtime Database

  1. Trong menu dự án, chọn BuildRealtime Database.
  2. Nhấn "Create Database".
    Bảng điều khiển Firebase hướng dẫn tạo Realtime Database cho sao đánh giá Blogger
    Chọn "Create Database" để bắt đầu thiết lập nơi lưu trữ dữ liệu rating
  3. Chọn vị trí (ví dụ: asia-southeast1).
    Tùy chọn vị trí máy chủ cho Firebase Realtime Database như Singapore hoặc Mỹ
    Chọn một vị trí máy chủ gần với độc giả của bạn, ví dụ: Singapore (asia-southeast1)
  4. Chọn "Start in locked mode". Nhấn "Enable".
    Chọn 'Start in locked mode' khi thiết lập quy tắc bảo mật ban đầu cho Firebase Realtime Database
    Bắt đầu với "Start in locked mode" để đảm bảo an toàn, chúng ta sẽ mở quyền sau

Lấy Firebase Config

  1. Nhấn biểu tượng bánh răng → Project settings.
  2. Tab General → Kéo xuống Your apps → Nhấp biểu tượng Web (</>).
  3. Đặt tên app (ví dụ: Blogger Rating App) → "Register app".
  4. Sao chép lại đối tượng firebaseConfig. Bạn sẽ cần dùng nó sau.
  5. Nhấn "Continue to console".

Thêm giao diện và JavaScript vào Blogger

Thêm HTML vào Theme

  1. Vào Chủ đềChỉnh sửa HTML.
  2. Tìm vị trí muốn hiển thị đánh giá (thường trong <div class='post-body'>).
  3. Dán code HTML:
    
    <b:if cond='data:view.isPost'>
      <div class='truongdevs-rating-section' expr:data-post-id='data:post.id'>
        <div class='truongdevs-rating-area'>
          <div class='truongdevs-rating-average'>
            <span id='truongdevs-avg-score'>0.0</span><small>/5.0</small>
          </div>
          <div class='truongdevs-rating-stars' id='truongdevs-rating-stars'>
            </div>
          <div class='truongdevs-rating-total'><span id='truongdevs-total-rating-count'>0</span> ratings</div>
          <div class='truongdevs-rating-caption truongdevs-hidden'>
            <p>Cảm ơn bạn đã đánh giá!</p>
          </div>
        </div>
        <div class='truongdevs-rating-breakdown'>
          <div class='truongdevs-rating-progress' data-rate='5'>
            <span class='truongdevs-rating-grade'>5</span>
            <div class='truongdevs-rating-bar'><div class='truongdevs-rating-bar-fill' style='width: 0%;'/></div>
            <span class='truongdevs-rating-votes'><span class='truongdevs-rating-votes-count'>0</span> votes</span>
          </div>
          <div class='truongdevs-rating-progress' data-rate='4'>
             <span class='truongdevs-rating-grade'>4</span>
             <div class='truongdevs-rating-bar'><div class='truongdevs-rating-bar-fill' style='width: 0%;'/></div>
             <span class='truongdevs-rating-votes'><span class='truongdevs-rating-votes-count'>0</span> votes</span>
          </div>
           <div class='truongdevs-rating-progress' data-rate='3'>
             <span class='truongdevs-rating-grade'>3</span>
             <div class='truongdevs-rating-bar'><div class='truongdevs-rating-bar-fill' style='width: 0%;'/></div>
             <span class='truongdevs-rating-votes'><span class='truongdevs-rating-votes-count'>0</span> votes</span>
          </div>
           <div class='truongdevs-rating-progress' data-rate='2'>
             <span class='truongdevs-rating-grade'>2</span>
             <div class='truongdevs-rating-bar'><div class='truongdevs-rating-bar-fill' style='width: 0%;'/></div>
             <span class='truongdevs-rating-votes'><span class='truongdevs-rating-votes-count'>0</span> votes</span>
          </div>
           <div class='truongdevs-rating-progress' data-rate='1'>
             <span class='truongdevs-rating-grade'>1</span>
             <div class='truongdevs-rating-bar'><div class='truongdevs-rating-bar-fill' style='width: 0%;'/></div>
             <span class='truongdevs-rating-votes'><span class='truongdevs-rating-votes-count'>0</span> votes</span>
          </div>
        </div>
      </div>
    </b:if>
    

Thêm CSS

  1. Thêm đoạn CSS sau vào khu vực CSS tùy chỉnh của theme:
    
    /* --- CSS CHO PHẦN ĐÁNH GIÁ --- */
    .truongdevs-rating-section{display:flex;align-items:center;flex-wrap:wrap;position:relative;width:100%;margin-top:35px;padding-top:35px;padding-bottom:35px;margin-bottom:35px;}
    .truongdevs-rating-section::before,.truongdevs-rating-section::after{content:'';position:absolute;left:0;width:100%;height:10px;background-image:radial-gradient(circle at 1px 1px,$(border) 1px,transparent 0);background-size:5px 5px;opacity:0.5;}
    .truongdevs-rating-section::before{top:0;}
    .truongdevs-rating-section::after{bottom:0;}
    .truongdevs-rating-area{width:35%;text-align:center;padding-right:30px;margin-right:30px;border-right:1px solid $(border);box-sizing:border-box;}
    .truongdevs-rating-average{font-weight:700;color:$(text.color);line-height:1;margin-bottom:10px;}
    #truongdevs-avg-score{font-size:48px;}
    #truongdevs-avg-score+small{font-size:20px;font-weight:500;color:$(meta.color);}
    .truongdevs-rating-stars{display:flex;gap:4px;justify-content:center;margin:15px 0 10px;}
    .truongdevs-rating-stars svg{width:32px;height:32px;cursor:pointer;transition:transform .2s ease;}
    .truongdevs-rating-stars svg path{fill:#ccc;transition:fill .2s ease;}
    .truongdevs-rating-stars:not(.rated) svg:hover{transform:scale(1.1);}
    .truongdevs-rating-stars:not(.rated) svg:hover path,.truongdevs-rating-stars:not(.rated) svg:hover~svg path{fill:#ffc107;}
    .truongdevs-rating-stars.rated svg{cursor:default;}
    .truongdevs-rating-stars.rated svg path{fill:#ffc107;}
    .truongdevs-rating-total{font-size:14px;color:$(meta.color);}
    .truongdevs-rating-caption p{font-size:15px;font-weight:600;color:#28a745;margin-top:10px;}
    .truongdevs-rating-breakdown{width:calc(65% - 30px);}
    .truongdevs-rating-progress{display:flex;align-items:center;gap:15px;margin-bottom:8px;}
    .truongdevs-rating-grade{display:flex;align-items:center;gap:5px;font-size:15px;font-weight:500;color:$(meta.color);flex-shrink:0;}
    .truongdevs-rating-grade::before{content:'\F586';font-family:'bootstrap-icons';font-size:14px;color:#ffc107;} /* Cần có font Bootstrap Icons */
    .truongdevs-rating-bar{flex-grow:1;height:8px;background-color:$(gray);border-radius:4px;overflow:hidden;}
    .truongdevs-rating-bar-fill{height:100%;background-color:$(accent.color);border-radius:4px;transition:width .6s ease;}
    .truongdevs-rating-votes{font-size:14px;font-weight:500;color:$(meta.color);width:50px;text-align:right;flex-shrink:0;}
    .truongdevs-rating-votes-count{font-weight:700;color:$(text.color);}
    .truongdevs-hidden{display:none!important;}
    @media (max-width:680px){.truongdevs-rating-section{flex-direction:column;align-items:stretch;gap:20px;padding-top:25px;padding-bottom:25px;margin-bottom:25px;}.truongdevs-rating-area{width:100%;border-right:none;border-bottom:1px solid $(border);margin:0 0 10px 0;padding:0 0 20px 0;}.truongdevs-rating-breakdown{width:100%;}}
    
  2. Đảm bảo theme đã nhúng font Bootstrap Icons và có các biến màu cần thiết.

Thêm JavaScript

Dán đoạn script sau vào theme, ngay trước thẻ đóng </body>. Quan trọng: Nhớ thay thế các giá trị placeholder trong firebaseConfigCACHE_URL bằng thông tin thực tế của bạn.


<script src='https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js'></script>
<script src='https://www.gstatic.com/firebasejs/9.6.1/firebase-database-compat.js'></script>
<script src='https://cdn.jsdelivr.net/npm/fingerprintjs2@2.1.4/dist/fingerprint2.min.js'></script>
<script>
//<![CDATA[
document.addEventListener('DOMContentLoaded', () => {
    const firebaseConfig = {
        apiKey: "YOUR_API_KEY", // <<= THAY BẰNG CONFIG CỦA BẠN
        authDomain: "YOUR_AUTH_DOMAIN", // <<= THAY BẰNG CONFIG CỦA BẠN
        databaseURL: "YOUR_DATABASE_URL", // <<= THAY BẰNG CONFIG CỦA BẠN
        projectId: "YOUR_PROJECT_ID", // <<= THAY BẰNG CONFIG CỦA BẠN
        storageBucket: "YOUR_STORAGE_BUCKET.appspot.com", // <<= THAY BẰNG CONFIG CỦA BẠN
        messagingSenderId: "YOUR_MESSAGING_SENDER_ID", // <<= THAY BẰNG CONFIG CỦA BẠN
        appId: "YOUR_APP_ID" // <<= THAY BẰNG CONFIG CỦA BẠN
    };
    // !!! QUAN TRỌNG: Thay đúng tên bucket của bạn vào đây !!!
    const CACHE_URL = 'https://storage.googleapis.com/YOUR_STORAGE_BUCKET.appspot.com/ratings_cache.json'; // <<= THAY TÊN BUCKET
    let db;
    let ratingSection;
    let postId;
    let ratingRef;
    let starsContainer;
    let stars;
    const FINGERPRINT_KEY = 'your_blog_rating_fp'; // Có thể đổi tên nếu muốn
    const starSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 .587l3.668 7.431L24 9.423l-6 5.848 1.417 8.268L12 18.897l-7.417 4.642L6 15.271 0 9.423l8.332-1.405z"/></svg>`;
    let cachedRatingData = null;
    let schemaCreated = false;

    ratingSection = document.querySelector('.truongdevs-rating-section');
    if (!ratingSection) return;
    postId = ratingSection.getAttribute('data-post-id');
    if (!postId) return;
    starsContainer = document.getElementById('truongdevs-rating-stars');
    if (!starsContainer) return;
    try {
        starsContainer.innerHTML = Array(5).fill(starSvg).join('');
        stars = starsContainer.querySelectorAll('svg');
    } catch(e) { return; }

    try {
        if (!firebase.apps.length) firebase.initializeApp(firebaseConfig);
        db = firebase.database();
        ratingRef = db.ref('ratings/' + postId);
    } catch (e) { updateUI(null); return; }

    const getFingerprint = () => new Promise(resolve => {
        try {
            const savedFp = localStorage.getItem(FINGERPRINT_KEY);
            if (savedFp) { resolve(savedFp); }
            else {
                Fingerprint2.get(components => {
                    const values = components.map(c => c.value);
                    const murmur = Fingerprint2.x64hash128(values.join(''), 31);
                    localStorage.setItem(FINGERPRINT_KEY, murmur);
                    resolve(murmur);
                });
            }
        } catch (e) {
             resolve('fp_error_' + Date.now());
        }
    });

    const updateUI = (data) => {
         try {
            const sum = data ? (data.sum || 0) : 0;
            const count = data ? (data.count || 0) : 0;
            const avg = count > 0 ? (sum / count) : 0;
            const avgDisplay = avg.toFixed(1);
            const countDisplay = count;
            const avgElement = document.getElementById('truongdevs-avg-score');
            const countElement = document.getElementById('truongdevs-total-rating-count');
            if (avgElement) avgElement.textContent = avgDisplay;
            if (countElement) countElement.textContent = countDisplay;
            stars.forEach((star, index) => {
                const path = star.querySelector('path');
                if (path) {
                    path.style.fill = index < Math.round(avg) ? '#ffc107' : '#ccc';
                }
            });
            for (let i = 1; i <= 5; i++) {
                const countForStar = data && data.distribution ? (data.distribution[i] || 0) : 0;
                const progressRow = document.querySelector(`.truongdevs-rating-progress[data-rate="${i}"]`);
                if (progressRow) {
                    const percentage = count > 0 ? (countForStar / count) * 100 : 0;
                    const barFill = progressRow.querySelector('.truongdevs-rating-bar-fill');
                    const votesCount = progressRow.querySelector('.truongdevs-rating-votes-count');
                    if (barFill) barFill.style.width = `${percentage}%`;
                    if (votesCount) votesCount.textContent = countForStar;
                }
            }
         } catch (e) { /* Ignore UI errors */ }
    };

    const generateSchema = (avg, count) => {
        try {
            const existingSchema = document.getElementById('truongdevs-rating-schema');
            if (existingSchema) existingSchema.remove();

            if (count > 0 && typeof avg === 'number' && !isNaN(avg)) {
                const canonicalLink = document.querySelector("link[rel='canonical']");
                const postTitleElement = document.querySelector('.entry-title, .post-title, h1');
                const postTitle = postTitleElement ? postTitleElement.textContent.trim() : document.title;
                if (!canonicalLink || !canonicalLink.href) return;

                const schema = {
                    "@context": "https://schema.org",
                    "@id": canonicalLink.href + "#aggregateRating",
                    "@type": "AggregateRating",
                    "itemReviewed": {
                       "@type": "CreativeWorkSeries", // Sử dụng CreativeWorkSeries
                       "name": postTitle,
                       "url": canonicalLink.href
                    },
                    "ratingValue": avg.toFixed(1),
                    "ratingCount": count
                };

                const scriptElement = document.createElement('script');
                scriptElement.id = 'truongdevs-rating-schema';
                scriptElement.type = 'application/ld+json';
                scriptElement.textContent = JSON.stringify(schema);

                setTimeout(() => { // Thêm độ trễ nhỏ
                    try {
                        if (document.head) {
                            document.head.appendChild(scriptElement);
                            schemaCreated = true;
                        }
                    } catch (appendError) { /* Ignore append errors */ }
                }, 150);

            } else if (schemaCreated) {
                 const existingSchemaToRemove = document.getElementById('truongdevs-rating-schema');
                 if (existingSchemaToRemove) existingSchemaToRemove.remove();
                 schemaCreated = false;
            }
        } catch (e) { /* Ignore schema generation errors */ }
    };

    const handleRating = async (e) => {
        try {
            const rating = parseInt(e.currentTarget.dataset.value);
            const fingerprint = await getFingerprint();
            if (!ratingRef) return;
            ratingRef.transaction((currentData) => {
                if (currentData === null) {
                    return { sum: rating, count: 1, distribution: { [rating]: 1 }, fingerprints: { [fingerprint]: true } };
                }
                if (currentData.fingerprints && currentData.fingerprints[fingerprint]) {
                    return;
                }
                const newData = JSON.parse(JSON.stringify(currentData));
                newData.count = (newData.count || 0) + 1;
                newData.sum = (newData.sum || 0) + rating;
                if (!newData.distribution) newData.distribution = {};
                newData.distribution[rating] = (newData.distribution[rating] || 0) + 1;
                if (!newData.fingerprints) newData.fingerprints = {};
                newData.fingerprints[fingerprint] = true;
                return newData;
            }, (error, committed, snapshot) => {
                if (!error && committed && snapshot && snapshot.exists()) {
                    if(starsContainer) starsContainer.classList.add('rated');
                    const caption = document.querySelector('.truongdevs-rating-caption');
                    if (caption) caption.classList.remove('truongdevs-hidden');
                }
            });
        } catch(e) { /* Ignore rating errors */ }
    };

    async function initializeRating() {
        let schemaSuccessfullyCreatedFromCache = false;
        try {
            const response = await fetch(CACHE_URL + '?t=' + Date.now(), { cache: "no-store" });
            if (response.ok) {
                const cache = await response.json();
                if (cache && cache[postId]) {
                    cachedRatingData = cache[postId];
                    const avgCache = parseFloat(cachedRatingData.avg);
                    const countCache = parseInt(cachedRatingData.count, 10);
                    if (!isNaN(avgCache) && !isNaN(countCache) && countCache > 0) {
                       generateSchema(avgCache, countCache);
                       schemaSuccessfullyCreatedFromCache = true;
                    }
                }
            }
        } catch (e) { /* Ignore cache errors */ }

        if (ratingRef) {
            ratingRef.on('value', async (snapshot) => {
                const firebaseData = snapshot.val();
                const currentData = firebaseData || { sum: 0, count: 0 };
                updateUI(currentData);
                const avg = currentData.count > 0 ? (currentData.sum / currentData.count) : 0;
                const count = currentData.count || 0;
                const cacheCount = cachedRatingData ? parseInt(cachedRatingData.count, 10) : -1;

                if (!schemaSuccessfullyCreatedFromCache || (count > 0 && count !== cacheCount) || (count === 0 && schemaCreated)) {
                     generateSchema(avg, count);
                     cachedRatingData = { avg: avg.toFixed(1), count: count };
                     schemaSuccessfullyCreatedFromCache = true;
                }
                try {
                    const fingerprint = await getFingerprint();
                    if (currentData.fingerprints && currentData.fingerprints[fingerprint]) {
                        if (starsContainer) starsContainer.classList.add('rated');
                        const caption = document.querySelector('.truongdevs-rating-caption');
                        if (caption) caption.classList.remove('truongdevs-hidden');
                    } else {
                         if (starsContainer) starsContainer.classList.remove('rated');
                    }
                } catch (fpError) { /* Ignore */ }
            }, (error) => {
                 updateUI(null); generateSchema(0, 0);
            });
        } else { updateUI(null); generateSchema(0, 0); }

        if (stars && stars.length > 0) {
            stars.forEach((star, index) => {
                star.dataset.value = index + 1;
                star.addEventListener('click', handleRating);
            });
        }
    }

    initializeRating();
});
//]]>
</script>

Thiết lập Cloud Function

Đây là bước then chốt để tạo file cache giúp Google đọc schema đánh giá nhanh hơn.

Khởi tạo Firebase CLI trên máy tính (nếu chưa)

  1. Mở Command Prompt (cmd) hoặc PowerShell.
  2. Di chuyển đến thư mục bạn muốn chứa code (ví dụ: cd E:\MyProjects).
  3. Đăng nhập Firebase:
    firebase login --no-localhost

    (Copy link, dán vào trình duyệt, đăng nhập, copy mã và dán lại vào cmd).

  4. Khởi tạo dự án:
    firebase init
  5. Chọn (*) Functions (dùng phím cách để chọn, Enter để xác nhận).
  6. Chọn Use an existing project -> Chọn dự án Firebase của bạn (ví dụ: myblog-ratings).
  7. Chọn ngôn ngữ JavaScript.
  8. Chọn No cho ESLint (cho đơn giản).
  9. Chọn Yes để cài đặt dependencies. Chờ quá trình hoàn tất.

Viết và Deploy Code Function

  1. Trong thư mục dự án của bạn, mở thư mục functions, sau đó mở file index.js.
  2. Xóa hết code mẫu.
  3. Dán code sau vào:
    
    const {onSchedule} = require("firebase-functions/v2/scheduler");
    const {initializeApp} = require("firebase-admin/app");
    const {getDatabase} = require("firebase-admin/database");
    const {getStorage} = require("firebase-admin/storage");
    const logger = require("firebase-functions/logger");
    
    initializeApp();
    
    exports.updateRatingsCache = onSchedule("every 4 hours", async (event) => { // <<= Đặt tần suất cập nhật
      try {
        const db = getDatabase();
        const storage = getStorage();
        const ratingsRef = db.ref("ratings"); // Tên nhánh dữ liệu Firebase
        const snapshot = await ratingsRef.once("value");
        const allRatings = snapshot.val();
        const cache = {};
    
        if (allRatings) {
          for (const postId in allRatings) {
            if (Object.prototype.hasOwnProperty.call(allRatings, postId)) {
              const ratingData = allRatings[postId];
              if (ratingData && ratingData.count > 0 && ratingData.sum) {
                cache[postId] = {
                  avg: (ratingData.sum / ratingData.count).toFixed(1),
                  count: ratingData.count,
                };
              }
            }
          }
        }
    
        const bucket = storage.bucket(); // Lấy bucket mặc định
        const file = bucket.file("ratings_cache.json"); // Tên file cache
    
        await file.save(JSON.stringify(cache), {
          metadata: {
            contentType: "application/json",
            cacheControl: "public, max-age=3600", // Cache trình duyệt 1 giờ
          },
        });
    
        await file.makePublic(); // Đặt file thành công khai
    
        logger.info("Successfully updated ratings cache.", {
          count: Object.keys(cache).length,
        });
      } catch (error) {
        logger.error("Error updating ratings cache:", error);
      }
    });
    
  4. Lưu file index.js.
  5. Quay lại Command Prompt, đảm bảo bạn đang ở trong thư mục dự án (ví dụ: E:\MyProjects).
  6. Chạy lệnh deploy:
    firebase deploy --only functions
  7. Chờ quá trình deploy hoàn tất.

Cấu hình Firebase Storage và CORS

Chạy Function lần đầu

  1. Vào Firebase ConsoleFunctions.
  2. Tìm updateRatingsCache → Nhấn menu (...) -> View in Cloud Scheduler.
  3. Trong Cloud Scheduler, chọn jobResume (nếu đang Paused) → Force run.
  4. Đợi khoảng 1 phút.

Kiểm tra và lấy link file cache

  1. Vào Firebase ConsoleStorage.
  2. Bạn sẽ thấy file ratings_cache.json.
  3. Nhấp vào file. Link chuẩn sẽ có dạng https://storage.googleapis.com/YOUR_BUCKET_NAME/ratings_cache.json.
  4. Copy link này và dán vào biến CACHE_URL trong script Blogger.

Cấu hình CORS (Quan trọng)

Bước này cho phép trang Blogger của bạn đọc được file cache.

  1. Cài đặt Google Cloud SDK trên máy tính (nếu chưa).
  2. Mở Command Prompt (cmd) hoặc PowerShell.
  3. Đăng nhập:
    gcloud auth login

    (Làm theo hướng dẫn trên trình duyệt).

  4. Khởi tạo (nếu chưa):
    gcloud init

    (Chọn tài khoản, chọn đúng dự án Firebase).

  5. Tạo file cors.json (ví dụ: trên Desktop) với nội dung:
    
    [
      {
        "origin": ["https://www.yourblogdomain.com"], // <<= THAY BẰNG DOMAIN CỦA BẠN
        "method": ["GET"],
        "maxAgeSeconds": 3600
      }
    ]
    
  6. Chạy lệnh (thay YOUR_BUCKET_NAME và đường dẫn file cors.json):
    
    gsutil cors set C:\path\to\your\cors.json gs://YOUR_BUCKET_NAME
    

    (Ví dụ: gsutil cors set cors.json gs://myblog-ratings.appspot.com)

  7. Đợi vài phút để CORS có hiệu lực.

Sửa Quy tắc Bảo mật Firebase (Rules)

Bước này đảm bảo chỉ có dữ liệu hợp lệ được ghi vào database. TruongDevs đề xuất sử dụng quy tắc an toàn sau, xử lý cả lần vote đầu tiên và các lần sau.

Giải thích Quy tắc

  • !data.exists() && newData.child('count').val() == 1: Cho phép ghi nếu đây là lần vote đầu tiên (dữ liệu cũ chưa tồn tại) và count gửi lên là 1.
  • data.exists() && newData.child('count').val() == data.child('count').val() + 1: Cho phép ghi nếu dữ liệu đã tồn tại và count mới bằng count cũ cộng thêm 1.

Quy tắc này rất hiệu quả trong việc chống spam và đảm bảo tính toàn vẹn của dữ liệu.

Cập nhật Rules

  1. Vào Firebase ConsoleRealtime DatabaseRules.
  2. Thay thế nội dung bằng:
    
    {
      "rules": {
        "ratings": {
          "$postId": {
            ".read": "true",
            // Quy tắc an toàn cho cả tạo mới và cập nhật
            ".write": "(!data.exists() && newData.child('count').val() == 1) || (data.exists() && newData.child('count').val() == data.child('count').val() + 1)",
            "count": { ".validate": "newData.isNumber()" },
            "sum": { ".validate": "newData.isNumber()" },
            "distribution": { ".validate": "newData.hasChildren()" },
            "fingerprints": { ".validate": "newData.hasChildren()" }
          }
        }
      }
    }
    
  3. Nhấn Publish.

Kiểm tra và Chờ đợi

  1. Kiểm tra lỗi Console: Mở bài viết có đánh giá trên trình duyệt, nhấn F12 → Console, xem có lỗi đỏ nào không.
  2. Kiểm tra Schema: Dùng Google Rich Results Test hoặc URL Inspection (Test Live URL) trong Search Console. Tìm thẻ <script id='truongdevs-rating-schema'> trong mã nguồn render hoặc xem Google có nhận diện "Đoạn trích đánh giá" không.
    Kết quả kiểm tra Rich Results của Google báo cáo Đoạn trích đánh giá hợp lệ
    Kết quả cuối cùng: Google đã nhận diện thành công "Đoạn trích đánh giá" (AggregateRating)
  3. Chờ đợi: Google cần thời gian (vài ngày đến vài tuần) để thu thập dữ liệu mới và hiển thị sao đánh giá.

Kết luận

Việc triển khai hệ thống đánh giá sao cho Blogger hiển thị trên Google đòi hỏi sự kết hợp giữa Firebase, Cloud Function và cấu hình cẩn thận. Bằng cách làm theo hướng dẫn chi tiết này từ TruongDevs, bạn đã xây dựng một giải pháp mạnh mẽ, chuẩn SEO và sử dụng dữ liệu realtime từ người dùng. Hãy kiên nhẫn chờ đợi Google cập nhật và tận hưởng thành quả!

Nếu bạn thấy bài viết này hữu ích và đã thực hiện thành công, đừng quên để lại một đánh giá 5 sao để ủng hộ TruongDevs nhé. Mỗi lượt đánh giá của bạn là nguồn động lực rất lớn để mình tiếp tục chia sẻ những hướng dẫn chi tiết và chất lượng hơn! Còn nếu gặp bất kỳ khó khăn nào, bước nào chưa làm được, hay gặp lỗi gì ở đâu, đừng ngần ngại để lại bình luận bên dưới. Mình sẽ xem và hỗ trợ để bạn làm được!
0.0/5.0
0 ratings

Thank you for your rating!

5
0 votes
4
0 votes
3
0 votes
2
0 votes
1
0 votes