Home
Contact Terms Privacy Catalog About

How to Add a Rating System to Blogger with Firebase

Implementing Serverless Rating Systems on Blogger via Firebase

Blogger lacks native database connectivity requiring external state management for user ratings. Firebase Realtime Database provides the necessary low-latency synchronization without server-side code injection. This implementation uses client-side JavaScript to read/write rating aggregates directly to a NoSQL structure.

The system relies on unique post identifiers to namespace data entries. Each blog post acts as a distinct key within the Firebase tree preventing data collision across the site. Users interact with a static interface that updates dynamically upon database confirmation.

Database Schema Architecture

Structure the Firebase Realtime Database to store aggregate counts and prevent duplicate voting from single sessions. The root node contains a ratings object where each child key matches a specific Blogger post ID.

Store total score and vote count separately to calculate averages on the fly. This approach minimizes read operations by avoiding complex queries during page load.

{
  "ratings": {
    "389204829384729384": {
      "totalScore": 14,
      "voteCount": 3,
      "average": 4.67
    },
    "827364827364827364": {
      "totalScore": 5,
      "voteCount": 1,
      "average": 5.0
    }
  }
}

Initialize the database rules to allow read access globally but restrict writes to validated data types. This prevents malicious actors from injecting non-numeric values into the score fields.

Firebase Configuration and Initialization

Obtain the project configuration object from the Firebase Console under Project Settings. Insert these credentials into a dedicated script block within the Blogger theme HTML or specific post layout.

Load the Firebase SDK version 9+ using modular imports to reduce bundle size. The initialization code establishes the connection before any DOM interaction occurs.

<script type="module">
  import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js";
  import { getDatabase, ref, onValue, update, runTransaction } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-database.js";

  const firebaseConfig = {
    apiKey: "YOUR_API_KEY",
    authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
    databaseURL: "https://YOUR_PROJECT_ID-default-rtdb.firebaseio.com",
    projectId: "YOUR_PROJECT_ID",
    storageBucket: "YOUR_PROJECT_ID.appspot.com",
    messagingSenderId: "YOUR_SENDER_ID",
    appId: "YOUR_APP_ID"
  };

  const app = initializeApp(firebaseConfig);
  const db = getDatabase(app);
</script>

Replace placeholder strings with actual values from the console output. Failure to match the databaseURL exactly results in immediate connection timeouts.

Post ID Extraction Logic

Blogger generates unique numeric IDs for every post accessible via the data:post.id tag or URL parameters. Extract this value to serve as the primary key for the database transaction.

Use JavaScript to parse the current window location or inject the ID directly via Blogger's data tags. Consistency in ID generation ensures the rating widget loads the correct data for every page view.

function getPostId() {
  const path = window.location.pathname;
  const parts = path.split('/');
  // Assumes URL structure /YYYY/MM/post-title.html or similar
  // Prefer injecting via <data:post.id/> if inside loop
  const postId = parts[parts.length - 1].replace('.html', '').replace(/-/g, ''); 
  // Fallback or direct injection recommended:
  return window.BLOGGER_POST_ID || postId; 
}

Injecting the ID via <script>window.BLOGGER_POST_ID = "<data:post.id/>";</script> inside the post body provides the highest reliability. This avoids regex parsing errors on custom URL structures.

Realtime Data Synchronization

Attach a listener to the specific database reference to update the UI instantly when values change. The onValue method triggers whenever the aggregate score or vote count updates in the cloud.

Calculate the average rating dynamically within the callback function to ensure display accuracy. Render the star visualization based on the computed float value.

function initRatingWidget(postId) {
  const ratingRef = ref(db, `ratings/${postId}`);
  const starContainer = document.getElementById('star-display');
  const countContainer = document.getElementById('vote-count');

  onValue(ratingRef, (snapshot) => {
    const data = snapshot.val();
    if (!data) {
      renderStars(0, 0);
      return;
    }
    const avg = data.totalScore / data.voteCount;
    renderStars(avg, data.voteCount);
  });
}

function renderStars(average, count) {
  const fullStars = Math.floor(average);
  const hasHalf = average % 1 >= 0.5;
  let html = '';
  
  for (let i = 0; i < 5; i++) {
    if (i < fullStars) html += '★';
    else if (i === fullStars && hasHalf) html += '½';
    else html += '☆';
  }
  
  document.getElementById('star-display').textContent = html;
  document.getElementById('vote-count').textContent = `(${count} votes)`;
}

