Staticman With GitHub and Zeit Now - part 3

A quick recap

In part 2 of this series we created a Staticman GitHub bot account and walked through all of the steps required to deploy an instance of Staticman to Zeit Now. In this final post of the series we are going to invite our Staticman instance to be a collaborator of our unpublished GitHub blog repository. Then we will prepare our blog to send RESTful requests to our Staticman instance. So let’s get started!

Invite Staticman to be your collaborator

I have two repositories associated with my blog.

bloggerbust.ca
This is where I write my content prior to it being published. We may refer to this as the unpublished repository
bloggerbust.github.io
Hugo reads content from the unpublished site and writes the result to the this repository. We may simply refer to this as the published repository. The unpublished repository actually has the published repository as a Git sub-module

Staticman writes comments in data files that need to be processed by a static site generator. Therefore Staticman needs to somehow get those data files committed to my unpublished repository for processing. Rather than forking my unpublished repository, Staticman keeps things simple by creating a branch directly in my repository. This is accomplished by registering its GitHub account as a collaborator with my GitHub account. As a collaborator, it must be given write access to my unpublished repository.

Sign into your blogging GitHub account. Navigate to the settings profile and click on Repositories under the Personal settings menu. In the right hand pane you will see a list of repositories, each repository has a collaborators link. Click the collaborators link next to your unpublished repository. The list of repositories will be replaced with a form to add a collaborator. You may navigate to this form directly by filling out the following URI pattern in your browser’s address bar then hit enter:

https://github.com/{YOUR-GITHUB-USERNAME}/{YOUR-UNPUBLISHED-BLOG-REPOSITORY}/settings/collaboration

You should now be looking at the Collaborators form. In the search field type the name of your bot account and click the Add collaborator button. Your bot account should now be listed above the Add collaborator search field with a status label similar to:

Awaiting BloggerBust-bot’s response

After registering the Staticman GitHub bot account an invitation will be issued that Staticman must accept. However, Staticman is not aware that the invitation has been created; therefore, we must ask Staticman to accept the invitation by sending the invitation details to our Staticman instance using a RESTful request. It is true that we could accept the invitation on behalf of Staticman since we have direct access to the bot account, but we want to make sure that things are working correctly, so no cheating!

The request requires three pieces of information.

YOUR-DEPLOYMENT-ALIAS
The alias was printed to the terminal when we deploy our Staticman instance. If you forgot the name of your instance then go to your Zeit Now dashboard and click the deployment details for staticman
YOUR-GITHUB-USERNAME
The username you use to push content to the unpublished repository
YOUR-REPOSITORY-NAME
The name of your unpublished repository
STATICMAN-API-VERSION
The number representing the Staticman API version. We are using API version 2.

Now all you need to do is enter following URI in your browser’s address bar filled in with the correct information and then hit enter:

https://{YOUR-DEPLOYMENT-ALIAS}/{API-VERSION}/connect/{YOUR-GitHub-USERNAME}/{YOUR-REPOSITORY-NAME}

If everything goes right then you will receive the message OK!. If on the other-hand, something goes wrong, you can access your logs like this:

https://{YOUR-DEPLOYMENT-ALIAS}/_logs

and your source files like this:

https://{YOUR-DEPLOYMENT-ALIAS}/_src

Prepare your Blog for Staticman

The last thing you need to do before you can bask in the glory of victoriously adding static comments to your GitHub pages is five things :-P

  1. Add a configuration file named staticman.yml to the root directory of your blog’s repository
  2. Add site parameters for Staticman to your site’s configuration file
  3. Add a partial view to your layout that will allow your readers to create a new comment
  4. Add a partial view to your layout that will allow your readers to view existing comments
  5. Update your post footer partial to include the create / view comment partials

Exactly how you should go about doing this depends on a number of factors, such as:

which static site generator you are using
Yes, I know, there are lots.
which static site generator theme you are using
I am using a simple theme named Hugo Dusk.
which Git forge you are using
Staticman currently only supports GitHub and GitLab so if you are using a different forge then you will need to be prepared to implement support yourself. If you do, be sure to make a pull request so the rest of the community can benefit.
Whether or not your unpublished content is in a separate repository from your published content
My preference is to maintain two separated repositories

