A lightweight PHP (8+) comments service for static sites and blogs.
It provides:
- An embeddable frontend widget (
/public/embed.js) - A JSON API for fetching/submitting comments (
/api/...) - A root admin panel (
/) with moderation and author replies - First-run setup (
/setup.php) that createsconfig.phpand the SQLite DB
index.phpis the admin UI (root path/)login.phphandles admin sign-insettings.phphandles admin settings updateslogout.phpends the admin sessionsetup.phpis first-run installer (only whenconfig.phpis missing - will delete after first run)api/index.phpis the API front controllerpublic/embed.jsis the embed script used on your blog/siteincludes/contains auth, DB, rendering, SES, session hardening, and helpers
- SQLite-backed comments with threaded replies
- Markdown input with safe HTML rendering (Parsedown safe mode)
- Optional commenter email encryption using libsodium
- Optional commenter website links
- Spam checks on submission (honeypot + human challenge)
- Configurable human challenge question/answer (set in Setup/Settings)
- Admin moderation for pending/published comments
- Author replies from admin panel
- Auto-publish pending comment when replying as author
- Thread delete from root comment (
Delete threadremoves descendants) - Admin pagination (20 primary threads per page)
- Admin update manager (check GitHub release, inspect package changes, apply update, restore backup)
- Login rate limiting (5 failed attempts in 5 minutes)
- Session hardening (
HttpOnly,SameSite, strict mode, secure-on-HTTPS)
- PHP 8+
- PHP extensions:
pdo_sqlitesodiumcurl(for SES email sending)
- Apache or nginx
- Write access for PHP user to:
- project root (during setup, to create
config.php) db/directory (for SQLite and login rate-limit store)
- project root (during setup, to create
- Deploy
purecommentsto your comments domain/subdomain or a subfolder path. - Ensure filesystem permissions allow creating
config.phpand writingdb/. - Visit
/setup.phpand complete setup. - Sign in at
/login.php. - Moderate comments at
/.
- Run setup immediately after deployment.
- Complete setup before exposing the service publicly.
- After setup completes,
setup.phpis automatically deleted.
setup.php writes config.php with:
db_pathusing a portable relative expression:__DIR__ . '/db/comments.sqlite'
admin_usernameadmin_password_hashsodium_keytimezonedate_formatprivacy_policy_urlpost_base_urlauthordetailsawsSES detailsmoderationdetails
config.php is intentionally blocked via root .htaccess.
Optional advanced setting:
app_base_path: Force URL prefix for admin/API routes if auto-detection is wrong behind a reverse proxy (examples:''for root,'/comments'for subfolder installs).
- Admin auth is session-based (not HTTP Basic auth).
- Login requires username/password from
config.php. - Passwords are verified against
admin_password_hash. - Login attempts are rate-limited by username+IP.
- Root
.htaccesssupports IP allowlisting forindex.phpandlogin.php- just uncomment lines 09-12 and change the example IP addresses.
Base path: /api
GET /api/comments/{post_slug}- Returns published comments for a post as a tree
POST /api/submit-comment- Accepts JSON body with fields like:
post_slug,name,email,website,content,parent_id,surname,trap_field
- Stores comment as
pending - Sends moderation email (if SES configured)
- Accepts JSON body with fields like:
If config.php is missing, API returns 503 with a setup hint.
Rewrite-free fallback endpoints (supported automatically by embed.js):
GET /api/index.php?endpoint=comments/{post_slug}POST /api/index.php?endpoint=submit-comment
Add this where you want comments:
<div id="comments"></div>
<script src="https://comments.example.com/public/embed.js" defer></script>embed.js needs a post identifier so it knows which comment thread to load and where new comments should be submitted.
By default, it infers that slug from the current page URL.
You can optionally use data-post-slug when URL-based detection is not what you want.
Common cases:
- Your post URL may change over time (slug edits, permalink migrations, trailing slash changes).
- You want two different URLs to share one comment thread (canonical + alternate route).
- You use query-string or hash-based routing and want a clean, stable identifier per post.
<div id="comments" data-post-slug="my-first-post"></div>
<script src="https://comments.example.com/public/embed.js" defer></script>Use one stable value per post. Different slugs create different, isolated threads.
embed.js normally infers the API base URL from its own src. So if your script URL is:
https://comments.example.com/public/embed.js
it will call:
https://comments.example.com/api/comments/{slug}https://comments.example.com/api/submit-comment
You can optionally use data-base-url when automatic detection is not what you want.
Common cases:
- You serve
embed.jsvia a CDN, but your API is on a different origin. - You proxy static assets and API through different domains.
- You want to force a specific API origin for testing.
<script src="https://comments.cdn.example.com/public/embed.js" data-base-url="https://comments.example.com" defer></script>Pure Comments includes a frontend example stylesheet at public/comments.css.
You have two simple options:
- Copy the rules you want into your site stylesheet.
- Link it directly on pages where comments are shown.
Example:
<link rel="stylesheet" href="https://comments.example.com/public/comments.css">embed.js only renders markup and behavior, so the visual style is fully up to you. Start with the example file, then adjust spacing, colours, typography, and badges to match your site.
- Comments are grouped into:
- Pending comments
- Published comments
- Each section paginates independently.
- Admin view is thread-oriented with accordion
details. - Replies display in chronological order within a thread.
- Thread root uses
Delete thread; replies useDelete reply. - Replying to a pending comment publishes it and posts your reply together.
Via Amazon SES (includes/ses.php):
- Moderation notifications on new pending comments
- Reply notifications when someone is replied to
If SES config values are empty, email sending will fail silently from a user perspective unless you add custom handling/logging.
Run with PHP built-in server:
php -S 127.0.0.1:8000 -t .Then open:
http://127.0.0.1:8000/setup.php(first run)http://127.0.0.1:8000/login.phphttp://127.0.0.1:8000/
index.php- admin panellogin.php- login formsettings.php- authenticated settings pagelogout.php- logout endpointsetup.php- first-run installerupdates.php- admin updates page (release check + apply + backup restore)api/index.php- comments APIincludes/admin_auth.php- auth + rate limiting helpersincludes/config_builder.php- sharedconfig.phpgeneration helperincludes/session.php- secure session bootstrapincludes/db.php- DB access and schema initincludes/render.php- admin rendering + thread helpersincludes/ses.php- Amazon SES sendingpublic/embed.js- embeddable comment UIpublic/style.css- shared UI stylesheetpublic/icons/sprite.svg- SVG icon sprite.htaccess- config deny + optional admin IP allowlist
- Keep
config.phpout of version control. - Keep DB (
db/comments.sqlite) out of version control. - Use HTTPS only.
- Rotate admin password periodically.
- Consider log/monitoring around failed logins and SES errors.
db_path: Filesystem path to your SQLite DB. Settings field:No(setup-generated/manual).admin_username: Username for admin login. Settings field:Yes(Admin username).admin_password_hash: Password hash for admin login (required for auth). Settings field:Yes(written when settingAdmin password).sodium_key: Secret key used to encrypt/decrypt commenter emails. Do not rotate casually. Settings field:No(preserved automatically).timezone: Timezone used when displaying timestamps in the admin UI. Settings field:Yes(Timezone).date_format: PHP date format string used for admin timestamp display. Settings field:Yes(Date format).privacy_policy_url: URL used by the frontend “Read the comment privacy notice” link. Settings field:Yes(Privacy policy URL).spam_challenge.question: Human check question shown on the comment form. Settings field:Yes(Challenge question).spam_challenge.answer: Expected answer for the human check (validated case-insensitively). Settings field:Yes(Challenge answer).spam_challenge.placeholder: Optional placeholder text for the human check input. Settings field:Yes(Challenge placeholder).post_titles: Optional slug-to-title map used in admin/email contexts for nicer post titles. Settings field:No(manual advanced config).post_base_url: Base URL of your blog posts (used to build links in admin emails). Settings field:Yes(Post base URL).author.name: Name used for “reply as author”. Settings field:Yes(Author name).author.email: Email used for “reply as author” and author-comment detection. Settings field:Yes(Author email).aws.region: AWS SES region (for sending moderation/reply emails). Settings field:Yes(AWS region).aws.access_key: AWS SES access key. Settings field:Yes(AWS access key).aws.secret_key: AWS SES secret key. Settings field:Yes(AWS secret key).aws.source_email: From email address for SES messages. Settings field:Yes(SES source email).aws.source_name: Optional display name for SES from address. Settings field:Yes(SES source name).moderation.notify_email: Email that receives “new comment awaiting moderation” notifications. Settings field:Yes(Moderation notify email).moderation.base_url: Base URL of this comments service, used in moderation emails. Settings field:Yes(Comments service URL).