Handle null states gracefully by displaying empty stars when no votes exist yet. This prevents UI breaks during the initial load phase before data retrieval completes.

Atomic Vote Submission

Use Firebase transactions to prevent race conditions when multiple users vote simultaneously. The runTransaction method ensures the read-modify-write cycle occurs atomically on the server.

Implement local storage checks to restrict users to a single vote per device. This basic mitigation reduces spam without requiring complex authentication flows.

function submitVote(postId, score) {
  if (localStorage.getItem(`voted_${postId}`)) {
    alert('You have already rated this post.');
    return;
  }

  const postRef = ref(db, `ratings/${postId}`);
  
  runTransaction(postRef, (currentData) => {
    if (currentData === null) {
      return { totalScore: score, voteCount: 1, average: score };
    }
    const newCount = currentData.voteCount + 1;
    const newTotal = currentData.totalScore + score;
    return {
      totalScore: newTotal,
      voteCount: newCount,
      average: newTotal / newCount
    };
  }).then(() => {
    localStorage.setItem(`voted_${postId}`, 'true');
    alert('Vote recorded successfully.');
  }).catch((error) => {
    console.error('Vote failed:', error);
  });
}

Bind this function to click events on star elements passing the integer score (1-5). Verify the transaction success callback before updating the local UI state to reflect the new average.

HTML Interface Integration

Construct the rating widget using semantic HTML elements styled with CSS variables for theme compatibility. Place the container within the post body where the script expects to find the target IDs.

Style the stars using the defined gold accent colors to match the site's dark and light modes. Ensure the layout remains responsive across mobile and desktop viewports.

<div class="rating-widget" style="width: 100%; display: flex; flex-direction: column; align-items: center; gap: 8px;">
  <div id="star-display" style="font-size: 24px; color: var(--gold); letter-spacing: 4px;">☆☆☆☆☆</div>
  <div id="vote-count" style="font-size: var(--text-sm); color: var(--text-muted);">(0 votes)</div>
  <div class="star-inputs" style="display: flex; gap: 4px;">
    <button onclick="submitVote(window.BLOGGER_POST_ID, 1)" aria-label="1 star">★</button>
    <button onclick="submitVote(window.BLOGGER_POST_ID, 2)" aria-label="2 stars">★</button>
    <button onclick="submitVote(window.BLOGGER_POST_ID, 3)" aria-label="3 stars">★</button>
    <button onclick="submitVote(window.BLOGGER_POST_ID, 4)" aria-label="4 stars">★</button>
    <button onclick="submitVote(window.BLOGGER_POST_ID, 5)" aria-label="5 stars">★</button>
  </div>
</div>

Add CSS rules to hide the input buttons after voting to prevent confusion. Use the --accent-primary variable for hover states to maintain visual consistency with the rest of the DoxLayer design system.

Security Rules Configuration

Define Firebase Realtime Database rules to enforce data integrity at the server level. Allow reads publicly but validate that writes only increment numeric fields.

Restrict updates to ensure the voteCount always increases by exactly one and the totalScore increases by a valid rating value. This logic prevents clients from setting arbitrary high scores.

{
  "rules": {
    "ratings": {
      "$postId": {
        ".read": true,
        ".write": true, 
        ".validate": "newData.hasChildren(['totalScore', 'voteCount', 'average'])",
        "totalScore": { ".validate": "newData.val() instanceof number" },
        "voteCount": { ".validate": "newData.val() instanceof number" },
        "average": { ".validate": "newData.val() instanceof number" }
      }
    }
  }
}

Note that fully secure validation of increment logic requires Cloud Functions which are not available on the free Blogger tier without external hosting. The client-side local storage lock serves as the primary deterrent for casual manipulation.

For higher security needs consider migrating the write logic to a serverless function triggered by an API call. This adds latency but guarantees vote validity.

Troubleshooting and Verification

Verify implementation by checking the browser console for Firebase connection errors or permission denials. Use the Firebase Console Data tab to inspect if the ratings node populates upon submission.

Test cross-post isolation by voting on one article and confirming the count does not affect others. Clear local storage to simulate a new user session for repeated testing.

Common failures include mismatched Post IDs causing data to write to the wrong key. Ensure the ID extraction method matches the actual URL structure or injected data tag value exactly.

Refer to the Blogger Schema Validator to ensure your post markup supports the required data injection points. Audit your template using the Blogger Template SEO Auditor to verify script placement does not block rendering.

Tools You Might Like

Handpicked utilities everyone is using right now