Based on these considerations I hope it is apparent that the following steps will require further customization on your part if you choose to integrate them into your own site.

Add Staticman.yml

You should read through the official documentation discussing the staticman.yml configuration file as I am not going to go into detail here. There is one thing that is important to understand before you move on to adding a create comment partial. The configuration file can have a root property, but it is not necessary. Take a look at the staticman.yml for Blogger Bust and notice that the root property is named comments.

# Name of the property. You can have multiple properties with completely
# different config blocks for different sections of your site.
# For example, you can have one property to handle comment submission and
# another one to handle posts.
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", "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 Staticman reader comment"

  # (*) REQUIRED
  #
  # Destination path (filename) for the data files. Accepts placeholders.
  filename: "{@id}"

  # 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: "staticman -> bloggerbust.ca"

  # (*) 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

You can name the root property anything you like, or remove it entirely, as long as the result is valid YAML. The root serves as a lookup key to a named configuration. If it is absent, then that means you can have only a single configuration. Otherwise, you can have one or many named configurations. If you read the comments in the header that were written by the original author it essentially explains this very concept. You could have a configuration for your comments, and a completely different configuration named posts for a forum. Honestly, you could get really creative with this.

The other thing I want to point out is that the configuration let’s you decide which fields are allowed to be posted to Staticman. This is accomplished through the allowedFields property. Thank you Eduardo for designing Staticman to be generic. Now take a moment to read up on things before moving on. Feel free to download my staticman.yml as a starting place if you wish. Remember that white space matters in YAML, so no mixing tabs and spaces inconsistently. You might want to run your configuration through a YAML linter if you hare having difficulty.

Add Staticman site parameters to config.toml

When our site posts content to Staticman for processing there are a number of parameters that must be passed. It is a good idea to keep the values for these parameters in your sites configuration file. The exact notation will depend on the static site generator that you are using. Here is what I added to the Hugo TOML configuration for Blogger Bust. If you are using Jekyll, or another static site generator, then you will have to look up for your self how to configure it correctly.

[params.staticman]
  domain = "staticman.bloggerbust-bot.now.sh"
  api_version = "2"
  username = "BloggerBust"
  repository = "bloggerbust.ca"
  branch = "master"

This is what each parameter is for:

domain
the domain name of our Staticman Zeit Now instance.
api_version
valid versions are 1 and 2. Version 3 is partially implemented, but not working at the moment.
username
The username of the blog’s account
repository
The name of the repository that contains the Staticman.yml configuration
branch
the target branch for your GitHub Staticman bot account’s pull requests

Add Create comments partial

Please refer to hugo-dusk PR #31 create-comment partial.

<form class="comment" method="POST" action="https://{{ .Site.Params.staticman.domain }}/v{{ .Site.Params.staticman.api_version }}/entry/{{ .Site.Params.staticman.username }}/{{ .Site.Params.staticman.repository }}/{{ .Site.Params.staticman.branch }}/comments" class="flex-container flex-column">
  <input type="hidden" name="options[redirect]" value="{{ .Permalink }}#comment-submitted">
  <input type="hidden" name="options[slug]" value="{{ .Page.File.ContentBaseName }}">
  <div class="flex-container flex-row">
    <input name="fields[name]" type="text" placeholder="Your name" class="flex-item">
    <input name="fields[email]" type="email" placeholder="Your email address" class="flex-item">
  </div>
  <div class="flex-container flex-row">
    <textarea name="fields[message]" placeholder="Your message. Feel free to use Markdown." rows="10" class="flex-item"></textarea>
  </div>
  <div class="flex-container flex-row">
    <input type="submit" value="Submit" class="flex-item">
  </div>
</form>
<div id="comment-submitted">
  Your comment has been submitted and is now pending moderation
</div>

