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.
There are four files that implement ghpages-ghcomments:
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,
};
ghpages-ghcomments has two fundamental capabilities:
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:
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
IssueUrl
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
CommentsArray
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:
showAllComments()
and CommentsArray
to format and insert the comments in the documentAll 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.
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:
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.
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
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
title
front matter of the postThe 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\"]
}"
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.
When a ghcomments-ghpages page loads, several <div>
elements begin hidden:
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.
Once ghpages-ghcomments has queried GitHub for the post’s comments and determines there are none, it shows the gpgc_no_comments <div>
.
Once ghpages-ghcomments has retrieved all of the comments for a post, it
formatAllComments()
and places them in the still-hidden gpgc_all_comments <div>
<button>
with a “Show N comments” messageIf 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 the user clicks or taps the show_comments_button <button>
, ghpages-ghcomments hides the button and shows the gpgc_all_comments <div>
.
Each comment is placed in its own <div>
with the following structure:
The color theme is specified at the top of public/css/gpgc_styles.css.
<div>
container that holds a single comment’s header and contents<div>
container that holds a single comment’s header<img>
element that presents a user’s avatar<div>
container that holds the GitHub comment contents<div>
container that holds the comment draft elements<div>
container that holds the comment form<textarea>
element for entering a new comment<div>
container that holds the Write and Preview buttons<button>
element that behaves like tab<div>
container that holds the Login and Submit buttons<div>
container that holds just-in-time help messages<div>
container that holds the ‘Show N Comments’ <span>
<span>
container that holds the ‘Show N Comments’ <button>
<button>
element that has an emphasized look and feel<button>
element that has a diminished look and feel<button>
element that has a text look and feel<div>
container for showing a help message<div>
container for showing an error message<div>
container for the last div, adds a bottom margin