Introducing TwitHub!
How I built TwitHub, a hack that turns your GitHub README.md into a social media platform.
When I started working at TakeShape in July, GitHub had just launched the profile-page README
easter egg. I had already spent too much time tricking mine out and I was looking for an interesting way to gain familiarity with the product. TakeShape already had some excellent, retro-web-styled sample projects for blogs and shops and portfolios, but its potential to serve as a developer-focused microblogging platform, free of all the noise and bad vibes and lack of hacks you might encounter on a Twitter or a Facebook, was untapped. Thus the idea of TwitHub was born.
TL;DR:
- It's a Twitter clone published in a GitHub
README.md
accomplished with GitHub Actions and TakeShape. - Go to the repo: https://github.com/takeshape/twithub
- Click
Use this template
to create your own copy. - Wait 30 seconds...
- Follow the instructions in the `README.md`.
- Start Twitting!
What does it do?
From the start I wanted to replicate as much of Twitter's functionality as I could considering I had no CSS, no JS and no backend server available. In addition to the basics of a microblog—posts, or twits—I needed to find reasonable analogs for following, sharing and replying. I generally wanted the design to read as a sort of web brutalist Twitter.
The patterns
First I needed a data model and an easy way to add content. Duh, that's what TakeShape does. The recently introduced "pattern" feature allowed me to build up a schema, add some seed data, export it, and add it to repo. This was awesome. I could easily enforce my 280 character defaults, since instead of a set of instruction to recreate a schema, all I needed to do was drop in a button linked to the pattern in the repo itself.
Naively, I made a schema where every post was its own object and they were shown in reverse chronological order. As I spent more time with Twitter, I saw that supporting threads properly would involve a different sort at the thread level, so I landed on using a repeater element to replicate the basic post object. This allowed for more extended meditations in a format often criticized for being shallow.
Then I added in a profile, preserving the charm of a max 50 character name and 160 character bio. Creative constraints!
I also gave the profile a pinnedTwit
relationship field, to keep a specific post at the top of the feed. The GraphQL interface then conveniently provides this post in the context of the profile query, making it easy to render outside of its typical place in a TwitList
.
With the pattern all set, I made sure there was some very basic seed data and performed a project export. The resulting zip file had just 3 files: the jsonl
data, the full schema definition, and a simple pattern definition file.
The posts
I started by generating a simple list of templated markdown using the nunjucks templating that TakeShape supports. I quickly realized it was going to be a challenge to preserve line-spacing and formatting with the features surrounding the posts and moved on to use HTML tags which are supported in GitHub-flavored markdown. I found the "Poor Man's Styleguide," which led me to the previously unknown <kbd>
tag. I abused GitHub's styling of this tag to call out date stamps on posts and add a little extra flair.
Like Twitter, I wanted to support images. TakeShape provides an image manipulation service and CDN (backed by imgix) which allowed me to display images in a reasonable size and add in some subtle corner styling that mirrors the Twitter presentation. GitHub itself doesn't allow for any custom CSS.
So, for instance, this set of query params applies a corner radius mask, and limits the dimensions of the image, while using image analysis to crop to a typical area of interest, human faces.
?auto=compress%2Cformat&corner-radius=15%2C15%2C15%2C15&crop=faces%2Centropy&fit=crop&mask=corners&max-h=510&q=100&w=510"/>
(TakeShape's static-site generator provides a nunjucks helper that simplifies creating these query params.)
Video links from YouTube are also supported, with the urls validated by a simple regex rule added in the content type editing UI.
Sharing
Much like Twitter, I wanted people to be able to share their Twits. This necessitated adding in-page anchors with unique IDs (derived from the post createdAt
time) and then leveraging the query param-based Twitter intents URLs.
<a href="https://twitter.com/intent/tweet?url=https://github.com/takeshape/twithub%231595944469-1&hashtags=TwitHub">
Share
</a>
Replies
As it turns out GitHub also supports using query params to pre-populate Issues. This lent itself to start a conversation based on a post in the Issues section, seeded with the text of the post. I could imagine a future iteration where I leverage the new-ish GitHub GraphQL API to easily harvest associate Issues and replies and embed them in the stream...
<a href="https://github.com/takeshape/twithub/issues/new?body=Welcome%20to%20TwitHub!%20What%20are%20you%20*foo*ing%3F%0A%0A---" rel="noopener noreferrer">
Reply
</a>
Follow me
Following seemed tricky. How could I give somebody the ability to see my latest posts without a backend to support the updates and push notifications? Oh, right, RSS. I created a simple template that would generate well-formed XML. Keeping to a general ethos of less is more, I was pleased to find out I could utilize the same query and template that produced the actual page to also generate the render logic on the route:
{%- if currentPath == 'README.xml' -%}
{{ renderRss(getTwitList.items, profile, env) }}
I wouldn't generally advocate overloading and obscure placement like this, but the end goal was a minimal, clean directory repo.
Generating the site
I needed a way to generate the README
that kept with the TwitHub ethos of being cheap, on-brand and with a minimum set of dependencies. GitHub Actions workflows were the obvious answer.
TakeShape's CLI has more conventional uses, like generating websites and uploading static site templates, so generating a single text file necessitated finding a Minimum Viable Project. I wanted the absolute least config and smallest set of dependencies that would let me output, first, a single README.md
, and then, as I really got the bug, full paginated Twit archives to head off the inevitable feature requests as this project takes off 🚀!
This workflow snippet became the heart of the whole generation process. Fortunately the CLI tools are surprisingly lightweight and run quickly via npx
. The step below executes reliably in the 15s - 20s range. Note the need to create a static
directory, even though we're not using one. A bug report has been filed...
steps:
...
- name: twitting...
if: steps.check-variables.outputs.initialized == 'true'
env:
TS_PROJECT_ID: ${{ secrets.TS_PROJECT_ID }}
TS_AUTH_TOKEN: ${{ secrets.TS_AUTH_TOKEN }}
run: |
test -d static || mkdir static
npx --package @takeshape/cli -c "tsg build"
I set the whole workflow to run on a schedule (every 30 minutes) as a reasonably responsive way to regenerate the site and pull in the latests posts.
on:
schedule:
- cron: "*/30 * * * *"
With GitHub Actions repository/workflow dispatch, it should be possible to make this a push using TakeShape's webhooks. We'll be adding in a feature to modify the payload to add the required fields soon.
Finally, I had to add in a step to commit the updated files back to the repo. This is a fairly standard process. The one tweak I made was to tag the repo with a timestamp so you can easily review the history of content updates in the releases/tags section:
Distributing TwitHub
The GitHub template repository feature was released last year but I had never used it. To turn the repo into a template repo, you just check a box in the repo settings and it adds a bright green Use this template
button at the top. This is a nice touch vs. instructing a user to fork the repo, but it offers no extra functionality.
I wanted to create a more tailored experience, in which the repo was customized for the new user, so I created another GitHub Actions workflow, _setup.yml
. By responding to the initial push to the default branch, I could replace the existing README.md
with new content.
Mixing yaml, which is sensitive to indentation, and a multiline bash string proved tricky. I could concatenate multiple lines, but that made reading and editing the text difficult. A heredoc
style was nice, but that conflicted with the yaml. Eventually I settled on a sed
replacement to strip the indented spaces from a simple multiline bash string.
run: |
echo "
...
7. That's it! Twit away by adding Twit content in TakeShape.
" | sed -E 's/^[[:space:]]{10}//' > README.md
Now, about 30 seconds after forking the template repo, helpful setup instructions replace the copied README.md
file and a scheduled check will later delete the file to prevent unnecessary runs.
Meme factory
Finally, with the pattern imported and the GitHub Action running, I was ready to create some content. Below is a sample post. You'll see I used a markdown body, which I'd describe as a key product differentiator of TwitHub.
I got impatient waiting for the scheduled action to run, so I triggered the workflow dispatch event myself from the workflow page. Another nice, relatively recent, addition to Github Actions. My twit was there, my galaxy brain meme was masked and cropped properly, and I shared the post with a friend who replied "cool." He clearly did not recognize he was looking at Web@Next in action.
In conclusion
TwitHub is silly but it was certainly a fun project to put together. The many limitations of the publishing platform—the GitHub README
—forced me to consider creative solutions that worked surprisingly well and recalled the great times I had hacking early websites and MySpace profile pages.
So go ahead: copy the repo, give it the same name as your github username, and start twitting! Your github.com/[USERNAME]
page will be most excellent.