The one where you automatically add your latest posts to your GitHub profile

The one where you automatically add your latest posts to your GitHub profile

Take advantage of Hashnode's Public API

A few weeks ago I was taking a look at the list of posts featured on Hashnode when suddenly I saw one that immediately caught my eye:

Featured post about using Hashnode's Public API

In that post Sandro, member of the Hashnode Engineering team, lists up to 6 examples of what you can build taking advantage of their public GraphQL API.

I'm gonna talk here about the last example, because I really liked the idea of automatically adding my latest blog posts to my GitHub profile.

Jannik Wempe's profile is shown as an example of what we can achieve with the provided API and GitHub Actions.

No code is included in the post regarding this functionality, so I decided to investigate a bit further to implement it in my own profile.

GitHub profile

I started by reading the linked post by the people from GitHub, about how to manage the content of your profile.

Basically it says that you can include whatever information you want, as long as you meet prerequisites needed for it to be displayed:

  • You've created a repository with a name that matches your GitHub username.
  • The repository is public.
  • The repository contains a file named in its root.
  • The file contains any content.

Follow the instructions included in that post to properly create the required repository and once it's created you can see a banner in the sidebar with following message:

backpackerhh repository message

Jannik uses a few GitHub Actions to include his latest blog posts and his recent activity in his profile. So did I.

I'll tell you the steps I followed to make all of this work.

Recent activity

After cloning your new repository, add .github/workflows/update-recept-activity.yml file with following content:

name: Update Recent Activity

    # 8:00 and 20:00 every day
    - cron: '0 8,20 * * *'

    runs-on: ubuntu-latest
    name: Update Activity

      - uses: actions/checkout@v4

      # Pin the version to the latest stable version.
      # If you want to live on edge, use 'master' branch.
      - uses: recaptime-dev/github-activity-readme@master
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

A lot of stuff is going on here, so let me explain it:

At the start of each workflow job, GitHub automatically creates a unique GITHUB_TOKEN secret that you can use to authenticate on behalf of GitHub Actions. Check more details here.

Next you can see some screenshots for this workflow:

Manual dispatch enabled in the UI

backpackerhh repository recent activity workflow manual dispatch enabled

Job manually triggered

backpackerhh repository workflow manually triggered

Job triggered via schedule

backpackerhh repository workflow triggered via schedule

Job successfully executed

backpackerhh repository workflow successful execution

Latest blog posts

Add .github/workflows/update-latest-blog-posts.yml file with following content:

name: Update Latest Blog Posts

  # for trigger via webhooks
    types: [trigger]

    runs-on: ubuntu-latest
    name: Update Posts

      - uses: actions/checkout@v4

      - uses: backpackerhh/github-latest-hashnode-posts@main
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Once again a lot of stuff is going on here, so let me explain it:

Again, GITHUB_TOKEN is used to authenticate on behalf of GitHub Actions. A custom repository secret containing the Hashnode publication ID (HASHNODE_PUBLICATION_ID) is used as well.

That particular value doesn't have to be secret, so you could directly put it in the YAML file if it suits you better.

Either way, you can add a new repository secret in following URL:<username>/<username>/settings/secrets/actions/new

Next you can see some screenshots for this workflow:

Manual dispatch enabled in the UI

backpackerhh repository latest blog posts workflow manual dispatch enabled

Job manually triggered

backpackerhh repository job manually triggered

Job triggered via repository dispatch

backpackerhh repository job triggered via repository dispatch

Job successfully executed

backpackerhh repository job successfully executed

Add content dynamically

For all of the above to work, following content must be initially added to the file:

## Latest Blog Posts


## Recent GitHub Activity


Those pairs of comments are used as placeholders so the GitHub actions can dynamically include the content between them.

Notice that a commit is created every time the content changes.


You can configure actions permissions in following URL:<username>/<username>/settings/actions

You have to enable "Allow all actions and reusable workflows" permission (default) and grant "Read and write permissions" for workflows.

Custom GitHub action

This new repository is simply a fork from the custom action created by Jannik.

I preferred to create a fork instead of using his action because I wanted to customize some things.

Next, I'll talk about the more relevant files present in the repository.


In case you don't know anything about this file, check the official documentation for a detailed explanation about every option defined in it.

In summary, this file allows a package to make it easy for others to manage and install. Besides, you can list the packages your project depends on and the scripts that are available to be executed.

Force npm as package manager:

$ npm run preinstall

> github-latest-hashnode-posts@0.0.1 preinstall
> npx only-allow npm

Compile the action code:

$ npm run prepare

> github-latest-hashnode-posts@0.0.1 prepare
> ncc build src/index.js -o dist --source-map --license licenses.txt

ncc: Version 0.36.1
ncc: Compiling file index.js into CJS
   9kB  dist/licenses.txt
  40kB  dist/sourcemap-register.js
 936kB  dist/index.js
