GitHub Comments for your GitHub Pages

Manual

Contents

Theory of Operation

ghpages-ghcomments uses GitHub issues to store comments by associating a specific page with a specific GitHub issue using the page’s title front matter.

For example, using this title in a post

---
layout: post
title: The Phrenic Shrine Reveals Itself
---

causes ghpages-ghcomments to look for an issue titled The Phrenic Shrine Reveals Itself in the GitHub repository specified by _data/gpgc.yml.

Jekyll

There are four files that implement ghpages-ghcomments:

  • Structure:
    • includes/gpgc_comments.html
  • Behavior:
    • public/js/gpgc_core.js
    • public/html/gpgc_redirect.html
  • Data:
    • _data/gpgc.yml

Including gpgc_comments.html requires one Jekyll tag parameter: post_title.

For example, adding this line to _layouts/post.html

{% include gpgc_comments.html post_title=page.title %}

yields this input to _includes/gpgc_comments.html

  var gpgc = {
    new_comments_disabled: new Boolean({{ site.data.gpgc.disabled }}).valueOf() || new Boolean({{ page.gpgc_disabled }}).valueOf(),
    site_url: "{{ site.url }}",
    page_path: "{{ page.path }}",
    issue_title: "{{ include.post_title }}",
    repo_id: "{{ site.data.gpgc.repo_owner }}/{{ site.data.gpgc.repo_name }}",
    use_show_action: {{ site.data.gpgc.use_show_action }},
    github_application_client_id: "{{ site.data.gpgc.github_application.client_id }}",
    github_application_code_authenticator_url: "{{ site.data.gpgc.github_application.code_authenticator }}",
    github_application_login_redirect_url: "{{ site.data.gpgc.github_application.callback_url }}",
    enable_diagnostics: {{ site.data.gpgc.enable_diagnostics }},
  };

and is rendered to HTML by Jekyll like this:

  var gpgc = {
    new_comments_disabled: new Boolean().valueOf() || new Boolean().valueOf(),
    site_url: "http://downtothewire.io",
    page_path: "_posts/2015-01-18-the-phrenic-shrine-reveals-itself.md",
    issue_title: "The Phrenic Shrine Reveals Itself",
    repo_id: "wireddown/ghpages-ghcomments",
    use_show_action: true,
    github_application_client_id: "0ef5ca17b24db4e46807",
    github_application_code_authenticator_url: "https://ghpages-ghcomments.herokuapp.com/authenticate/",
    github_application_login_redirect_url: "http://downtothewire.io/ghpages-ghcomments/public/html/gpgc_redirect/index.html",
    enable_diagnostics: false,
  };

JavaScript

ghpages-ghcomments has two fundamental capabilities:

  1. Finding and showing comments, accessible to every site visitor
  2. Drafting and posting a new comment, accessible to visitors that login to GitHub

Finding and showing comments

When a visitor views a page, ghpages-ghcomments uses the GitHub web API to find and show the comments for the page, which happens in three steps:

  1. Use the page’s title to find the associated issue in the owner’s repository.
  2. Retrieve the comments from the issue as HTML.
  3. Place the comments in the page’s DOM.

The process begins at the single entry point to public/js/gpgc_core.js:

gpgc_main();

After initializing the page, gpgc_main() calls findAndCollectComments(), which uses the global variable gpgc to build a GitHub query to search a repository’s issues. Continuing the example above, the request looks like:

https://api.github.com/search/issues?
  q=The%20Phrenic%20Shrine%20Reveals%20Itself
  +repo:wireddown/ghpages-ghcomments
  +type:issue
  +in:title

When the request returns, onSearchComplete() parses the JSON sent by GitHub and

Continuing the example above, the request looks like:

https://api.github.com/repos/wireddown/ghpages-ghcomments/issues/4/comments

When the request returns, onCommentsUpdated() parses the JSON sent by GitHub and

  • appends the returned comments in the global variable CommentsArray
  • sends a request to retrieve the next page of the issue’s comments

If the issue has more than 30 comments, GitHub will paginate its responses. The response’s HTTP Link header indicates where the next page is:

Link:

<https://api.github.com/repositories/29450581/issues/4/comments?page=2>; rel="next",
<https://api.github.com/repositories/29450581/issues/4/comments?page=2>; rel="last"

Continuing the example above, the second page of comments is retrieved by sending a request that looks like:

https://api.github.com/repositories/29450581/issues/4/comments

