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.

  1. Di Google Sheets tadi, klik Extensions > Apps Script.
  2. 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='&quot;rating-form-&quot; + data:post.id'>
    <div class='rating__stars'>
      <!-- Star 1 -->
      <input class='rating__input rating__input-1' expr:id='&quot;rating-1-&quot; + data:post.id' name='rating' type='radio' value='1'/>
      <label class='rating__label' expr:for='&quot;rating-1-&quot; + 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='&quot;rating-2-&quot; + data:post.id' name='rating' type='radio' value='2'/>
      <label class='rating__label' expr:for='&quot;rating-2-&quot; + 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='&quot;rating-3-&quot; + data:post.id' name='rating' type='radio' value='3'/>
      <label class='rating__label' expr:for='&quot;rating-3-&quot; + 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='&quot;rating-4-&quot; + data:post.id' name='rating' type='radio' value='4'/>
      <label class='rating__label' expr:for='&quot;rating-4-&quot; + 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='&quot;rating-5-&quot; + data:post.id' name='rating' type='radio' value='5'/>
      <label class='rating__label' expr:for='&quot;rating-5-&quot; + 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} &#9733;</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_ID benar-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!

Aurealisa ID
Fakta Gambar (Science Fact), Kutipan (Quote Of The Day), Ulasan (Science Review), Tips, Artikel.

Komentar