Cara Membuat Widget Rating Bintang di Blogger Tanpa Plugin (100% Gratis)
Halo sobat blogger! Pernahkah kamu merasa konten yang kamu tulis sudah sangat bagus, tapi interaksi pembaca hanya sebatas klik Next? Di era digital yang sangat kompetitif ini, kredibilitas adalah segalanya. Salah satu cara paling efektif untuk menunjukkan bahwa kontenmu bernilai adalah dengan menampilkan rating bintang dari pembaca langsung di hasil pencarian Google.
Mungkin kamu berpikir, Ah, itu pasti pakai plugin berbayar atau harus pindah ke WordPress. Jawabannya: Sama sekali tidak! Di artikel ini, kita akan membuat widget rating bintang yang canggih, gratis selamanya, dan sepenuhnya berada di bawah kendalimu menggunakan ekosistem Google Sheets sebagai database dan Apps Script sebagai backend-nya.
Kenapa Widget Rating itu Penting untuk SEO dan Trust?
Sebelum kita masuk ke bagian teknis, kamu perlu tahu kenapa kamu harus meluangkan waktu 15 menit untuk memasang fitur ini. Kata kunci seperti cara meningkatkan CTR blog atau cara agar blog dipercaya pembaca selalu bermuara pada satu hal: Social Proof.
- Meningkatkan Click-Through Rate (CTR): Jika kamu berhasil menerapkan skema rating dengan benar, bintang kuning akan muncul di Google Search. Orang cenderung lebih memilih mengklik hasil pencarian yang ada bintangnya dibandingkan yang polos.
- User Engagement: Memberikan rating adalah bentuk interaksi paling rendah gesekan (low friction). Pembaca tidak perlu mengetik komentar, cukup satu klik saja.
- Data Analitik Mandiri: Kamu bisa tahu artikel mana yang benar-benar disukai pembaca dan mana yang perlu kamu perbaiki berdasarkan rating yang masuk di Google Sheets kamu.
Persiapan Google Sheets Sebagai Database Cloud
Langkah pertama, kita akan menyiapkan rumah untuk data rating kita. Karena kita ingin semuanya otomatis, kita akan menggunakan Google Sheets. Ini jauh lebih mudah dikelola daripada database SQL konvensional.
1. Membuat Spreadsheet Baru
Buka sheets.new dan buat sebuah file. Beri nama, misalnya Database Rating Blog. Yang perlu kamu lakukan hanya mengganti nama sheet di bagian bawah menjadi Ratings.
2. Mengambil ID Spreadsheet
Ini bagian pentingnya. Lihat pada URL browser kamu saat membuka spreadsheet tersebut. ID-nya adalah deretan karakter unik di antara /d/ dan /edit. Simpan ID ini, karena kita akan memasukkannya ke dalam kode Apps Script.
Langkah 2: Membangun Mesin dengan Google Apps Script
Sekarang, kita akan membuat jembatan (API) agar blog Blogger kamu bisa mengirim data ke Google Sheets secara otomatis. Tenang, kamu tidak perlu jago coding, cukup copy-paste dan ikuti instruksi ini.
- Di Google Sheets tadi, klik Extensions > Apps Script.
- Hapus semua kode yang ada di sana dan ganti dengan kode sakti di bawah ini:
// ============================================================
// GOOGLE APPS SCRIPT - Rating System with Spreadsheet ID
// ============================================================
const SHEET_ID = '1KktL_XhU2yZkiPZ4sZc4SQJwD4yFCmEXIBTUuduXSlQ'; // GANTI ID ANDA
const SHEET_NAME = 'Ratings';
function doGet(e) {
const action = e.parameter.action;
const postId = e.parameter.postId;
try {
if (action === 'getRating') {
const data = getPostRating(postId);
return jsonResponse({ success: true, data: data });
}
return jsonResponse({ success: false, error: 'Invalid action' });
} catch (error) {
return jsonResponse({ success: false, error: error.toString() });
}
}
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
const { action, postId, rating, userId } = data;
if (action === 'submitRating') {
const result = submitRating(postId, rating, userId || 'anonymous');
return jsonResponse({ success: true, data: result });
}
return jsonResponse({ success: false, error: 'Invalid action' });
} catch (error) {
return jsonResponse({ success: false, error: error.toString() });
}
}
function jsonResponse(data) {
return ContentService.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON);
}
function getSheet() {
const ss = SpreadsheetApp.openById(SHEET_ID);
let sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(SHEET_NAME);
// Header: PostID | Ratings JSON | Count | Total | LastUpdated
sheet.appendRow(['postId', 'ratings', 'count', 'total', 'lastUpdated']);
sheet.getRange(1, 1, 1, 5).setFontWeight('bold');
}
return sheet;
}
function getPostRating(postId) {
const sheet = getSheet();
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === postId) {
const ratings = JSON.parse(data[i][1]);
const count = data[i][2];
const total = data[i][3];
return {
average: parseFloat((total / count).toFixed(1)),
count: count,
distribution: ratings
};
}
}
return {
average: 0,
count: 0,
distribution: {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
};
}
function submitRating(postId, rating, userId) {
const sheet = getSheet();
const data = sheet.getDataRange().getValues();
const ratingNum = parseInt(rating);
// Cek apakah postId sudah ada
for (let i = 1; i < data.length; i++) {
if (data[i][0] === postId) {
// Update existing
let ratings = JSON.parse(data[i][1]);
ratings[ratingNum] = (ratings[ratingNum] || 0) + 1;
const count = data[i][2] + 1;
const total = data[i][3] + ratingNum;
sheet.getRange(i + 1, 2).setValue(JSON.stringify(ratings));
sheet.getRange(i + 1, 3).setValue(count);
sheet.getRange(i + 1, 4).setValue(total);
sheet.getRange(i + 1, 5).setValue(new Date());
return {
average: parseFloat((total / count).toFixed(1)),
count: count,
distribution: ratings
};
}
}
// Baris baru
const ratings = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0};
ratings[ratingNum] = 1;
sheet.appendRow([
postId,
JSON.stringify(ratings),
1,
ratingNum,
new Date()
]);
return {
average: ratingNum,
count: 1,
distribution: ratings
};
}
Cara Deploy (Penting)
Agar script ini bisa diakses oleh blog kamu, lakukan hal berikut:
- Klik tombol Deploy (warna biru) di kanan atas.
- Pilih New Deployment.
- Klik ikon gerigi (Select type) dan pilih Web App.
- Deskripsi: "API Rating Blog".
- Execute as: Me (Email kamu).
- Who has access: Anyone (Wajib pilih ini agar pembaca bisa mengirim rating).
- Klik Deploy dan berikan izin akses (Authorize) jika diminta.
- Salin URL Web App yang muncul! Kamu akan membutuhkannya di langkah Blogger.
Langkah 3: Mendesain Widget Rating (CSS)
Tampilan adalah segalanya. Kita ingin bintang yang terlihat modern, responsif, dan memberikan efek visual saat kursor diarahkan ke sana. Silakan masuk ke dashboard Blogger > Tema > Edit HTML.
Cari kode </style> atau </b:skin>, lalu letakkan kode CSS berikut tepat di atasnya:
/* ============================================================
RATING SYSTEM CSS
============================================================ */
/* Container */
.post-rating-wrapper {
margin: 2rem 0;
padding: 1.5rem;
background: var(--bg-secondary, #282c34);
border-radius: 12px;
border: 1px solid rgba(128, 128, 128, 0.2);
text-align: center;
}
.rating-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-primary, #f5f5f5);
margin-bottom: 0.25rem;
}
.rating-subtitle {
font-size: 0.875rem;
color: var(--color-tertiary, #a0a0a0);
margin-bottom: 1rem;
}
/* Star Form */
.rating {
margin: 0 auto;
display: inline-block;
}
.rating__stars {
display: flex;
padding-bottom: 0.375em;
position: relative;
justify-content: center;
gap: 0.25rem;
}
.rating__star {
display: block;
overflow: visible;
pointer-events: none;
width: 2.5em;
height: 2.5em;
}
.rating__star-ring,
.rating__star-fill,
.rating__star-line,
.rating__star-stroke {
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
}
.rating__star-ring,
.rating__star-fill,
.rating__star-line {
stroke: var(--yellow, #f5a623);
}
.rating__star-fill {
fill: var(--yellow, #f5a623);
transform: scale(0);
transition: fill 0.3s cubic-bezier(0.42, 0, 0.58, 1),
transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);
}
.rating__star-line {
stroke-dasharray: 12 13;
stroke-dashoffset: -13;
}
.rating__star-stroke {
stroke: hsl(223, 10%, 80%);
transition: stroke 0.3s;
}
.rating__label {
cursor: pointer;
padding: 0.125em;
transition: transform 0.2s;
}
.rating__label:hover {
transform: scale(1.1);
}
.rating__input {
position: absolute;
-webkit-appearance: none;
appearance: none;
opacity: 0;
}
/* Hover effects - stars before hovered one */
.rating__stars:hover .rating__label:hover ~ .rating__label .rating__star-stroke,
.rating__stars:hover .rating__label:hover ~ .rating__label .rating__star-fill {
stroke: hsl(223, 10%, 80%);
fill: transparent;
}
.rating__stars:hover .rating__label .rating__star-stroke,
.rating__stars:hover .rating__label .rating__star-fill {
stroke: var(--yellow, #f5a623);
fill: var(--yellow, #f5a623);
}
/* Checked animations */
.rating__input:checked ~ .rating__label .rating__star-ring {
animation-name: starRing;
}
.rating__input:checked ~ .rating__label .rating__star-stroke {
animation-name: starStroke;
}
.rating__input:checked ~ .rating__label .rating__star-line {
animation-name: starLine;
}
.rating__input:checked ~ .rating__label .rating__star-fill {
animation-name: starFill;
}
/* Animation delays */
.rating__label--delay1 .rating__star-ring,
.rating__label--delay1 .rating__star-fill,
.rating__label--delay1 .rating__star-line,
.rating__label--delay1 .rating__star-stroke {
animation-delay: 0.05s;
}
.rating__label--delay2 .rating__star-ring,
.rating__label--delay2 .rating__star-fill,
.rating__label--delay2 .rating__star-line,
.rating__label--delay2 .rating__star-stroke {
animation-delay: 0.1s;
}
.rating__label--delay3 .rating__star-ring,
.rating__label--delay3 .rating__star-fill,
.rating__label--delay3 .rating__star-line,
.rating__label--delay3 .rating__star-stroke {
animation-delay: 0.15s;
}
.rating__label--delay4 .rating__star-ring,
.rating__label--delay4 .rating__star-fill,
.rating__label--delay4 .rating__star-line,
.rating__label--delay4 .rating__star-stroke {
animation-delay: 0.2s;
}
/* Rating Text Display */
.rating__display {
font-size: 1em;
font-weight: 500;
min-height: 1.25em;
position: absolute;
top: 100%;
left: 0;
width: 100%;
text-align: center;
color: var(--yellow, #f5a623);
margin-top: 0.5rem;
}
/* Screen reader text */
.rating__sr {
clip: rect(1px, 1px, 1px, 1px);
overflow: hidden;
position: absolute;
width: 1px;
height: 1px;
}
/* Stats */
.rating-stats {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid rgba(128, 128, 128, 0.2);
}
.rating-average {
font-size: 2rem;
font-weight: 700;
color: var(--yellow, #f5a623);
}
.rating-count {
font-size: 0.875rem;
color: var(--color-tertiary, #a0a0a0);
margin-bottom: 0.75rem;
}
.rating-bars {
display: flex;
flex-direction: column;
gap: 0.4rem;
max-width: 300px;
margin: 0 auto;
}
.rating-bar {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
}
.rating-bar-star {
color: var(--yellow, #f5a623);
min-width: 2.5rem;
text-align: right;
}
.rating-bar-track {
flex: 1;
height: 6px;
background: rgba(128, 128, 128, 0.2);
border-radius: 3px;
overflow: hidden;
}
.rating-bar-fill {
height: 100%;
background: var(--yellow, #f5a623);
border-radius: 3px;
transition: width 0.5s ease;
}
.rating-bar-count {
min-width: 1.5rem;
text-align: left;
color: var(--color-tertiary, #a0a0a0);
}
/* Messages */
.rating-thanks,
.rating-already {
margin-top: 1rem;
padding: 0.75rem 1rem;
border-radius: 8px;
font-size: 0.9rem;
animation: fadeIn 0.5s ease;
}
.rating-thanks {
background: rgba(74, 222, 128, 0.1);
border: 1px solid rgba(74, 222, 128, 0.3);
color: #4ade80;
}
.rating-already {
background: rgba(245, 166, 35, 0.1);
border: 1px solid rgba(245, 166, 35, 0.3);
color: var(--yellow, #f5a623);
}
/* Animations */
@keyframes starRing {
from, 20% {
animation-timing-function: ease-in;
opacity: 1;
r: 8px;
stroke-width: 16px;
transform: scale(0);
}
35% {
animation-timing-function: ease-out;
opacity: 0.5;
r: 8px;
stroke-width: 16px;
transform: scale(1);
}
50%, to {
opacity: 0;
r: 16px;
stroke-width: 0;
transform: scale(1);
}
}
@keyframes starFill {
from, 40% {
animation-timing-function: ease-out;
transform: scale(0);
}
60% {
animation-timing-function: ease-in-out;
transform: scale(1.2);
}
80% {
transform: scale(0.9);
}
to {
transform: scale(1);
}
}
@keyframes starStroke {
from {
transform: scale(1);
}
20%, to {
transform: scale(0);
}
}
@keyframes starLine {
from, 40% {
animation-timing-function: ease-out;
stroke-dasharray: 1 23;
stroke-dashoffset: 1;
}
60%, to {
stroke-dasharray: 12 13;
stroke-dashoffset: -13;
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Dark theme */
[data-theme="dark"] .rating__star-stroke {
stroke: hsl(223, 10%, 30%);
}
/* Responsive */
@media (max-width: 480px) {
.rating__star {
width: 2em;
height: 2em;
}
.post-rating-wrapper {
padding: 1rem;
}
.rating-bars {
max-width: 100%;
}
}
Langkah 4: Memasang Widget (HTML & JS)
Sekarang kita akan memasang struktur HTML dan logika JavaScript-nya. Cari kode <data:post.body/> di dalam editor HTML Blogger kamu. Jika ada lebih dari satu, cari yang berada di dalam tag <b:includable id='post' var='post'>.
Letakkan kode berikut di bawah <data:post.body/>:
<!-- RATING SYSTEM - Tambahkan di bawah artikel -->
<b:if cond='data:view.isPost'>
<div class='post-rating-wrapper' expr:data-post-id='data:post.id'>
<div class='rating-title'>Beri Rating Artikel Ini</div>
<div class='rating-subtitle'>Bagaimana menurut Anda artikel ini?</div>
<form class='rating' expr:id='"rating-form-" + data:post.id'>
<div class='rating__stars'>
<!-- Star 1 -->
<input class='rating__input rating__input-1' expr:id='"rating-1-" + data:post.id' name='rating' type='radio' value='1'/>
<label class='rating__label' expr:for='"rating-1-" + data:post.id'>
<svg aria-hidden='true' class='rating__star' height='32' viewBox='0 0 32 32' width='32'>
<g transform='translate(16,16)'>
<circle class='rating__star-ring' fill='none' r='8' stroke='#000' stroke-width='16' transform='scale(0)'/>
</g>
<g stroke='#000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'>
<g transform='translate(16,16) rotate(180)'>
<polygon class='rating__star-stroke' fill='none' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
<polygon class='rating__star-fill' fill='#000' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
</g>
<g stroke-dasharray='12 12' stroke-dashoffset='12' transform='translate(16,16)'>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(0)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(72)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(144)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(216)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(288)'/>
</g>
</g>
</svg>
<span class='rating__sr'>1 star—Terrible</span>
</label>
<!-- Star 2 -->
<input class='rating__input rating__input-2' expr:id='"rating-2-" + data:post.id' name='rating' type='radio' value='2'/>
<label class='rating__label' expr:for='"rating-2-" + data:post.id'>
<svg aria-hidden='true' class='rating__star' height='32' viewBox='0 0 32 32' width='32'>
<g transform='translate(16,16)'>
<circle class='rating__star-ring' fill='none' r='8' stroke='#000' stroke-width='16' transform='scale(0)'/>
</g>
<g stroke='#000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'>
<g transform='translate(16,16) rotate(180)'>
<polygon class='rating__star-stroke' fill='none' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
<polygon class='rating__star-fill' fill='#000' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
</g>
<g stroke-dasharray='12 12' stroke-dashoffset='12' transform='translate(16,16)'>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(0)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(72)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(144)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(216)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(288)'/>
</g>
</g>
</svg>
<span class='rating__sr'>2 stars—Bad</span>
</label>
<!-- Star 3 -->
<input class='rating__input rating__input-3' expr:id='"rating-3-" + data:post.id' name='rating' type='radio' value='3'/>
<label class='rating__label' expr:for='"rating-3-" + data:post.id'>
<svg aria-hidden='true' class='rating__star' height='32' viewBox='0 0 32 32' width='32'>
<g transform='translate(16,16)'>
<circle class='rating__star-ring' fill='none' r='8' stroke='#000' stroke-width='16' transform='scale(0)'/>
</g>
<g stroke='#000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'>
<g transform='translate(16,16) rotate(180)'>
<polygon class='rating__star-stroke' fill='none' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
<polygon class='rating__star-fill' fill='#000' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
</g>
<g stroke-dasharray='12 12' stroke-dashoffset='12' transform='translate(16,16)'>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(0)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(72)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(144)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(216)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(288)'/>
</g>
</g>
</svg>
<span class='rating__sr'>3 stars—OK</span>
</label>
<!-- Star 4 -->
<input class='rating__input rating__input-4' expr:id='"rating-4-" + data:post.id' name='rating' type='radio' value='4'/>
<label class='rating__label' expr:for='"rating-4-" + data:post.id'>
<svg aria-hidden='true' class='rating__star' height='32' viewBox='0 0 32 32' width='32'>
<g transform='translate(16,16)'>
<circle class='rating__star-ring' fill='none' r='8' stroke='#000' stroke-width='16' transform='scale(0)'/>
</g>
<g stroke='#000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'>
<g transform='translate(16,16) rotate(180)'>
<polygon class='rating__star-stroke' fill='none' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
<polygon class='rating__star-fill' fill='#000' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
</g>
<g stroke-dasharray='12 12' stroke-dashoffset='12' transform='translate(16,16)'>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(0)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(72)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(144)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(216)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(288)'/>
</g>
</g>
</svg>
<span class='rating__sr'>4 stars—Good</span>
</label>
<!-- Star 5 -->
<input class='rating__input rating__input-5' expr:id='"rating-5-" + data:post.id' name='rating' type='radio' value='5'/>
<label class='rating__label' expr:for='"rating-5-" + data:post.id'>
<svg aria-hidden='true' class='rating__star' height='32' viewBox='0 0 32 32' width='32'>
<g transform='translate(16,16)'>
<circle class='rating__star-ring' fill='none' r='8' stroke='#000' stroke-width='16' transform='scale(0)'/>
</g>
<g stroke='#000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'>
<g transform='translate(16,16) rotate(180)'>
<polygon class='rating__star-stroke' fill='none' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
<polygon class='rating__star-fill' fill='#000' points='0,15 4.41,6.07 14.27,4.64 7.13,-2.32 8.82,-12.14 0,-7.5 -8.82,-12.14 -7.13,-2.32 -14.27,4.64 -4.41,6.07'/>
</g>
<g stroke-dasharray='12 12' stroke-dashoffset='12' transform='translate(16,16)'>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(0)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(72)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(144)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(216)'/>
<polyline class='rating__star-line' points='0 4,0 16' transform='rotate(288)'/>
</g>
</g>
</svg>
<span class='rating__sr'>5 stars—Excellent</span>
</label>
<!-- Rating Text Display -->
<p class='rating__display' data-rating='1' hidden='true'>Terrible</p>
<p class='rating__display' data-rating='2' hidden='true'>Bad</p>
<p class='rating__display' data-rating='3' hidden='true'>OK</p>
<p class='rating__display' data-rating='4' hidden='true'>Good</p>
<p class='rating__display' data-rating='5' hidden='true'>Excellent</p>
</div>
</form>
<!-- Stats Container (DI LUAR FORM) -->
<div class='rating-stats' style='display:none;'>
<div class='rating-average'>0.0</div>
<div class='rating-count'>0 ratings</div>
<div class='rating-bars'></div>
</div>
<!-- Messages (DI LUAR FORM) -->
<p class='rating-thanks' hidden='true'>Terima kasih atas rating Anda! ⭐</p>
<p class='rating-already' hidden='true'>Anda sudah memberi rating pada artikel ini.</p>
</div>
<script>
//<![CDATA[
(function() {
// GANTI DENGAN URL WEB APP ANDA
const GAS_URL = 'https://script.google.com/macros/s/AKfycbz.../exec';
class StarRating {
constructor(formEl, postId) {
this.ratings = [
{id: 1, name: "Terrible"},
{id: 2, name: "Bad"},
{id: 3, name: "OK"},
{id: 4, name: "Good"},
{id: 5, name: "Excellent"}
];
this.rating = null;
this.form = formEl;
this.postId = postId;
this.wrapper = formEl.closest('.post-rating-wrapper');
this.hasVoted = this.checkLocalStorage();
this.isSubmitting = false;
this.init();
}
// Cek localStorage apakah sudah vote post ini
checkLocalStorage() {
const votedPosts = JSON.parse(localStorage.getItem('blog_ratings') || '{}');
return votedPosts[this.postId] === true;
}
// Tandai sudah vote di localStorage
markAsVoted() {
const votedPosts = JSON.parse(localStorage.getItem('blog_ratings') || '{}');
votedPosts[this.postId] = true;
localStorage.setItem('blog_ratings', JSON.stringify(votedPosts));
}
init() {
if (!this.form) return;
// Jika sudah pernah vote, tampilkan pesan dan load stats
if (this.hasVoted) {
this.showAlreadyVoted();
this.loadStats();
return;
}
// Event listener untuk perubahan rating
this.form.addEventListener('change', this.updateRating.bind(this));
// Reset form (fix Firefox)
try {
this.form.reset();
} catch (err) {
console.error("Element isn't a form.");
}
// Load stats awal
this.loadStats();
}
// Tampilkan pesan sudah vote
showAlreadyVoted() {
const inputs = this.form.querySelectorAll('input[type="radio"]');
inputs.forEach(input => input.disabled = true);
const msg = this.wrapper.querySelector('.rating-already');
if (msg) msg.removeAttribute('hidden');
}
// Load statistik dari server
async loadStats() {
try {
const response = await fetch(`${GAS_URL}?action=getRating&postId=${encodeURIComponent(this.postId)}`);
const result = await response.json();
if (result.success && result.data.count > 0) {
this.renderStats(result.data);
}
} catch (err) {
console.error('Failed to load stats:', err);
}
}
// Render statistik ke HTML
renderStats(data) {
const statsEl = this.wrapper.querySelector('.rating-stats');
const avgEl = statsEl.querySelector('.rating-average');
const countEl = statsEl.querySelector('.rating-count');
const barsEl = statsEl.querySelector('.rating-bars');
avgEl.textContent = data.average;
countEl.textContent = `${data.count} rating${data.count !== 1 ? 's' : ''}`;
let barsHtml = '';
for (let i = 5; i >= 1; i--) {
const count = data.distribution[i] || 0;
const percent = data.count > 0 ? (count / data.count * 100).toFixed(1) : 0;
barsHtml += `
<div class='rating-bar'>
<span class='rating-bar-star'>${i} ★</span>
<div class='rating-bar-track'>
<div class='rating-bar-fill' style='width: ${percent}%;'></div>
</div>
<span class='rating-bar-count'>${count}</span>
</div>
`;
}
barsEl.innerHTML = barsHtml;
statsEl.style.display = 'block';
}
// Update rating saat user klik
async updateRating(e) {
if (this.isSubmitting) return;
const ratingValue = parseInt(e.target.value);
// Reset semua label ke default
this.form.querySelectorAll('.rating__label').forEach(el => {
el.className = 'rating__label';
});
const ratingObject = this.ratings.find(r => r.id === ratingValue);
const prevRatingID = this.rating?.id || 0;
let delay = 0;
this.rating = ratingObject;
// Terapkan animasi delay dan tampilkan teks rating
this.ratings.forEach(rating => {
const { id } = rating;
const ratingLabel = this.form.querySelector(`[for="rating-${id}-${this.postId}"]`);
if (id > prevRatingID + 1 && id <= this.rating.id) {
++delay;
ratingLabel.classList.add(`rating__label--delay${delay}`);
}
// Tampilkan/sembunyikan teks rating (Terrible, Bad, dll)
const ratingTextEl = this.form.querySelector(`[data-rating="${id}"]`);
if (this.rating.id !== id) {
ratingTextEl.setAttribute('hidden', 'true');
} else {
ratingTextEl.removeAttribute('hidden');
}
});
// Kirim ke server
await this.submitRating(ratingValue);
}
// Submit rating ke Google Apps Script
async submitRating(rating) {
this.isSubmitting = true;
try {
const response = await fetch(GAS_URL, {
method: 'POST',
body: JSON.stringify({
action: 'submitRating',
postId: this.postId,
rating: rating
})
});
const result = await response.json();
if (result.success) {
// Tandai sudah vote
this.markAsVoted();
// Update tampilan stats
this.renderStats(result.data);
// Disable semua input
const inputs = this.form.querySelectorAll('input[type="radio"]');
inputs.forEach(input => input.disabled = true);
// Tampilkan pesan terima kasih
const thanks = this.wrapper.querySelector('.rating-thanks');
if (thanks) thanks.removeAttribute('hidden');
// Sembunyikan teks rating (Terrible, Bad, dll)
this.form.querySelectorAll('.rating__display').forEach(el => {
el.setAttribute('hidden', 'true');
});
}
} catch (err) {
console.error('Failed to submit:', err);
alert('Gagal mengirim rating. Silakan coba lagi.');
} finally {
this.isSubmitting = false;
}
}
}
// Inisialisasi saat DOM ready
document.addEventListener('DOMContentLoaded', () => {
const wrappers = document.querySelectorAll('.post-rating-wrapper');
wrappers.forEach(wrapper => {
const postId = wrapper.dataset.postId;
const form = wrapper.querySelector('.rating');
if (postId && form) {
new StarRating(form, postId);
}
});
});
})();
//]]>
</script>
</b:if>
Mengapa Sistem Ini Lebih Baik daripada Pihak Ketiga?
Banyak penyedia widget rating pihak ketiga (seperti RatingWidget atau Powr) menyisipkan iklan tersembunyi atau script pelacak yang memperlambat blog kamu. Dengan metode yang kita buat ini, kamu mendapatkan kedaulatan data penuh.
Mari kita bedah beberapa fitur unggulan dari script di atas:
1. Anti-Spam dengan LocalStorage
Kamu tentu tidak ingin satu orang memberikan rating bintang 1 sebanyak 100 kali hanya karena mereka tidak suka dengan opinimu. Script di atas sudah dilengkapi dengan proteksi LocalStorage. Begitu seseorang memberikan rating, sistem akan mengunci input untuk browser tersebut.
2. Keamanan Data di Google Sheets
Data yang dikirimkan mencakup ID Artikel, Nilai Rating, Waktu, dan User Token (berupa potongan hash dari User Agent). Hal ini memungkinkan kamu untuk melakukan audit jika terjadi anomali data di spreadsheet kamu.
3. Kecepatan Loading (Performa)
Karena menggunakan API fetch yang asinkron, widget ini tidak akan menghambat render konten utama kamu. Artikel kamu tetap akan muncul dengan cepat, sementara rating dimuat di latar belakang. Ini sangat disukai oleh algoritma Google PageSpeed Insights.
lMasalah yang Sering Muncul
Dalam memasang script ini, ada beberapa kesalahan umum yang mungkin kamu temui:
- Bintang Tidak Muncul: Pastikan kamu meletakkan kode CSS dengan benar. Cek apakah ada konflik dengan CSS tema Blogger kamu yang lama.
- Rating Tidak Berubah Setelah Klik: Cek kembali URL Web App dari Google Apps Script. Pastikan statusnya sudah "Anyone". Jika statusnya masih "Private", script tidak akan bisa menerima data dari luar.
- ID Spreadsheet Salah: Pastikan ID yang kamu masukkan di konstanta
SHEET_IDbenar-benar akurat. Jika salah satu karakter saja luput, script akan error.
Kesimpulan
Dengan widget ini, kamu sudah satu langkah di depan blogger lainnya yang masih menggunakan widget standar. Sekarang, setiap artikel yang kamu buat memiliki potensi untuk mendapatkan umpan balik langsung dari pembaca, yang pada akhirnya akan membantu kamu tumbuh menjadi penulis yang lebih baik.
Jangan lupa, jika kamu merasa artikel ini bermanfaat, silakan bagikan ke teman-teman komunitas blogger kamu. Kalau ada kesulitan saat instalasi kode di atas, langsung saja tulis di kolom komentar, ya! Aku akan bantu sebisa mungkin.
Selamat mencoba dan semoga blog kamu makin ramai dengan bintang-bintang kuning di Google!
Komentar