Add in-house comments (Lib\Comments)
A reusable comment thread any sidecar can attach to any page, tied to real logged-in accounts (never anonymous), auto-approved on submission with hide/delete moderation at /admin/comments — only a verified account can post, so there's no anonymous-spam vector to pre-vet against. Framework-level (novaconium/lib, novaconium/migrations, novaconium/pages), not App/migrations, matching Admin auth and Media manager's shape. A page needs its own sidecar to use it — this repo has no client-side JS, so comments ride the same server-rendered POST pattern as every other dynamic feature, which is also what excludes a page from the static cache. App/pages/blog/comments-demo/ demonstrates the pattern without touching existing posts that are referenced elsewhere as the sidecar-less example. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,13 @@
|
||||
</aside>
|
||||
<article>
|
||||
{% block blog_content %}{% endblock %}
|
||||
{# Only pages whose sidecar opts in by returning a 'comments'
|
||||
key get a thread — see /admin/docs/comments and
|
||||
App/pages/blog/hello-world/index.php. A sidecar-less post
|
||||
never has this key, so it's silently skipped. #}
|
||||
{% if comments is defined %}
|
||||
{% include '_partials/comments/thread.twig' %}
|
||||
{% endif %}
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
// Demonstrates attaching Lib\Comments to a page — see /admin/docs/comments.
|
||||
// This is the one post under App/pages/blog/ with a sidecar, specifically
|
||||
// so it can carry a live comment thread; giving it one is what excludes
|
||||
// it from the static HTML cache (Renderer::render() only ever caches
|
||||
// sidecar-less pages) — every other post stays sidecar-less/cached and
|
||||
// has no thread, since blog/_layout/layout.twig only includes one when
|
||||
// 'comments' is present in the returned context.
|
||||
|
||||
use App\AdminAuth;
|
||||
use App\Response;
|
||||
use Lib\Comments;
|
||||
use Lib\Csrf;
|
||||
use Lib\FormValidator;
|
||||
use Lib\Input;
|
||||
use Lib\SpamGuard;
|
||||
|
||||
$pagePath = Comments::currentPagePath();
|
||||
$user = AdminAuth::currentUser();
|
||||
$spamGuard = new SpamGuard();
|
||||
$commentError = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!Csrf::verify(Input::post('csrf_token'))) {
|
||||
return Response::redirect($pagePath . '?error=security');
|
||||
}
|
||||
|
||||
if ($user === null) {
|
||||
return Response::redirect($pagePath . '?error=login');
|
||||
}
|
||||
|
||||
$body = Input::post('body', '');
|
||||
$validator = (new FormValidator())
|
||||
->required($body, 'body', 'Enter a comment.')
|
||||
->maxLength($body, 'body', 2000, 'Comments are 2000 characters max.');
|
||||
|
||||
if ($validator->passes()) {
|
||||
// Same "bot gets an identical response" reasoning as the contact
|
||||
// form — see App/pages/contact/index.php. Only the insert is
|
||||
// skipped for spam.
|
||||
if (!$spamGuard->isSpam(Input::post())) {
|
||||
Comments::create($pagePath, $user['id'], $body);
|
||||
}
|
||||
|
||||
return Response::redirect($pagePath);
|
||||
}
|
||||
|
||||
$commentError = $validator->errors()['body'] ?? null;
|
||||
}
|
||||
|
||||
return [
|
||||
'comments' => Comments::forPage($pagePath),
|
||||
'currentUser' => $user,
|
||||
'commentError' => $commentError,
|
||||
'renderedAt' => $spamGuard->renderedAt(),
|
||||
'csrfField' => Csrf::fieldName(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends layout %}
|
||||
|
||||
{% import '_layout/icons.twig' as icons %}
|
||||
|
||||
{% block title %}Comments Demo{% endblock %}
|
||||
{% block description %}A worked example of Lib\Comments — this post has its own sidecar, unlike every other post here, specifically so it can carry a live comment thread.{% endblock %}
|
||||
|
||||
{% block robots %}index, follow{% endblock %}
|
||||
{% block tags %}comments, meta{% endblock %}
|
||||
{% block canonical %}{{ request_path|default('/') }}{% endblock %}
|
||||
|
||||
{% block og_type %}article{% endblock %}
|
||||
{% block og_title %}{{ block('title') }}{% endblock %}
|
||||
{% block og_description %}{{ block('description') }}{% endblock %}
|
||||
{% block og_url %}{{ block('canonical') }}{% endblock %}
|
||||
|
||||
{% block twitter_card %}summary{% endblock %}
|
||||
{% block twitter_title %}{{ block('title') }}{% endblock %}
|
||||
{% block twitter_description %}{{ block('description') }}{% endblock %}
|
||||
|
||||
{% block blog_content %}
|
||||
<h1>Comments Demo</h1>
|
||||
|
||||
<p>Unlike every other post under <code>App/pages/blog/</code>, this one has its own <code>index.php</code> sidecar — <code>App/pages/blog/comments-demo/index.php</code> — which reads and writes a <code>comments</code> table via <code>Lib\Comments</code> and returns a <code>comments</code> key in its context. <code>App/pages/blog/_layout/layout.twig</code> only includes the comment-thread partial (<code>novaconium/pages/_partials/comments/thread.twig</code>) when that key is present, so this is the only post here with a thread below.</p>
|
||||
|
||||
<p>Having a sidecar means this page is never served from the static HTML cache the way its sidecar-less siblings are (see <a class="icon-link" href="/admin/docs/caching">{{ icons.book() }}Static caching</a>) — an explicit, per-page tradeoff you accept the moment a page needs comments. See <a class="icon-link" href="/admin/docs/comments">{{ icons.users() }}Comments</a> for the full write-up of <code>Lib\Comments</code>, including why comments are tied to real logged-in accounts rather than anonymous name/email fields, and why they're auto-approved with after-the-fact moderation at <code>/admin/comments</code> rather than a pending queue.</p>
|
||||
{% endblock %}
|
||||
@@ -46,5 +46,11 @@ return [
|
||||
'excerpt' => 'A tour of what ships with novaconium out of the box: routing, sidecars, caching, admin auth, access control, media manager, database, search, RSS, and more.',
|
||||
'published' => '2026-07-15',
|
||||
],
|
||||
[
|
||||
'slug' => 'comments-demo',
|
||||
'title' => 'Comments Demo',
|
||||
'excerpt' => 'A worked example of Lib\\Comments — this post has its own sidecar, unlike every other post here, specifically so it can carry a live comment thread.',
|
||||
'published' => '2026-07-15',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<li><strong>Access control</strong> — assign a page (or a section, one line per page) to a user or group from its sidecar: <code>Access::require('group:members')</code> returns <code>null</code> or a ready-made <code>Response</code> (login redirect with a return path, or a 404 for the wrong account). Public is the default — a sidecar that never calls it is untouched, and static (sidecar-less, cached) pages are always public by construction.</li>
|
||||
<li><strong>Draft pages</strong> — list a route under <code>draft_routes</code> in <code>App/config.php</code> to make it visible only to an authenticated admin; anyone else gets a plain 404, not a login prompt.</li>
|
||||
<li><strong>Media manager</strong> — <code>/admin/media</code>, an upload/browse/delete UI for files under <code>public/uploads/</code>, covered by the existing <code>/admin/*</code> auth gate with no separate flag needed. Extension allowlist and max upload size are configurable.</li>
|
||||
<li><strong>Comments</strong> — <code>Lib\Comments</code>, a reusable comment thread any page can attach to itself via its own sidecar (see <code>App/pages/blog/comments-demo/</code>). Tied to real logged-in accounts, not anonymous name/email fields; auto-approved on submission with after-the-fact hide/delete moderation at <code>/admin/comments</code>.</li>
|
||||
<li><strong>Dark/light theme toggle</strong> — a nav button flips a <code>data-theme</code> attribute (persisted to <code>localStorage</code>) that swaps every color via CSS custom properties.</li>
|
||||
<li><strong>Self-hosted spam prevention & form validation</strong> — <code>Lib\SpamGuard</code> (honeypot + submission-timing check, no external CAPTCHA), <code>Lib\FormValidator</code>, and <code>Lib\Validate</code>, demonstrated on the contact form.</li>
|
||||
<li><strong>Form security by default</strong> — <code>Lib\Input</code> (cleaning accessor for <code>$_POST</code>/<code>$_GET</code>) and <code>Lib\Csrf</code> (standalone session-token CSRF protection), wired into the contact form and every admin form.</li>
|
||||
|
||||
Reference in New Issue
Block a user