Hướng Dẫn Tạo Đánh Giá Sao Cho Blogger Realtime Hiển Thị Trên Google
Hướng dẫn A-Z cách tạo đánh giá sao realtime cho Blogger dùng Firebase & Cloud Function, giúp hiển thị sao trên Google (rich snippets) hiệu quả.
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é!
![]() |
Cách tạo đánh giá sao Blogger hiển thị trên Google |
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
- Truy cập Firebase Console.
- 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
).Đặt tên cho dự án Firebase của bạn, ví dụ: myblog-ratings
Tạo Realtime Database
- Trong menu dự án, chọn Build → Realtime Database.
- Nhấn "Create Database".
Chọn "Create Database" để bắt đầu thiết lập nơi lưu trữ dữ liệu rating - Chọn vị trí (ví dụ:
asia-southeast1
).Chọn một vị trí máy chủ gần với độc giả của bạn, ví dụ: Singapore (asia-southeast1) - Chọn "Start in locked mode". Nhấn "Enable".
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
- Nhấn biểu tượng bánh răng → Project settings.
- Tab General → Kéo xuống Your apps → Nhấp biểu tượng Web (</>).
- Đặt tên app (ví dụ:
Blogger Rating App
) → "Register app". - Sao chép lại đối tượng
firebaseConfig
. Bạn sẽ cần dùng nó sau. - Nhấn "Continue to console".
Thêm giao diện và JavaScript vào Blogger
Thêm HTML vào Theme
- Vào Chủ đề → Chỉnh sửa HTML.
- Tìm vị trí muốn hiển thị đánh giá (thường trong
<div class='post-body'>
). - 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
- 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%;}}
- Đả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 firebaseConfig
và CACHE_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)
- Mở Command Prompt (cmd) hoặc PowerShell.
- Di chuyển đến thư mục bạn muốn chứa code (ví dụ:
cd E:\MyProjects
). - Đă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).
- Khởi tạo dự án:
firebase init
- Chọn
(*) Functions
(dùng phím cách để chọn, Enter để xác nhận). - Chọn
Use an existing project
-> Chọn dự án Firebase của bạn (ví dụ:myblog-ratings
). - Chọn ngôn ngữ
JavaScript
. - Chọn
No
cho ESLint (cho đơn giản). - Chọn
Yes
để cài đặt dependencies. Chờ quá trình hoàn tất.
Viết và Deploy Code Function
- Trong thư mục dự án của bạn, mở thư mục
functions
, sau đó mở fileindex.js
. - Xóa hết code mẫu.
- 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); } });
- Lưu file
index.js
. - Quay lại Command Prompt, đảm bảo bạn đang ở trong thư mục dự án (ví dụ:
E:\MyProjects
). - Chạy lệnh deploy:
firebase deploy --only functions
- Chờ quá trình deploy hoàn tất.
Cấu hình Firebase Storage và CORS
Chạy Function lần đầu
- Vào Firebase Console → Functions.
- Tìm
updateRatingsCache
→ Nhấn menu (...) → View in Cloud Scheduler. - Trong Cloud Scheduler, chọn job → Resume (nếu đang Paused) → Force run.
- Đợi khoảng 1 phút.
Kiểm tra và lấy link file cache
- Vào Firebase Console → Storage.
- Bạn sẽ thấy file
ratings_cache.json
. - Nhấp vào file. Link chuẩn sẽ có dạng
https://storage.googleapis.com/YOUR_BUCKET_NAME/ratings_cache.json
. - 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.
- Cài đặt Google Cloud SDK trên máy tính (nếu chưa).
- Mở Command Prompt (cmd) hoặc PowerShell.
- Đăng nhập:
gcloud auth login
(Làm theo hướng dẫn trên trình duyệt).
- Khởi tạo (nếu chưa):
gcloud init
(Chọn tài khoản, chọn đúng dự án Firebase).
- 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 } ]
- Chạy lệnh (thay
YOUR_BUCKET_NAME
và đường dẫn filecors.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
) - Đợ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ằngcount
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
- Vào Firebase Console → Realtime Database → Rules.
- 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()" } } } } }
- Nhấn Publish.
Kiểm tra và Chờ đợi
- 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.
- 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ả cuối cùng: Google đã nhận diện thành công "Đoạn trích đánh giá" (AggregateRating) - 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!
bóc tem :P
ReplyDeleteBóc nhẹ thôi...
Deleteđịt mẹ mày lồn truongdevs: https://www.truongdevs.click
Deleteđã áp dụng thành công keke 😎
ReplyDeleteChúc mừng anh :v
Deleteđịt mẹ mày lồn truongdevs: https://www.truongdevs.click
Delete