Adding Staticman comments to a Jekyll site
Static sites do not have a database waiting for comment submissions. Staticman takes a different route: it turns each submission into a data file in the site’s Git repository. A GitHub Pages build can then publish that file along with the rest of the Jekyll site.
With moderation enabled, Staticman opens a pull request instead of publishing the comment immediately. I like that model for a personal site because comments stay with the content and moderation uses the same review flow as any other change.
Connect Staticman
Install the Staticman GitHub app from https://github.com/apps/staticman-net on the repository.
Then add the version 3 endpoint to _config.yml as staticman_url. Replace the placeholders with the GitHub username, repository, and branch:
staticman_url: https://api.staticman.net/v3/entry/github/[YOUR USERNAME]/[YOUR REPOSITORY]/[BRANCH]/comments
The final comments segment names the configuration block that Staticman should use.
Configure submitted fields
Create staticman.yml in the root of the site. This example accepts a name, email address, URL, and message; requires the name, email, and message; hashes the email address; and sends each submission through moderation:
comments:
# (*) REQUIRED
#
# Names of the fields the form is allowed to submit. If a field that is
# not here is part of the request, an error will be thrown.
allowedFields: ["name", "email", "url", "message"]
# (*) REQUIRED
#
# Name of the branch being used. Must match the one sent in the URL of the
# request.
branch: "master"
# Text to use as the commit message or pull request title. Accepts placeholders.
commitMessage: "Add comments."
# (*) REQUIRED
#
# Destination path (filename) for the data files. Accepts placeholders.
filename: "entry{@timestamp}"
# The format of the generated data files. Accepted values are "json", "yaml"
# or "frontmatter"
format: "yaml"
# List of fields to be populated automatically by Staticman and included in
# the data file. Keys are the name of the field. The value can be an object
# with a `type` property, which configures the generated field, or any value
# to be used directly (e.g. a string, number or array)
generatedFields:
date:
type: date
options:
format: "timestamp-seconds"
# Whether entries need to be appproved before they are published to the main
# branch. If set to `true`, a pull request will be created for your approval.
# Otherwise, entries will be published to the main branch automatically.
moderation: true
# Name of the site. Used in notification emails.
name: "example.com"
# Notification settings. When enabled, users can choose to receive notifications
# via email when someone adds a reply or a new comment. This requires an account
# with Mailgun, which you can get for free at http://mailgun.com.
#notifications:
# Enable notifications
#enabled: true
# (!) ENCRYPTED
#
# Mailgun API key
#apiKey: "1q2w3e4r"
# (!) ENCRYPTED
#
# Mailgun domain (encrypted)
#domain: "4r3e2w1q"
# (*) REQUIRED
#
# Destination path (directory) for the data files. Accepts placeholders.
path: "_data/comments/{options.slug}"
# Names of required fields. If any of these isn't in the request or is empty,
# an error will be thrown.
requiredFields: ["name", "email", "message"]
# List of transformations to apply to any of the fields supplied. Keys are
# the name of the field and values are possible transformation types.
transforms:
email: md5
The branch value must match the branch in the endpoint URL. Change name, path, and the notification settings to suit the site. If email notifications are enabled, encrypt the Mailgun credentials rather than committing plain secrets.
Render comments and add the form
Add the comment list and submission form to the post layout:
<!-- Comments -->
{% if site.data.comments[page.slug] %}
<h3>
{% if site.data.comments[page.slug].size > 1 %}
{{ site.data.comments[page.slug] | size }}
{% endif %}
Comments:
</h3>
{% assign comments = site.data.comments[page.slug] | sort %}
{% for comment in comments %}
<label>
{% if comment[1].url %}
<a href="{{ comment[1].url }}">
{% endif %}
<strong>{{ comment[1].name }}</strong>
{% if comment[1].url %}
</a>
{% endif %}
</label>
<em>{{ comment[1].date | date: "%B %d, %Y" }}</em>
<p>{{ comment[1].message | markdownify }}</p>
{% endfor %}
{% endif %}
<!-- Comments Form -->
<form method="POST" action="{{ site.staticman_url }}">
<input name="options[redirect]" type="hidden" value="https://example.com">
<input name="options[slug]" type="hidden" value="{{ page.slug }}">
<label>Name</label>
<input name="fields[name]" type="text">
<label>E-mail (optional)</label>
<input name="fields[email]" type="email">
<label>Website (optional)</label>
<input name="fields[url]" type="url">
<label>Message</label>
<textarea style="width:100%" name="fields[message]" rows="12"></textarea>
<small>Comments will appear after moderation.</small>
<button type="submit">Submit comment</button>
</form>
When someone submits the form, Staticman validates the allowed and required fields, applies the configured transformations, and creates a comment file under _data/comments/{options.slug}. Because moderation is true, that file arrives in a pull request for review.
The example is intentionally small. A public form will also need a plan for spam, useful validation messages, and what happens after a failed submission. Still, storing comments as data files is a neat fit for Jekyll: there is no separate comment database to keep alive, and an approved comment becomes part of the same versioned site as the article.