Take a close look at the form’s action. Notice, that at the end of URI, there is a URI path component segment named /comments. That segment must match the name of the Staticman.yml configuration key intended for this view. If you choose to not name your configuration, then you must remove the segment /comments from the end of the URI. Similarly, if you choose to name your configuration differently, then you must name the segment the same. I think the rest is self explanatory. Again, I have only tested this template with the Hugo static site generator. If you are using a different static site template engine, then you may need to access site parameters differently, but the required parameters and general structure of the form can remain the same.

Add View comments partial

Please refer to hugo-dusk PR #31 view-comment partial.

{{ $comments := readDir "data/comments" }}
{{ $.Scratch.Add "hasComments" 0 }}
{{ $entryId := .Page.File.ContentBaseName }}

{{ range $comments }}
  {{ if eq .Name $entryId }}
    {{ $.Scratch.Add "hasComments" 1 }}
    {{ range $index, $comments := sort (index $.Site.Data.comments $entryId ) "date" "asc"}}
<blockquote class="comment">
  <p>{{ .message | markdownify }}</p>
  <cite class="flex-container flex-row flex-end">
    <img class="flex-item avatar" src="https://www.gravatar.com/avatar/{{ .email }}?s=50&d=retro">
    <div class="flex-container flex-column flex-item flex-center">
      <strong>{{ .name }}</strong><br>{{ dateFormat "02/01/2006" .date }}
    </div>
  </cite>
</blockquote>
    {{ end }}
  {{ end }}
{{ end }}

{{ if eq ($.Scratch.Get "hasComments") 0 }}
<p>Be the first to comment on this article.</p>
{{ end }}

In the first line you can see that the comments are being read from the data/comments directory. The Hugo static site generator uses the data directory to store static template data in YAML files. Jekyll has an equivalent directory named _data. Exactly where the comment data is written to by Staticman is specified by the path property of the Staticman.yml. Make sure that the Staticman.yml comments path property and the relative path passed to readDir in comment-view are the same. Everything has to be consistent for the machine to work.

Update post footer with comments

Please refer to hugo-dusk PR #31 postfooter partial.

<footer class="post-footer">

  <div class="post-footer-data">
    {{ partial "tags.html" . }}
    <div class="date"> {{ .Date.Format "Jan 2, 2006" }} </div>
  </div>

</footer>

{{ if eq .Type "post" }}

  {{ if ne (default "undefined" .Site.Params.staticman) "undefined" }}
<hr>
<h2 id="Comments">
  Comments
</h2>
    {{ partial "staticman/create-comment" . }}
    {{ partial "staticman/view-comments" . }}
  {{ else }}
    {{ template "_internal/disqus.html" . }}
  {{ end }}
{{ end }}

In the postfooter partial of the hugo-dusk, theme I added logic to include the create and view comment partials. The logic works as follows: If the site parameter named staticman is undefined, then the default disqus internal template will be loaded; otherwise, a header named Comments will be rendered along with the create and view comment partials.

Again, if you are using a different static site generator, or a different theme, then exactly how you include the create and view comment partials will differ in some way. This is a problem that you will have to solve for yourself.

Conclusion

That is it! If you followed along and were able to add a form for creating comments, as well as a template for viewing existing comments, in a way that is compatible with your static site generator and chosen theme, then your blog should now support static comments. Congratulations, you may now bask in the glory of victoriously adding static comment support to your GitHub pages. I hope you appreciate and enjoy the freedom of keeping your data where it belongs.


Comments

Your comment has been submitted and is now pending moderation

Hi Anatoliy,

I have been meaning to update my Staticman fork for a while now. I am a bit busy at the moment, but I will try to fit it in over the next few weeks. In the meantime, if you want to try and trouble shoot this yourself, you need to access the server side logs to see what is going wrong. Here is the relevant documentation to get you started:

https://vercel.com/docs/platform/deployments#logs

I will comment here after I have updated my fork.

Cheers!

Trevor Wilson
28/11/2020