Once the last page has been collected, updateCommentsAndActions() adjusts the contents of the HTML structural elements (discussed below) to show the reader an action:

  • Show *N* Comments, which uses showAllComments() and CommentsArray to format and insert the comments in the document

All of the web requests for searches and comments are sent using XMLHttpRequest with a custom request header:

Accept:	application/vnd.github.v3.html+json

This asks GitHub to return everything in JSON but to render the markdown content to HTML.

In addition, GitHub has permissive CORS, which allows XMLHttpRequest to make GitHub API calls in modern browsers without any additional configuration.

Drafting and posting a new comment

When a visitor wants to leave a comment, they must login to GitHub since the comments are stored in an issue for a GitHub repository.

ghpages-ghcomments uses the Web Application Flow to authenticate a visitor with GitHub, which happens in three steps:

  1. Open a new browser window to redirect the visitor to login to GitHub.
  2. Upon login, close the newly opened window and exchange GitHub’s temporary code with the visitor’s OAuth token.
  3. Persist the visitor’s token in the browser’s local storage for the site so the visitor doesn’t need to login every time.

The process starts when the visitor clicks or taps the Login button, which calls loginToGitHub() to build a secure GitHub authentication request with URL-encoded data:

  var data = {
    "client_id": gpgc.github_application_client_id,
    "scope": "public_repo",
    "state": StateChallenge,
    "redirect_uri": gpgc.github_application_login_redirect_url
  };

This request is opened in a new browser window so that it can handle GitHub’s redirect and send the original window a message event. When the visitor logs in using the new window, GitHub redirects back to the redirect_uri with a temporary code and the state provided in the request. The redirect_uri is a simple JavaScript-only Jekyll page (public/html/gpgc_redirect.html) that uses the data from GitHub’s response to send a message event to the original window before closing itself.

The original window handles the event in onMessage(), and inspects the event data to confirm the security of the transaction. Once the message has been verified, ghpages-ghcomments uses a simple GitHub application to exchange the temporary code with a revocable OAuth token. For more details about this step, see Custom GitHub App.

The last login step is saving the OAuth token in the site’s local storage. The next time the visitor returns, ghpages-ghcomments automatically authenticates them with GitHub with the User API.

Once logged in, the visitor can post their draft as a new comment. ghpages-ghcomments uses the GitHub Markdown API to render a markdown draft in HTML when the visitor clicks or taps the Preview button, and uses the GitHub Issue API to add comment to the page’s associated issue when the visitor clicks or taps the Submit button.

git

Since both the blog post and its repository issue must have the same title for the JavaScript to retrieve the comments, ghpages-ghcomments uses git hooks to automate the creation of issues when new posts are pushed to the repository.

All of the automation is installed and executed from a single bash script: **tools/gpgcCreateCommentIssue.sh**. Placing the script in the system’s _$PATH allows it to be executed by the blog author and by the git hooks.

When the script installs the hooks, it adds thin wrappers back to gpgcCreateCommentIssue.sh:

pre-commit

gpgcCreateCommentIssue="$(which gpgcCreateCommentIssue.sh)"

if test -x "${gpgcCreateCommentIssue}"; then
  "${gpgcCreateCommentIssue}" commit
fi

The pre-commit hook

  • identifies the changed files in the _posts directory that are about to be committed
  • stores their relative path in a file: .git/gpgc_cache

Continuing the example above, after committing _posts/2015-01-18-the-phrenic-shrine-reveals-itself.md, the gpgc_cache file contains:

_posts/2015-01-18-the-phrenic-shrine-reveals-itself.md

pre-push

gpgcCreateCommentIssue="$(which gpgcCreateCommentIssue.sh)"

if test -x "${gpgcCreateCommentIssue}"; then
  "${gpgcCreateCommentIssue}" push 1234567890abcdef...78
fi

The pre-push hook does the majority of work. It uses _config.yml, _data/gpgc.yml, and .git/gpgc_cache to

  • set the repository owner and name
  • set the label name and color
  • create the label if it doesn’t already exist
  • create an issue for each post in .git/gpgc_cache if it doesn’t already exist
    • the name of the issue is taken from the title front matter of the post
    • the issue’s content is a link to its post
  • reset .git/gpgc_cache if an issue was created

The pre-push hook queries for the existence of labels and issues using curl and the GitHub API.

Continuing the example above, the curl request to query for the label’s existence looks like:

curl -s "https://api.github.com/repos/wireddown/ghpages-ghcomments/labels"

