Pushing the boundaries of the markdown file using Go
What would be your initial response when someone asked “is it possible to play a tic-tac-toe game from Github README.md file” ?
If your answer is “yes”, then you are in for a delight.
Let’s dive in and see how it’s possible to make a Github README.md file as a frontend for a tic-tac-toe game.
How It All Started
You may ask why I even went on this journey, and the following are a few of the reasons behind it.
I wanted to push the boundaries of a markdown file and make it as interactive as possible.
to uniquely identify and track the traffic coming to my individual GitHub project page.
Why did I even get these wild ideas in the first place? Well, it’s because whenever I add an image or image link to the GitHub readme markdown file, it’s always getting replaced with a proxy URL when I try to open the image(proxy URL looks like this: https://camo.githubusercontent.com/some-hash
).
I wondered why it was happening like that. I mean, it’s okay to proxy an external URL, but why are even static images placed in repositories getting a proxy URL when they’re referenced from a readme file? What is GitHub trying to achieve here?
Then I thought about what would happen if there was no proxy server involved while serving the images. If there is no proxy, then the HTTP request to fetch the image will directly come to our server(where we host the image) and we can read the HTTP request and try to get a user IP address, which can be used for any tracking purposes and GitHub will have no control over it.
So I kind of understood the reason behind why Github added the image proxy server. At this stage, I wanted to know whether it’s possible to get any unique identification from the HTTP request made from the user machine as part of rendering the markdown file.
I hoped there would be a way to uniquely identify each session/machine. So I thought of building a simple game that can be played from within the markdown file itself to test that. The idea was to build a multiplayer game where each player would have their own state saved against a unique id. “Tic-tac-toe” game seems to be a good fit for this, since it’s a well-known game, and it’s fairly straightforward to implement.
After I started the development, I quickly found out there was no way we could get a unique id from the http request fired from the readme markdown file. This is because each time a request comes from a random proxy server, and on top of that, cookies are not allowed either.
So, without a unique id, there won’t be any states saved for each player, and individual gameplay is impossible. Therefore, only a single gameplay will be shared with the entire internet. (In other words, anyone in the world can see and play the game, but only one gameplay is visible to everyone; changes made by one are reflected to all.)
At this stage, I thought of abandoning the quest, but playing a game from the Github README.md file itself seems like a cool idea, even when the gameplay is shared with the entire internet. So I ran this idea by my friends, and they seemed interested in seeing how this would turn out, so I thought of investing some time in developing this.
Quick Recap
GitHub uses a proxy server to serve media content, and each time a random proxy server will be used to serve media (when the cache is disabled).
The proxy server doesn’t allow us to store cookies.
And we would like to make markdown interactive with a dynamic UI.
The Life Cycle of an HTTP Request From a GitHub Markdown File
Let’s consider the below Github project structure — where we have localImageFile.png
and README.md
file.
The README.md
file shows two images, one from the repository itself and the other from a remote server.
The below sequence diagram is illustrated for loading localImageFile.png from the same repo:
The below sequence diagram is illustrated for loading remoteImage.png from the remote server:
Let’s Build the Tic-Tac-Toe
Building APIs for the game — We are going to build a backend server that exposes APIs to
start
the game,restart
the game,read the current state
of the game,make a move
from the user, andread the history of the game state
.How do we call these APIs from a Github Readme Markdown file — Well, we are going to create CTAs (call to action) for each of the APIs and call the APIs whenever the CTAs are clicked.
How do we create CTAs on Markdown files — For CTAs, we will use images, and when the image is clicked, an HTTP request will be fired from the markdown file.
How do we show the tic-tac-toe board on UI — You might have already guessed it. We will use images to reflect the current state of the game.
In short, the backend will return a set of images we can use as CTA, and when clicking the images, we fire the corresponding API which will manipulate the game state.
As I mentioned earlier, since we can’t identify the request made from the same system, only one gameplay will be shared with the entire internet. Hence, the backend maintains the game state of the 3x3 tic-tac-toe in a single variable. The APIs that are exposed will basically try to modify this single game state and reflect the same on the UI (i.e. the README file) using images sent from the server.
Image assets stored on the backend server
Please keep the svg file names in mind as we will go over the uses for each image file shortly.
Image assets stored on the backend server
The backend server is written in Go:
func main() {
port := getPort()
log.Println("Starting tic-tac-toe service at port ", port)
http.Handle("/", http.HandlerFunc(onHome))
http.Handle("/renderCell", http.HandlerFunc(onRenderCell))
http.Handle("/clickCell", http.HandlerFunc(onClickCell))
http.Handle("/renderPlayControls", http.HandlerFunc(onRenderPlayControl))
http.Handle("/clickPlayControls", http.HandlerFunc(onPlayControlClick))
http.Handle("/renderActivities", http.HandlerFunc(renderActivities))
http.ListenAndServe(":"+port, nil)
}
Let’s look at each API and see what it does!
Render cell
The
renderCell
API takes cell index as a URL parameter and returns the cell image. For a 3x3 tic-tac-toe game, we have nine cells to render. The backend returns the corresponding cell image based on the cell’s state.rect_x.svg
is returned when the user has pressed that cell.rect_o.svg
is returned when the computer has pressed that cell.rect_empty.svg
is returned when no one has pressed that cell.rect_x_selected.svg
is returned to highlight the area won by the user.rect_o_selected.svg
is returned to highlight the area won by the computer.
Click cell
The
clickCell
API takes the cell index as a URL parameter and marks the cell as selected by the user (provided the cell is empty).The
clickCell
API is attached to the anchor link of each cell that is rendered by the renderCell API.
Please see the screenshot below to see the markdown file content and its preview:
Markdown file content with a preview
When each cell is clicked, the server modifies the game state and reloads the page so that the new modified state can be read from the server.
Page reloading is a mandatory step. Since this is a markdown file, this is the only way to get the modified state from the server.
Reload the page
This is one of the crucial steps. Remember how GitHub uses an image proxy server to serve media files? Well, that image proxy caches the response for a certain amount of time. So we need to add the cache control response header as ‘no-cache,no-store,must-revalidate’ to disable this cache mechanism.
If we don’t disable the cache mechanism, then the proxy server will simply return the cached response and will not call our backend till the cache expires. Therefore, we won’t be able to reflect the current game state back to the user, and we will end up showing stale data (not a good user experience).
const REDIRECT_URL = "https://github.com/sridhar-sp/tic-tac-toe"
responseWriter.Header().Set("Content-Type", "image/svg+xml")
responseWriter.Header().Set("Cache-Control", "no-cache,
no-store,must-revalidate")
responseWriter.Header().Set("expires", "0")
responseWriter.Header().Set("pragma", "no-cache")
http.Redirect(
responseWriter, req,
REDIRECT_URL, http.StatusMovedPermanently
)
Render play controls
- The
renderPlayControls
API returns computer_start_button.svg when the game is yet to begin and restart_button.svg when the game is finished.
Click play controls
This API is attached as an anchor to renderPlayControls
When the play control button is pressed, the corresponding action takes place on the backend, such as restarting the game or letting the computer make the first move.
Please see the screenshot below to see the play control markdown file content and its preview:
Render game activities
- The
renderActivities
API returns all game activities in svg image format.
Please see the screenshot below to see the game activities markdown file content and its preview:
Demo
Player ‘X’ is a human, and Player ‘O’ is a computer.
Please see the screenshot below to see the markdown file content and its preview:
Click here to see Markdown file with its interactive preview
Source Code
Here are the GitHub repositories for the final frontend application and the backend written in Go:
Reference
https://github.blog/2010-11-13-sidejack-prevention-phase-3-ssl-proxied-assets/
https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls
Top comments (0)