Hello! Now Zeit has turned into Vercel. Their instrument is now not “now”, but “vercel”. But overall, your instruction works!! I have forked your repository. As a result, the application started for me, I saw (https://{YOUR-DEPLOYMENT-ALIAS}/) Hello from Staticman version 3.0.0!

but following the link ```https://{YOUR-DEPLOYMENT-ALIAS}/v3/connect/{YOUR-GitHub-USERNAME}/{YOUR-REPOSITORY-NAME}``` i see

{“success”:false,“errorCode”:“INVALID_VERSION”} and by clicking on a link of this kindhttps://{YOUR-DEPLOYMENT-ALIAS}/v2/connect/{YOUR-GitHub-USERNAME}/{YOUR-REPOSITORY-NAME}i see errorInternal Server Error```

Anatoliy
26/11/2020

That is great news Ricardo! You are right that Staticman could use improved error logging. When I merge upstream and get things working I will add better error logging unless someone beats me to it.

cat staticman_key | xargs -0 sh -c ‘now secret add – staticman-rsa-private-key “$0”’

Thanks for the tip, that simplifies things a bit.

Thank you very much for your time and for pointing me in the right direction. BTW, that Zeit product looks pretty good! Thanks for sharing the info!

Your welcome. I am glad that you enjoyed this introduction to Zeit. I feel that they have done a good job simplifying the user experience of app deployment making it an easy platform to work with.

Cheers!

Trevor

Trevor Wilson
31/08/2019

Hi Trevor:

Yay!! I am finally basking in the glory of victoriously adding static comments to my blogs!

It took me a while to track it down (lots of console.logs, since the app logs where not very helpful), but in the end it was, of course, my mistake. I forgot to re-encrypt my reCaptcha secret with the new service, so the decrypt was failing (silently).

Also, I decided not to strip the \r and \n characters from the private key sent to “now secret” (in one of my trial/error runs). Instead, I am doing the following:

cat staticman_key | xargs -0 sh -c ‘now secret add – staticman-rsa-private-key “$0”’

And it works fine.

Thank you very much for your time and for pointing me in the right direction. BTW, that Zeit product looks pretty good! Thanks for sharing the info!

Best regards,

Ricardo

Ricardo
30/08/2019

Hi, thanks for taking the time to answer.

Actually I only see two deprecation messages for each try:

WARN { Deprecation: [@octokit/rest] new Octokit({headers}) is deprecated. Use {userAgent, previews} instead. WARN { Deprecation: [@octokit/rest] octokit.authenticate() is deprecated. Use “auth” constructor option instead.

Other than that, not even the 500 status is showing in the log.

Ricardo

Ricardo Morin
30/08/2019

Hi Ricardo,

Unfortunately, I have not been able to post comments since I am getting a 500 error without any details on the response or in the logs.

I am sorry to hear that. The first thing to do is confirm that you are looking in the correct places for the logs. Try signing in to zeit now from the terminal. Make sure you are using your bot account.

$ now whoami > bloggerbust-bot

If you are not using your bot account then logout and log back in using your bot account.

$ now logout ✔ Logged out!… $ now login > We sent an email to trevor.wilson+bot@bloggerbust.ca. Please follow the steps provided inside it and make sure the security code matches Sparkling Chameleon. ✔ Email confirmed > Ready! Authentication token and personal details saved in “~/.now”

After that, confirm your alias:

$ now ls > 4 total deployments found under bloggerbust-bot [303ms] > To list more deployments for an app run now ls [app] project url inst # type state age staticman staticman-lq210tvnu.now.sh - - READY 133d

Now that you know your alias, fetch the logs and store them locally for easier inspection.

$ now logs staticman-lq210tvnu.now.sh -a -d –since=2019-08-28 > staticman-lq210tvnu.debug.log $ less staticman-lq210tvnu.debug.log

Do you see any errors in the log?

Trevor Wilson
30/08/2019

I was able to follow your guide successfully until (and including) the “connect” part where the bot account accepts the collaborator invitation. Unfortunately, I have not been able to post comments since I am getting a 500 error without any details on the response or in the logs. I am puzzled, since I had v2 working with the staticman service before it went bad, so the client code should be ok (with the only change being the endpoint). Any hints as to where to begin looking? Thanks.

Ricardo
29/08/2019

Yay, it worked!

Trevor Wilson
21/04/2019