1092kB  dist/
1092kB  dist/
2077kB  [2108ms] - ncc 0.36.1

This step is really important, because the content within dist directory is the actual code that will run the GitHub action.


Here is where the configuration and the metadata of the GitHub action is actually defined. For more details about all available options, check the official documentation.

For this specific action, I'd like to highlight a couple of them:

  • inputs: A list of input parameters that the action accepts, such as GITHUB_TOKEN, HASHNODE_PUBLICATION_ID or MAX_POSTS.
  • runs: Describes how the action should be executed, including the runtime environment (node20) and the entry point (dist/index.js).


In this file is where all the action happens (pun intended).

A request to the Hashnode GraphQL API is made with the inputs previously defined in action.yml file:

query LatestPosts($id: ObjectId!, $first: Int!) {
  publication(id: $id) {
    posts(first: $first) {
      edges {
        node {
          coverImage {

Where id is the Hashnode publication ID and first is the number of posts that should be retrieved.

Hashnode provides a playground in case you need to customize your query. For instance, regarding what Jannik had, I added the url of the post to the query so in my profile each post includes a link in the title and the cover.

Hashnode API Playground

A function gives then expected format to all the posts retrieved from the API and replaces the content of the file with them.

As a reminder, if everything goes well a commit is created with those changes.

The missing piece

There is a big difference regarding how the two workflows mentioned above run to update my profile. While I'm ok running one workflow twice a day to check any recent activity, the other one should only run when an event related to a blog post is triggered, either because it was published, updated or deleted.

Knowing that I'd need to automate that process somehow, the first thing that came to my mind was the webhooks provided by Hashnode:

Hashnode form to create a new webhook

However, I'd need to provide a URL to some kind of serverless function that would validate the secret defined in Hashnode (optional, but recommended) and trigger a request to the desired repository in GitHub.

There are a lot of services out there that would allow to do it very easily, such as AWS Lambda, Vercel or Netlify, to name a few, but it feels kind of overkill to set up something like that for what I want to achieve.

Bear in mind that I didn't check that approach, so I might be overlooking something else.

For that reason, I thought that it could be easier to just trigger an action from another repository.

Hashnode provides an integration with GitHub, so all my posts are backed up automatically to this repository:

GitHub integration in Hashnode

After some research, I found this article which proved to be an invaluable help.

Let's see how it's done.

Add .github/workflows/dispatcher.yml file to the repository where blog posts are backed up with following content:

name: Dispatcher

    branches: [main]

    name: Dispatch event
    runs-on: ubuntu-latest
    timeout-minutes: 2
      - name: Dispatch
        run: |
            curl -L \
              -X POST \
              -H "Accept: application/vnd.github+json" \
              -H "Authorization: Bearer ${{ secrets.DISPATCH_TOKEN }}" \
              -d '{"event_type":"trigger"}'

Yet once again a lot of stuff is going on here, so let me explain it:

Again, a custom repository secret is required to be used as authorization token (DISPATCH_TOKEN).

You can add a new repository secret in following URL:<username>/<blog_posts_repository>/settings/secrets/actions/new

The value of that secret must be the value of a personal access token (PAT) associated to your account.

If you prefer to create a classic PAT follow next steps:

  1. Go to
  2. Fill in following fields:
    • Note -> Blog posts
    • Expiration -> No expiration (at your own peril)
    • Select scopes -> workflow (will mark repo automatically)
  3. Click on Generate token button

If you prefer to create a fine-grained PAT follow next steps:

  1. Go to
  2. Fill in following fields:
    • Token name -> Blog posts
    • Expiration -> 90 days (custom is around 100 days)
    • Repository access -> Only select repositories -> select <username>/<username>
    • Permissions -> Repository permissions
      • Contents -> Access: Read and write
      • Metadata -> Access: Read access (automatically selected)
  3. Click on Generate token button

Although is less secure, for this particular use case I've created a classic PAT without expiration. I prefer to simply generate a token and just forget about it.

Next you can see some screenshots for this workflow:

Job triggered via push to main branch

blog posts repository job triggered via push to main branch

Job successfully executed

blog posts repository job successfully executed

It's worth mentioning that you could use an already available GitHub action instead, such as repository-dispatch.


Next, you can see how the process of updating my GitHub profile with the latest blog posts works:

Workflow diagram

And the result of running that workflow would be something as follows:

GitHub profile after workflow runs


There was a lot of trial and error to get everything to work correctly, especially due to permissions issues with workflows and PATs, but it's not a really complex process.

To be honest, I had never thought about having a GitHub profile with something more than pinned repositories, but seeing the potential of this kind of workflows, from now on I will keep thinking about how to improve it even more.

Thank you for reading and see you in the next one!