If you go to https://github.com/peterbe it lists the most recent blog posts here on my blog. The page is rebuilt every hour using GitHub Actions. This blog post is about how I built that, so that you can build something just like it.
In case you don't have access or it's quicker to look at a picture, this is what it looks like:
The way GitHub profiles work is you create a GitHub repo that is in the same name as your username. In my case, my username is peterbe
, and the repo is thus called peterbe
. So, it's named https://github.com/peterbe/peterbe. It has to be a public repo for this to work.
In that repo you have a README.md
and mine looks like this: https://github.com/peterbe/peterbe/blob/main/README.md?plain=1 If you look carefully, the Markdown in that README.md
contains:
<!-- blog posts -->
...
<!-- /blog posts -->
By default, HTML comments work in GitHub-flavored Markdown just like they do in HTML.
Then, I have a Node script that finds that inside the file and replaces its content with a list of Markdown links.
Node script
The Node script itself can be seen here: script/update-readme-blog-posts.ts
Yes, it's a simple script, but that's the beauty of it. You can easily copy this and implement it the way you need. In my case, the most recent blog posts are available on an API: GET /api/v1/plog/homepage
Then, all the script does, is, it opens the existing README.md
, generates the list of links and then injects it back into the content using...
const spaceRex = /(<!-- blog posts -->)(.|\n)*(<!-- \/blog posts -->)/;
Lastly, it writes it back into the file using...
function saveReadme(text: string) {
writeFileSync("README.md", text, "utf-8");
}
Something I think is very important is that all of that script and how you run it, has nothing to do with GitHub Actions. Completely independent of GitHub Actions, I can now run this script on my laptop, over and over, till I'm happy with how it works. When I type npm run test
it runs:
DRYRUN=1 node --experimental-strip-types script/update-readme-blog-posts.ts
Note that it's a .ts
TypeScript file, but because it's using Node >=22, I don't need any other tooling to transpile the TypeScript to JavaScript.
The GitHub Action
In its glory, the whole workflow is here: .github/workflows/update-readme-blog-posts.yml
. There are a few things worth pointing out:
When it runs
on:
pull_request:
schedule:
- cron: '0 * * * *'
workflow_dispatch:
What this means is;
- It runs the script on every Pull Request
- I can manually run it using the button
- It's a scheduled job that runs every hour
This makes it easy to debug because within the workflow you'll see this:
- name: Lint code
if: ${{ github.event_name == 'pull_request' }}
run: npm run lint
i.e. it only checks the code itself when executed as a Pull Request. The quality of the code doesn't matter when it's run on a schedule.
And equally, you only want to bother trying to commit any changes if it's run on a schedule or started manually.
- name: Commit
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
...
An important feature is this:
git status | grep 'nothing to commit' && exit 0
That's a simple solution to bail out of the Bash code if the updating of the README.md
meant that nothing actually happened.
Comments
Thanks so much for this post, Peter! I have updated my GitHub profile to now also show the latest five posts from my blog and it looks so sweet :) I had to do mine a bit differently as I do not have an API endpoint that return JSON but I _do_ have an RSS feed. This means I had to quickly learn how one works with XML in Nodejs 🙈
It was surprisingly not that painful and the end result can be seen here: https://github.com/schalkneethling/schalkneethling 🤘