# Static comments script for Hugo (in PHP)

**URL:** <https://discourse.gohugo.io/t/static-comments-script-for-hugo-in-php/51924>\
**Category:** tips & tricks\
**Created:** [October 13, 2024, 2:57am UTC](https://discourse.gohugo.io/t/static-comments-script-for-hugo-in-php/51924 "2024-10-13T02:57:40Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![dertuxmalwieder](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/dertuxmalwieder/32/496_2.png) [@dertuxmalwieder](https://discourse.gohugo.io/u/dertuxmalwieder)\
**Post date:** [October 13, 2024, 2:57am UTC](https://discourse.gohugo.io/t/static-comments-script-for-hugo-in-php/51924/1 "2024-10-13T02:57:41Z")

</div>

I finally managed to fix a few annoying issues 😉 with my first approach, and this one finally works: _Baking **static, Markdown-ready comments** right into your Hugo pages/posts_ without a subscription or even a GitHub account. All you need is PHP [with YAML support](https://www.php.net/manual/en/book.yaml.php) on the server that serves your Hugo page. 🙂

It works like this:

1. A random internet user comments on `/2024/10/foo`.
2. The script writes the comment into `/content/2024/10/foo/comments/`.
3. A page rebuild is triggered.
4. Hugo will automatically add the new comment’s contents to the output page.

(I recommend some kind of moderation here, but you’re free to establish your own pipeline.)

**The script:**

```php
<?php
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);

$timestamp_file = (new \DateTime())->format('Y-m-d\TH.i.s.u');
$timestamp_object = (new \DateTime())->format('Y-m-d H:i:s.uZ');
$comment = array(
    "post_id" => $_POST["postid"],
    "timestamp" => $timestamp_object,
    "author" => $_POST["author"],
    "email" => $_POST["email"],
    "link" => $_POST["url"],
    "comment" => $_POST["comment"]
);
$hash = substr(hash('md5', $_POST["comment"]), 0, 6);

$commentdir = dirname( __FILE__ ) . "/content/" . str_replace("/blog", "", $_POST["storage"]) . "comments/";
$filepath = $commentdir . $timestamp_file . "-" . $hash . ".yml";

if (!file_exists($commentdir)) {
    mkdir($commentdir, 0777, true);
}
$success = yaml_emit_file($filepath, $comment);

if ($success) {
    exec("cd " . dirname( __FILE__ ) . " && hugo");
    header("Location: " . $_POST["postid"]);
}
else {
    echo "Error. :-("; // << Here's a good place to log it...
}
?>

```

**`themes/quux/layouts/partials/comment-form.html`:**

```html
<form action="comments.php" method="POST" id="commentform">
  <input type="hidden" name="postid" value="{{ .RelPermalink }}" />
  <input type="hidden" name="storage" value="{{ .File.Dir }}" />
  <p class="comment-form-author"><label for="author">Name (optional):</label> <input id="author" name="author" type="text" placeholder="Name" size="30" maxlength="245" /></p>
  <p class="comment-form-email"><label for="email">E-mail (optional):</label><input id="email" name="email" type="text" placeholder="E-mail address" size="30" maxlength="100" /></p>
  <p class="comment-form-url"><label for="url">Website (optional):</label><input id="url" name="url" type="text" placeholder="Website (URL)" size="30" maxlength="200" /></p>
  <p class="comment-form-comment"><textarea name="comment" id="comment" cols="45" rows="8" required="required" placeholder="Your comment (Markdown is enabled)"></textarea></p>
  <p class="form-submit"><input type="submit" value="Post" /></p>
</form>

```

**`themes/quux/layouts/partials/comment-list.html`:**

```html
{{ $scratch := newScratch }}
{{ $scratch.Set "comments" (.Resources.Match "comments/*.yml") }}
{{ if eq 0 (len ($scratch.Get "comments")) }}
  <h2>No comments left here yet.</h2>
{{ else }}
  {{ $.Scratch.Set "commentcounter" 0 }}
  <ol class="commentlist">
    {{ range ($scratch.Get "comments") }}
    {{ $.Scratch.Set "commentcounter" (add ($.Scratch.Get "commentcounter") 1) }}
    <li class="comment">
      {{ $comment := (.Content | transform.Unmarshal) }}
      <article id="comment-{{$.Scratch.Get "commentcounter"}}" class="comment">
          <footer>
              <div class="comment-author">
                  <cite class="fn">{{ if $comment.link }}
                    <a href="{{ $comment.link }}">{{ $comment.author }}</a>
                  {{ else }}
                    {{ $comment.author }}
                  {{ end }}
                  </cite>
              </div>
              <div class="comment-meta">
                  {{ $comment.timestamp | dateFormat ":date_long" -}}
                  &nbsp;at
                  {{ $comment.timestamp | dateFormat ":time_short" -}}
              </div>
          </footer>

          <div class="comment-content">{{ $comment.comment | markdownify }}</div>
      </article>
    </li>
    {{ end }}
  </ol>
{{ end }}

```

**`themes/quux/_default/single.html`:**

```html
<article>
...
</article>

<div id="comments" class="comments-area">
  <h1>Comments:</h1>

  {{- partial "comment-list.html" . -}}
  <br />
  <h3>Add a new comment</h3>
  {{- partial "comment-form.html" . -}}
</div>

```

Works for me.

A _reply\_to_ field for those who would like to have nested comments is left as an exercise to the reader.

---

<div class="post-metadata">

**Author:** ![dertuxmalwieder](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/dertuxmalwieder/32/496_2.png) [@dertuxmalwieder](https://discourse.gohugo.io/u/dertuxmalwieder)\
**Post date:** [October 27, 2024, 10:59pm UTC](https://discourse.gohugo.io/t/static-comments-script-for-hugo-in-php/51924/2 "2024-10-27T22:59:43Z")

</div>

> [@dertuxmalwieder](#):
>
> (I recommend some kind of moderation here, but you’re free to establish your own pipeline.)

I wrote a small(ish) moderation script for the shell. Requires `ksh` (probably works with `bash` too) and `yq`. You’ll need to store moderated comments as `.yml2` (just change the `$filepath` line in the PHP script!).

```bash
#!/usr/bin/env ksh
done_something=0 # 1, if Hugo needs to be run

echo Comments moderation script for Hugo.
read content_path?"Path to /content: "

# Remove the trailing /, if any:
if [["$content_path" == */]]; then
    content_path=${content_path%?}
fi

if [-d $content_path]; then
    # Path exists.
    # Search for .yml2 files:
    file_list=`find $content_path -name "*.yml2"`
    if [-z $file_list]; then
        echo No comments are currently awaiting moderation.
        exit
    fi

    # Comments were found.
    comment_counter=1
    for comment_file in $file_list; do
        echo ""
        echo ---------------------
        echo Comment no. $comment_counter:
        echo ""

        # Read:
        echo " Post: `yq -e '.post_id' $comment_file`"
        echo " Author: `yq -e '.author' $comment_file` (`yq -e '.email' $comment_file`)"
        echo " Text: `yq -e '.comment' $comment_file`"
        echo ""

        # What to do?
        read action?"[P]ublish, [D]elete, [E]dit first? "
        case $action in
            P|p)
                # Rename .yml2 to .yml:
                newName=${comment_file%?}
                mv $comment_file $newName
                done_something=1
                echo The comment will be published.
                ;;
            D|d)
                # Delete $comment_file:
                rm $comment_file
                echo Comment deleted.
                ;;
            E|e)
                # Edit $comment_file, then publish it:
                vi $comment_file # you could also use ed here, or emacs...
                newName=${comment_file%?}
                mv $comment_file $newName
                done_something=1
                echo The comment has been edited and will be published.
                ;;
            *)
                echo This comment will be skipped.
                ;;
        esac
        
        ((comment_counter++))
    done

    # Run Hugo if a new comment has been published:
    if [[$done_something -eq 1]]; then
        cd $content_path/..
        hugo
    fi
else
    echo Wrong path.
fi

```

---

<div class="post-metadata">

**Author:** ![Peng\_Hu](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/peng_hu/32/23329_2.png) [@Peng\_Hu](https://discourse.gohugo.io/u/Peng_Hu)\
**Post date:** [November 13, 2024, 5:38am UTC](https://discourse.gohugo.io/t/static-comments-script-for-hugo-in-php/51924/3 "2024-11-13T05:38:12Z")

</div>

No offense, I’m just curious that why it called static?

Static websites mean there is no real-time interactions between server and clients, likes github pages base on my understanding. There will be some misunderstanding after a brief glance

Still Thanks for your product.

---

<div class="post-metadata">

**Author:** ![dertuxmalwieder](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/dertuxmalwieder/32/496_2.png) [@dertuxmalwieder](https://discourse.gohugo.io/u/dertuxmalwieder)\
**Post date:** [November 13, 2024, 9:49am UTC](https://discourse.gohugo.io/t/static-comments-script-for-hugo-in-php/51924/4 "2024-11-13T09:49:38Z")

</div>

> [@Peng\_Hu](#):
>
> I’m just curious that why it called static?

Unlike dynamic commenting systems like Disqus, the comments are baked right into the website, not loaded “on demand”. Depending on your deployment pipeline, you can even keep both `comments.php` and the commenting script on an entirely different server, so your Hugo server remains _static_.

The scripts itself aren’t static, the comments will be.

> [@Peng\_Hu](#):
>
> There will be some misunderstanding after a brief glance

I’m open for ideas.

---

<div class="post-metadata">

**Author:** ![Peng\_Hu](https://yyz2.discourse-cdn.com/flex036/user_avatar/discourse.gohugo.io/peng_hu/32/23329_2.png) [@Peng\_Hu](https://discourse.gohugo.io/u/Peng_Hu)\
**Post date:** [November 13, 2024, 9:58am UTC](https://discourse.gohugo.io/t/static-comments-script-for-hugo-in-php/51924/5 "2024-11-13T09:58:46Z")

</div>

Thanks for you friendly reply.  
I think I got it right now, sorry for my reckless views.
