Introduction
I wanted to give my GitHub profile a fresh new look, and since I’ve started writing articles on Dev.to, I thought it would be a great idea to automatically showcase the most recent ones on my profile.
You can find pre-made GitHub Actions workflows which periodically fetches the latest content and updates your README. However, I wanted to create a very basic, more flexible solution from scratch using Deno 2.0 (with Typescript).
That’s why I’m writing this article, to share the steps with you, which you can easily adapt whether you’re fetching data from Dev.to, YouTube videos, RSS feeds, Medium, or any other source!
I’m assuming you already have a GitHub profile repository set up with a README.md
file. If not, you can get started here.
1. Creating the GitHub Action Workflow
We can start by setting up the GitHub Action Workflow:
Create a
.github/workflows/update-readme-articles.yml
file in your profile repository.Add a
schedule
to run the job every month at 0:00 UTC (or more often if you are a fast writer, which is not my case 😅). You can also include theworkflow_dispatch
event trigger, enabling the workflow to be run manually when needed.-
Add in the
jobs
section the following steps:- Check out the codebase with
actions/checkout@v4
. - Set up Deno with
denoland/setup-deno@v2
. - Run the
update-readme-articles.ts
script to update the README. - Commit and push only changed files to the repository.
- Check out the codebase with
name: Update readme articles
on:
# Add a schedule to run the job every month at 0:00 UTC
schedule:
- cron: '0 0 1 * *'
# Allow running this workflow manually
workflow_dispatch:
jobs:
update-readme-articles:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Setup Deno environment
uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Run script
run: |
deno run -A scripts/update-readme-articles.ts
- name: Commit and push changes
env:
# This is necessary in order to push a commit to the main branch
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add .
if [[ -n "$(git status --porcelain)" ]]; then
git commit -m ":wrench: Update readme articles"
git push origin main
fi
2. Updating the README
2.1 Adding markers
To update the README with the latest articles from Dev.to, start by adding markers to your README.md
file as shown below:
# Hi 👋, I'm Joan Roucoux
## About me 💬
...
## My latest articles 📝
<!-- ARTICLES:START -->
<!-- ARTICLES:END -->
🚀 This section is updated by GitHub Actions Workflows ❤️
...
2.2 Initializing the Deno Project
Run the following command to initialize a Deno project (make sure Deno is installed locally. If not, follow this):
deno init
2.3 Creating the script logic
Next, you’ll need to create a script that follows these steps:
Create the required files:
scripts/types.ts
: define the types for your articles.
scripts/update-readme-articles.ts
: main logic to fetch and process articles.Read the original file content with
fs.readFile()
.Fetch latest articles using the /articles operation from the Dev.to API with your name. For instance with mine, it gives https://dev.to/api/articles?username=joanroucoux&page=1&per_page=5. Feel free to adapt the source of your content here as mentioned in the introduction.
Generate new markdown with
generateArticlesContent()
and replace the content between markers withreplaceContentBetweenMarkers()
.Save the updated file with
fs.writeFile()
.
The updated markdown code will look like this:
...
<!-- ARTICLES:START -->
- [Update README with Latest Articles Using GitHub Actions](https://dev.to/joanroucoux/update-readme-with-latest-articles-using-github-actions-41m3)
- [Building a Marvel Search Application with Qwik](https://dev.to/joanroucoux/building-a-marvel-search-application-with-qwik-ll7)
<!-- ARTICLES:END -->
...
Which will be rendered as follows:
Here are the .ts
files, which performs the above steps:
// types.ts
interface User {
name: string;
username: string;
twitter_username: string | null;
github_username: string;
user_id: number;
website_url: string;
profile_image: string;
profile_image_90: string;
}
export interface Article {
type_of: string;
id: number;
title: string;
description: string;
readable_publish_date: string;
slug: string;
path: string;
url: string;
comments_count: number;
public_reactions_count: number;
collection_id: number | null;
published_timestamp: string;
positive_reactions_count: number;
cover_image: string | null;
social_image: string;
canonical_url: string;
created_at: string;
edited_at: string;
crossposted_at: string | null;
published_at: string;
last_comment_at: string;
reading_time_minutes: number;
tag_list: string[];
tags: string;
user: User;
}
export interface ArticlePreview {
title: string;
url: string;
}
// update-readme-articles.ts
import * as fs from 'node:fs/promises';
import path from 'node:path';
import { Article, ArticlePreview } from './types.ts';
const DEV_TO_API_BASE_URL = 'https://dev.to/api';
const USERNAME = 'joanroucoux';
const main = async (): Promise<void> => {
// Read the original file content
const filePath = '../README.md';
const markdown = await readFile(filePath);
// Proceed only if the file was read successfully
if (markdown) {
// Fetch latest articles
const articles = await fetchArticles(USERNAME);
// Generate new content
const newContent = generateArticlesContent(articles);
// Replace content between markers
const START_MARKER = '<!-- ARTICLES:START -->';
const END_MARKER = '<!-- ARTICLES:END -->';
const updatedMarkdown = replaceContentBetweenMarkers(
markdown,
START_MARKER,
END_MARKER,
newContent
);
// Save the updated file
await saveFile(filePath, updatedMarkdown);
}
};
// Fetch latest articles
const fetchArticles = async (
username: string,
page: number = 1,
perPage: number = 5
): Promise<ArticlePreview[]> => {
const response = await fetch(
`${DEV_TO_API_BASE_URL}/articles?username=${username}&page=${page}&per_page=${perPage}`
);
const data: Article[] = await response.json();
return data?.map((article: Article) => ({
title: article.title,
url: article.url,
}));
};
// Generate markdown from articles
const generateArticlesContent = (articles: ArticlePreview[]): string => {
let markdown = '';
articles?.forEach((article) => {
markdown += `- [${article.title}](${article.url})\n`;
});
return markdown;
};
// Read file
const readFile = async (filePath: string): Promise<string | null> => {
try {
const absolutePath = path.resolve(import.meta.dirname, filePath);
console.log('Reading file from:', absolutePath);
return await fs.readFile(absolutePath, 'utf8');
} catch (err) {
console.error('Error reading file:', err);
return null;
}
};
// Generate updated markdown
const replaceContentBetweenMarkers = (
markdown: string,
startMarker: string,
endMarker: string,
newContent: string
): string => {
const regex = new RegExp(`(${startMarker})([\\s\\S]*?)(${endMarker})`, 'g');
return markdown.replace(regex, `$1\n${newContent}$3`);
};
// Save file
const saveFile = async (filePath: string, content: string): Promise<void> => {
try {
const absolutePath = path.resolve(import.meta.dirname, filePath);
await fs.writeFile(absolutePath, content, 'utf8');
console.log('File has been saved successfully!');
} catch (err) {
console.error('Error saving file:', err);
}
};
main();
3. Pushing and testing
That’s it! After pushing all the files to your repository, you can manually trigger the workflow from the "Actions" tab or wait for it to run based on the scheduled trigger. You can also test the script locally by running deno run -A --watch scripts/update-readme-articles.ts
before pushing. Once completed, check the logs for any errors and verify that your README.md
has been updated correctly.
4. Conclusion
By automating the process of updating your README with GitHub Actions, you make sure the file stays up-to-date with your latest content. It's also a flexible solution that you can easily tweak to integrate different sources of data!
I hope you found this tutorial helpful! You can check out my repository here 🚀
Resources:
Top comments (1)
This is awesome 🚀🚀! I love how you’ve streamlined the process with GitHub Actions.
Keep up the great work.
Can’t wait to see what you come up with next 💯