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