And querying for the issue’s existence looks like:

curl -s "https://api.github.com/search/issues?
  q=The%20Phrenic%20Shrine%20Reveals%20Itself
  +repo:wireddown/ghpages-ghcomments
  +type:issue
  +in:title"

When the pre-push hook creates a label or an issue, it needs to authenticate with GitHub as the repository owner. GitHub recommends creating an OAuth access token for command line authentication, which is the 40-character string used in the hook.

curl passes the authentication to GitHub as an extra header using the -H flag:

-H "Authorization: token 1234567890abcdef...78"

curl passes the contents of the POST request as a string using the -d flag:

-d "{
  \"title\":\"The Phrenic Shrine Reveals Itself\",
  \"body\":\"This is the comment thread for [The Phrenic Shrine Reveals Itself](http://downtothewire.io/ghpages-ghcomments/2015/01/18/the-phrenic-shrine-reveals-itself).\",
  \"assignee\":\"wireddown\",
  \"labels\":[\"Example GitHub Pages Comments\"]
  }"

HTML Structure

The source is the authority on the HTML structure (includes/gpgc_comments.html) but here is an image illustrating the layout. It will be helpful to keep one of these open while reading this section.

On load

When a ghcomments-ghpages page loads, several <div> elements begin hidden:

  • gpgc_all_comments, which holds all of the comments
  • gpgc_no_comments, which holds a message saying there are no comments
  • gpgc_actions, which allows the visitor to show comments
  • gpgc_reader_error, which shows any error or diagnostic messages

At the same time, ghpages-ghcomments searches for the page’s associated issue and its comments. Depending on whether there are comments, the layout updates. Regardless, the new comment form is initialized to allow the visitor to draft a comment.

When there are no comments

Once ghpages-ghcomments has queried GitHub for the post’s comments and determines there are none, it shows the gpgc_no_comments <div>.

When there are comments

Once ghpages-ghcomments has retrieved all of the comments for a post, it

  • formats all the comments via formatAllComments() and places them in the still-hidden gpgc_all_comments <div>
  • updates the show_comments_button <button> with a “Show N comments” message

If data/gpgc.yml specifies true for use_show_action, then ghpages-ghcomments shows the show_comments_button <button>; otherwise, it shows the gpgc_all_comments <div>.

When comments are presented

When the user clicks or taps the show_comments_button <button>, ghpages-ghcomments hides the button and shows the gpgc_all_comments <div>.

Comment structure

Each comment is placed in its own <div> with the following structure:

HTML structure for a comment

Look and Feel with CSS

Theme

The color theme is specified at the top of public/css/gpgc_styles.css.

Class list

  • Comments
    • .gpgc-comment
      <div> container that holds a single comment’s header and contents
    • .gpgc-comment-header
      <div> container that holds a single comment’s header
    • .gpgc-avatar
      <img> element that presents a user’s avatar
    • .gpgc-comment-contents
      <div> container that holds the GitHub comment contents
  • New comment
    • .gpgc-new-comment
      <div> container that holds the comment draft elements
    • .gpgc-new-comment-form
      <div> container that holds the comment form
    • gpgc-new-comment-form-textarea
      <textarea> element for entering a new comment
    • .gpgc-tabs
      <div> container that holds the Write and Preview buttons
    • .gpgc-tab
      <button> element that behaves like tab
    • .gpgc-new-comment-actions
      <div> container that holds the Login and Submit buttons
    • .gpgc-comment-help
      <div> container that holds just-in-time help messages
  • ‘Show N Comments’ button
    • .gpgc-actions
      <div> container that holds the ‘Show N Comments’ <span>
    • .gpgc-action
      <span> container that holds the ‘Show N Comments’ <button>
  • Mixins
    • .gpgc-hidden
      Hides an element
    • .gpgc-centered-text
      Centers the text in the element
    • .gpgc-comments-font
      Sets the font traits for the element
    • .gpgc-new-section
      Sets the top margin for the element
    • .gpgc-normal-primary-button
      <button> element that has an emphasized look and feel
    • .gpgc-large-secondary-button
      <button> element that has a diminished look and feel
    • .gpgc-text-button
      <button> element that has a text look and feel
    • .gpgc-help-message
      <div> container for showing a help message
    • .gpgc-help-error
      <div> container for showing an error message
  • Misc
    • .gpgc_last_div
      <div> container for the last div, adds a bottom margin

Other advanced topics: