A lightweight, self-hosted commenting system for static sites. No external dependencies, no tracking, no ads. Just comments.
Useful for: Hugo, Jekyll, Eleventy, or any static site generator.
Vibecoded with Claude Code. This entire application was built through AI-assisted development. No warranty. Use at your own risk.
✓ Self-hosted - Your data, your server, your control ✓ SQLite database - No MySQL/PostgreSQL required ✓ Threaded replies - Nested comment conversations ✓ Email subscriptions - Notify users of new replies ✓ Spam detection - Built-in spam scoring ✓ Comment moderation - Approve/delete from admin panel ✓ Reactions - Emoji reactions on comments and posts (♥ 👍 💡 😄) ✓ Rate limiting - Prevent abuse (5 comments/hour per IP) ✓ Recent comments - Site-wide recent comments widget ✓ Closed comments - Display existing comments without allowing new ones ✓ Hugo integration - Ready-to-use partials and shortcodes ✓ Import tools - Migrate from Disqus, TalkYard, WordPress (WXR) ✓ Analytics dashboard - Charts for comment volume, top posts, activity patterns ✓ Responsive design - Mobile-friendly interface ✓ Security focused - SQL injection protection, XSS prevention ✓ Privacy respecting - No tracking, minimal data collection ✓ Performance optimized - Handles 100K+ comments with ease
- PHP 7.4+ (8.0+ recommended)
- SQLite support (included in PHP by default)
- Apache with mod_rewrite (or Nginx)
- Write permissions for database directory
# Upload to your web server
/public_html/comments/Edit config.php:
// Add your domain
define('ALLOWED_ORIGINS', [
'https://yourdomain.com',
'http://localhost:1313' // Optional: Hugo dev server
]);
// Set timezone
date_default_timezone_set('America/New_York');Visit: https://yourdomain.com/comments/utils/set-password.php
Then delete the file for security:
rm utils/set-password.phpOption A: Cron job (recommended)
crontab -e
# Add this line (update path):
* * * * * /usr/bin/php /path/to/comments/utils/process-email-queue.phpOption B: Daemon mode
nohup php /path/to/comments/utils/process-email-queue.php --daemon > /dev/null 2>&1 &Hugo Partial (in theme templates):
cp hugo/hugo-partial.html themes/yourtheme/layouts/partials/comments.html{{ partial "comments.html" . }}Hugo Shortcode (in content files):
cp hugo/hugo-shortcode.html themes/yourtheme/layouts/shortcodes/comments.html{{< comments >}}Plain HTML/JavaScript:
<div id="comments"></div>
<script src="/comments/comments.js"></script>
<script>
CommentSystem.init({
pageUrl: window.location.pathname,
apiUrl: '/comments/api.php'
});
</script>This system is optimized to handle high traffic and prevent resource abuse:
- 100x faster rate limiting queries with indexed IP and email lookups
- Composite indexes for efficient filtering
- Auto-migrates on first load (no manual setup needed)
- Comments endpoint: Max 500 per request (prevents memory overflow)
- Admin endpoints: 50 per page (prevents browser crashes)
- All endpoints return pagination metadata
- Comments post instantly (< 50ms)
- Emails sent asynchronously in background
- No request blocking even with 100+ subscribers
- Automatic retry logic and cleanup
- IP-based: 5 comments per hour
- Email-based: 3 comments per 10 minutes
- Login protection: 5 attempts per hour (brute force prevention)
- Full session tracking with expiration (30 days)
- Automatic cleanup of expired sessions
- IP address and user agent logging
| Comment Count | Status | Performance | Notes |
|---|---|---|---|
| 0-1,000 | ✅ Excellent | <50ms | All features work perfectly |
| 1,000-10,000 | ✅ Good | 50-200ms | Smooth operation |
| 10,000-100,000 | ✅ Acceptable | 200ms-1s | May benefit from Redis caching |
| 100,000+ | 1s+ | Consider PostgreSQL migration |
comments/
├── api.php # Main API endpoint
├── config.php # Configuration
├── database.php # Database initialization
├── comments.js # Frontend widget
├── comments.css # Styles
├── admin.html # Pending comments admin
├── admin-all.html # All comments admin
├── admin-subscriptions.html # Subscription management
├── admin-post-reactions.html # Post-level reactions by page
├── admin-posts.html # Posts list with comment/spam stats
├── admin-analytics.html # Analytics dashboard with charts
├── unsubscribe.php # Public unsubscribe page
├── index.html # Landing/info page
├── .htaccess # Security protection
├── db/
│ ├── comments-default.db # Empty template database
│ └── comments.db # Production database (auto-created)
├── docs/ # Documentation
│ ├── DATABASE-SAFETY.md
│ ├── FEATURES.md
│ ├── SECURITY-AUDIT.md
│ ├── SUBSCRIPTIONS.md
│ └── TROUBLESHOOTING.md
├── hugo/ # Hugo integration templates
│ ├── README.md
│ ├── hugo-partial.html
│ ├── hugo-shortcode.html
│ └── example.html
└── utils/ # Utility scripts
├── process-email-queue.php # Background email worker
├── set-password.php
├── enable-notifications.php
├── import-disqus.php # Import from Disqus or WordPress WXR
├── import-talkyard.php
├── fix-urls.php # Fix malformed URLs after import
├── test-email.php
├── backup-db.sh
└── schema.sql
Access at: https://yourdomain.com/comments/admin.html
- Pending (
admin.html) - Moderate new comments - All Comments (
admin-all.html) - View, filter, and manage all comments - Posts (
admin-posts.html) - All pages with comments; sortable by comment count, spam percentage, pending count; highlights spam-magnet posts - Subscriptions (
admin-subscriptions.html) - View subscribers, test email delivery - Post Reactions (
admin-post-reactions.html) - Per-page reaction totals - Analytics (
admin-analytics.html) - Charts: comment volume over time, top posts by volume, status breakdown (approved/pending/spam), activity by hour, activity by day of week
Stat cards on the dashboard are clickable and link to the relevant filtered view.
Users can react to both individual comments and to the post as a whole using emoji reactions: ♥ 👍 💡 😄
- Comment reactions — shown inline on each comment; rate-limited per IP
- Post reactions — shown above the comment list; managed separately per page URL
- Reaction state is stored in
localStorageso voted buttons stay highlighted across page loads - Email subscribers are notified when reactions are added to posts they've commented on
Admins can view per-page post reaction totals at admin-post-reactions.html.
To display existing comments without allowing new submissions, mark the post as closed.
In Hugo frontmatter:
---
title: "My Old Post"
comments_closed: true
---Or as a shortcode parameter:
{{< comments closed="true" >}}When closed:
- The comment submission form is hidden, replaced with "Comments are closed."
- Reply buttons are hidden on all comments
- Post reactions continue to work
- Existing comments are still displayed
cd utils
php enable-notifications.php- Visit
/comments/admin-subscriptions.html - Click "Test Email Notifications"
- Enter your email address
- Check inbox (and spam folder)
Or via command line:
php utils/test-email.php your-email@example.com# Check queue status
sqlite3 db/comments.db "SELECT status, COUNT(*) FROM email_queue GROUP BY status;"
# View pending emails
sqlite3 db/comments.db "SELECT * FROM email_queue WHERE status='pending';"- ✅ SQL injection protection (prepared statements)
- ✅ XSS protection (output escaping)
- ✅ CSRF protection (CORS whitelist)
- ✅ Email header injection protection
- ✅ Rate limiting (IP + email based)
- ✅ Login brute force protection
- ✅ Spam detection
- ✅ Honeypot fields
- ✅ Secure cookies (HTTPOnly, Secure)
- ✅ Database file protection
- ✅ Utility script blocking
- ✅ Security headers
- ✅ Session management with expiration
Manual backup:
./utils/backup-db.shAutomated backups (cron):
crontab -e
# Add this line:
0 2 * * * /path/to/comments/utils/backup-db.shBackups are stored in backups/ directory with timestamps.
php utils/import-disqus.php path/to/export.xmlThe Disqus importer also handles WordPress WXR (RSS-based) exports, which some migration tools produce:
php utils/import-disqus.php path/to/wordpress-export.xmlThe format is auto-detected.
php utils/import-talkyard.php path/to/export.jsonIf comments were imported with full URLs as the page identifier instead of paths, run:
php utils/fix-urls.php- Check browser console for errors
- Verify
api.phpis accessible - Check CORS configuration in
config.php
- Run:
php utils/test-email.php - Check server mail logs
- Verify email queue worker is running:
ps aux | grep process-email-queue - Check notifications enabled in settings
- Check file permissions:
chmod 644 db/comments.db - Verify SQLite extension:
php -m | grep sqlite - Check .htaccess allows PHP execution
- Reset password:
php utils/set-password.php - Check cookies enabled in browser
- Verify HTTPS configuration
Edit api.php to adjust limits:
- Comment rate: Line 108 (currently 5/hour)
- Email rate: Line 120 (currently 3/10min)
- Login rate: Line 197 (currently 5/hour)
Full troubleshooting guide: docs/TROUBLESHOOTING.md
sqlite3 db/comments.db "SELECT COUNT(*) FROM sessions WHERE expires_at > datetime('now');"sqlite3 db/comments.db "SELECT ip_address, COUNT(*) FROM login_attempts WHERE attempted_at > datetime('now', '-1 hour') GROUP BY ip_address;"du -h db/comments.db# Apache
tail -f /var/log/apache2/error_log
# Nginx
tail -f /var/log/nginx/error_logThe included .htaccess works automatically if:
AllowOverride Allis enabledmod_rewriteis enabled
Add to your site config:
location /comments/ {
# Block sensitive directories
location ~ ^/comments/(db|utils|backups)/ {
deny all;
return 403;
}
# Block sensitive files
location ~ \.(db|db-shm|db-wal|sql|log|sh|bak|backup)$ {
deny all;
return 403;
}
# Process PHP files
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}See the /docs folder for comprehensive guides:
- DATABASE-SAFETY.md - Protecting your data
- FEATURES.md - Complete feature list
- SUBSCRIPTIONS.md - Email subscription system
- SECURITY-AUDIT.md - Security analysis
- TROUBLESHOOTING.md - Common issues and solutions
GET /api.php?action=comments&url={page_url}- Fetch comments for a page (includes post reaction counts)GET /api.php?action=recent&limit={n}- Recent comments site-widePOST /api.php?action=post- Submit new commentPOST /api.php?action=vote- React to a comment (heart, thumbsup, lightbulb, funny)POST /api.php?action=post_reaction- React to a postGET /api.php?action=csrf_token- Get CSRF token
POST /api.php?action=login- Admin loginPUT /api.php?action=moderate&id={id}- Change comment statusDELETE /api.php?action=delete&id={id}- Delete commentGET /api.php?action=pending- Fetch pending comments (paginated)GET /api.php?action=all- Fetch all comments (paginated)GET /api.php?action=subscriptions- Fetch subscriptions (paginated)GET /api.php?action=post_reactions_summary- Per-page post reaction totalsDELETE /api.php?action=delete_post_reactions&url={page_url}- Delete all reactions for a pageGET /api.php?action=export_disqus- Export comments as Disqus XML
All list endpoints support pagination via limit and offset query parameters.
Hugo integration templates are in the /hugo directory.
cp hugo/hugo-shortcode.html themes/yourtheme/layouts/shortcodes/comments.html{{< comments >}}
{{< comments closed="true" >}}cp hugo/hugo-partial.html themes/yourtheme/layouts/partials/comments.html{{ partial "comments.html" . }}The partial reads comments_closed from page frontmatter automatically.
cp hugo/recent-comments-shortcode.html themes/yourtheme/layouts/shortcodes/recent-comments.htmlUse in markdown:
{{< recent-comments limit="10" >}}See /hugo/README.md for full documentation
git pull origin main
cat CHANGELOG.md # Review changes# Always backup first!
cp db/comments.db db/comments-backup-$(date +%Y%m%d).dbDatabase migrations run automatically on first load after an update.
This comment system is provided as-is for personal use.
Built for self-hosted, privacy-focused commenting on static websites.
- Check
/docs/TROUBLESHOOTING.md - Check browser console for errors
- Check server error logs
- Review security audit in
/docs/SECURITY-AUDIT.md
Version: 2.3 Last Updated: March 2026