1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
|
#+date: <2021-04-23>
#+title: Roll Your Own Static Commenting System in PHP
#+description:
#+slug: php-comment-system
* The Terrible-ness of Commenting Systems
The current state of affairs regarding interactive comment systems is,
well, terrible. It is especially awful if you're a privacy conscious
person who does not generally load third-party scripts or frames on the
websites you visit.
Even further, many comment systems are charging exorbitant fees for
something that should be standard.
Of course, there are some really terrible options:
- Facebook Comments
- Discourse
There are some options that are better but still use too many scripts,
frames, or social integrations on your web page that could impact some
users:
- Disqus
- Isso
- Remark42
Lastly, I looked into a few unique ways of generating blog comments,
such as using Twitter threads or GitHub issues to automatically post
issues. However, these both rely on external third-party sites that I
don't currently use.
* Stay Static with Server-Side Comments
The main issue for my personal use-case is that my blog is completely,
100% static. I use PHP on the back-end but website visitors only see
HTML and a single CSS file. No external javascript and no embedded
frames.
So, how do we keep a site static and still allow users to interact with
blog posts? The key actually pretty simple - I'm already using PHP, so
why not rely on the classic HTML =<form>= and a PHP script to save the
comments somewhere? As it turns out, this was a perfect solution for me.
The second issue for my personal use-case is that I am trying to keep
the contents of my website accessible over time, as described by
[cite/t:@brandur], in his post entitled
[[https://brandur.org/fragments/graceful-degradation-time][Blog with
Markdown + Git, and degrade gracefully through time]] .
This means I cannot rely on a database for comments, since I do not rely
on a database for any other part of my websites.
I blog in plain Markdown files, commit all articles to Git, and ensure
that future readers will be able to see the source data long after I'm
gone, or the website has gone offline. However, I still haven't
committed any images served on my blog to Git, as I'm not entirely sold
on Git LFS yet - for now, images can be found at
[[https://img.cleberg.net][img.cleberg.net]].
Saving my comments back to the Git repository ensures that another
aspect of my site will degrade gracefully.
* Create a Comment Form
Okay, let's get started. The first step is to create an HTML form that
users can see and utilize to submit comments. This is fairly easy and
can be changed depending on your personal preferences.
Take a look at the code block below for the form I currently use. Note
that =<current-url>= is replaced automatically in PHP with the current
post's URL, so that my PHP script used later will know which blog post
the comment is related to.
The form contains the following structure:
1. =<form>= - This is the form and will determine which PHP script to
send the comment to.
2. =<section hidden>= - This section is hidden from the user and is used
to ensure that we know which blog post sent the comment.
3. =<section>= Display Name (Optional) - Used to accept a display name,
if entered.
4. =<section>= Comment (Required) - Used to accept the user's full
comment. Markdown is allowed.
5. =<button>= - A button to submit the form.
#+begin_src html
<form action="/comment.php" method="POST">
<h3>Leave a Comment</h3>
<section hidden>
<label class="form-label" for="postURL">Post URL</label>
<input
class="form-control"
id="postURL"
name="postURL"
type="text"
value="<current-url>"
/>
</section>
<section>
<label class="form-label" for="userName">Display Name</label>
<input
class="form-control"
id="userName"
name="userName"
placeholder="John Doe"
type="text"
/>
</section>
<section>
<label class="form-label" for="userContent">Your Comment</label>
<textarea
class="form-control"
id="userContent"
name="userContent"
rows="3"
placeholder="# Feel free to use Markdown"
aria-describedby="commentHelp"
required
></textarea>
<div id="commentHelp" class="form-text">
Comments are saved as Markdown and cannot be edited or deleted.
</div>
</section>
<button type="submit">Submit</button>
</form>
#+end_src
* Handle Comments via POST
Now that we have a form and can submit comments, we need to create a PHP
script so that the server can fetch the comment data and save it. Make
sure your PHP script name matches the name you entered in the =action=
field in your form.
See the code block below for the contents of my =comment.php= script. We
perform the following tasks in this script:
1. Grab the POST data from the HTML form.
2. Sanitize the comment data with =htmlentities=.
3. Set the display name to =Anonymous= if it was left blank.
4. Create a PHP object that combines all of this data.
5. Check if our destination file =comments.json= exists.
6. If so, convert the PHP object to JSON and save it to the file.
7. If the =comments.json= file does not exist, the script will exit with
an error. You can alter this to ensure it creates the script, but my
source code includes the file by default, so it will always exist.
8. Finally, send the user back to the =#comments= section of the blog
post they just read.
#+begin_src php
// Get the content sent from the comment form
$comment = htmlentities($_POST['userContent']);
$post_url = $_POST['postURL'];
// Set default values if blank
if (isset($_POST['userName']) && trim($_POST['userName']) !== "") {
$username = $_POST['userName'];
} else {
$username = 'Anonymous';
}
// Create an empty PHP object
$user_object = new stdClass();
// Add object content
$user_object->timestamp = date('Y-m-d H:i:s');
$user_object->username = $username;
$user_object->comment = $comment;
$user_object->post_url = $post_url;
// Append JSON to file
$file_name = 'comments.json';
if (file_exists($file_name)) {
$source_data = file_get_contents($file_name);
$temp_array = json_decode($source_data);
array_push($temp_array, $user_object);
$json_data = json_encode($temp_array, JSON_PRETTY_PRINT);
file_put_contents($file_name, $json_data);
} else {
die('Error: The "comments.json" file does not exist.');
}
// Send the user back
header('Location: ' . $post_url . '#comments');
#+end_src
If you're using Apache, make sure the =www-data= user on your server has
the correct permissions to your website directory or else it will not be
able to write to =comments.json=.
#+begin_src sh
chgrp -R www-data /path/to/website/
chmod -R g+w comments.json
#+end_src
* Display User Comments
Now that we can submit comments, and they are saved to the
=comments.json= file, let's make sure we can show those comments in each
blog post.
The code block below shows the function I use to decode my
=comments.json= file, check if the comments apply to the current post,
and then display them.
This piece of code should *really* be inside a function (or at least in
an organized PHP workflow). Don't just copy-and-paste and expect it to
work. You need to at least supply a =$query= variable depending on the
page visited.
#+begin_src php
$query = 'your-blog-post.html';
// Load saved comments
$comments_file = 'comments.json';
$comments_raw = file_get_contents($comments_file);
$comments = json_decode($comments_raw);
$comment_section = '<section id="comments" class="comments"><h3>Comments</h3>';
foreach ($comments as $comment) {
if ($comment->post_url == "https://example.com/post/" . $query) {
// Assign metadata to variables
$comment_timestamp = $comment->timestamp;
$comment_username = $comment->username;
$comment_content = $comment->comment;
// Parse the comment, in case it contains Markdown
$comment_md = Parsedown::instance()->text($comment_content);
$comment_html = new DOMDocument();
$comment_html->loadHTML($comment_md);
$comment_html_links = $comment_html->getElementsByTagName('a');
foreach ($comment_html_links as $comment_html_link) {
$comment_html_link->setAttribute('rel', 'noreferrer');
$comment_html_link->setAttribute('target', '_blank');
}
$comment_secured_html = $comment_html->saveHTML();
// Apply metadata to comments section
$comment_section .= '<div class="user-comment"><div class="row"><label>Timestamp:</label><p>' . $comment_timestamp . '</p></div><div class="row"><label>Name:</label><p>' . $comment_username . '</p></div><div class="row markdown"><label>Comment:</label><div class="comment-markdown">' . $comment_secured_html . '</div></div></div>';
}
}
echo $comment_section;
#+end_src
* Bonus: Create a 'Recent Comments' Page
Finally, the last part of my current system is to create a Recent
Comments page so that I can easily check-in on my blog and moderate any
spam. As an alternative, you could use PHP's =mail()= function to email
you for each blog comment.
The code to do this is literally the same code as the previous section,
I just make sure it is printed when someone visits
=https://example.com/comments/=.
* Possible Enhancements
This comment system is by no means a fully-developed system. I have
noted a few possible enhancements here that I may implement in the
future:
- Create a secure moderator page with user authentication at
=https://blog.example.com/mod/=. This page could have the option to
edit or delete any comment found in =comments.json=.
- Create a temporary file, such as =pending_comments.json=, that will
store newly-submitted comments and won't display on blog posts until
approved by a moderator.
- Create a =/modlog/= page with a chronological log, showing which
moderator approved which comments and why certain comments were
rejected.
|