PERSONAL NOTE

Blog Posts


# Show Your Blog Posts on Your GitHub Profile

Smart Writer gives every published blog an RSS feed. With a few minutes of setup,
you can also make your GitHub profile automatically display your latest posts, just
like a live news ticker under your bio.

Once configured, it updates itself every
day with zero effort.

## What you need

- A GitHub account
- At least one published post on your Smart Writer blog
- About 10 minutes

## Step 1: Find your feed URL

Every Smart Writer blog exposes an RSS feed at:

    https://stdnote.com/blog/rss.php?user=YOUR_USERNAME

Replace `YOUR_USERNAME` with the username you publish under. For example, the
user `admin` has the feed:

    https://stdnote.com/blog/rss.php?user=admin

Open your feed URL in a browser to confirm it loads and shows your posts
before continuing.

## Step 2: Create your profile README

1. On GitHub, create a new repository named exactly like your username
   (for example, if your username is `admin`, the repo must be `admin/admin`).
2. Make it **public** and check **"Add a README file"**.
3. GitHub now displays this README on your profile page.

## Step 3: Create the update workflow

Inside your new repository:

1. Create the folder `.github/workflows` (use **Add file → Create new file**
   and type `.github/workflows/update.yml` as the filename).
2. Paste the following content:

```yaml
name: Update RSS Feed

on:
  schedule:
    - cron: '0 2 * * *'
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.x'

      - name: Generate feed
        run: |
          python3 generate_feed.py "${{ secrets.RSS_FEED_URL }}" 5 > feed.md
          sha256sum feed.md | awk '{print $1}' > feed.hash

      - name: Update README
        run: |
          TODAY=$(date -u +"%b %d, %Y")

          sed -i "s/^Last update:.*/Last update: $TODAY/" README.md

          OLD_HASH=$(grep -oE '<!-- FEED_HASH: [a-f0-9]+' README.md | awk '{print $3}')
          NEW_HASH=$(cat feed.hash)

          if [ "$OLD_HASH" = "$NEW_HASH" ]; then
            exit 0
          fi

          sed -e '/<!-- FEED_START -->/,/<!-- FEED_END -->/{//!d}' \
              -e '/<!-- FEED_START -->/r feed.md' \
              README.md > README.new

          sed -i "s/<!-- FEED_HASH:.*/<!-- FEED_HASH: $NEW_HASH -->/" README.new
          mv README.new README.md

      - name: Commit and push
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

          git add README.md
          git commit -m "Update RSS feed (auto)" || true
          git pull --rebase origin main
          git push origin HEAD:main

Step 4: Add the feed reader script

Still in the repository, create a file named generate_feed.py in the root (Add file → Create new file) and paste:


#!/usr/bin/env python3

import sys
import urllib.request
import xml.etree.ElementTree as ET
from datetime import datetime
from email.utils import parsedate_to_datetime

FEED_URLS = sys.argv[1:-1]
MAX_ITEMS = int(sys.argv[-1])

ATOM_NS = {'atom': 'http://www.w3.org/2005/Atom'}

def fetch(url):
    req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
    with urllib.request.urlopen(req) as resp:
        return resp.read()

def parse_date(date_str):
    if not date_str:
        return None
    try:
        return datetime.fromisoformat(date_str.replace('Z', '+00:00'))
    except ValueError:
        pass
    try:
        return parsedate_to_datetime(date_str)
    except Exception:
        pass
    return None

def collect_items(feed_url):
    root = ET.fromstring(fetch(feed_url))
    entries = root.findall('.//item')
    if not entries:
        entries = root.findall('.//atom:entry', ATOM_NS)

    items = []
    for e in entries:
        title = e.findtext('title') or e.findtext('{http://www.w3.org/2005/Atom}title')
        link = e.findtext('link')
        if not link:
            link_el = e.find('{http://www.w3.org/2005/Atom}link')
            if link_el is not None:
                link = link_el.attrib.get('href')
        pub = e.findtext('pubDate') or e.findtext('{http://www.w3.org/2005/Atom}updated')
        dt = parse_date(pub)
        items.append({'title': title or '(untitled)', 'link': link or '', 'dt': dt})
    return items

def main():
    all_items = []
    for url in FEED_URLS:
        try:
            all_items.extend(collect_items(url))
        except Exception as exc:
            print('skipped feed %s: %s' % (url, exc), file=sys.stderr)

    dated = [i for i in all_items if i['dt'] is not None]
    undated = [i for i in all_items if i['dt'] is None]
    dated.sort(key=lambda i: i['dt'], reverse=True)
    all_items = dated + undated

    lines = []
    for i in all_items[:MAX_ITEMS]:
        pub_pretty = i['dt'].strftime('%b %d, %Y') if i['dt'] else ''
        suffix = ' (*%s*)' % pub_pretty if pub_pretty else ''
        lines.append('- [%s](%s)%s' % (i['title'], i['link'], suffix))

    print('\n'.join(lines))

if __name__ == '__main__':
    main()

This script accepts multiple feed URLs, so you can list several blogs later without changing anything else.

Step 5: Store your feed URL as a secret

  1. In your repository, go to Settings.
  2. Left sidebar: Secrets and variables → Actions.
  3. Click New repository secret.
  4. Name: RSS_FEED_URL
  5. Secret: your feed URL from Step 1
  6. Click Add secret.

Secrets are write-only: GitHub never displays the value again after saving. That is intentional and safe.

Step 6: Prepare your README

Edit your README.md and add these markers where you want the post list to appear:

Last update: never

### Latest posts

<!-- FEED_HASH: 0 --> <!-- FEED_START --> <!-- FEED_END -->

The workflow replaces everything between the markers with your latest posts. Anything you write outside the markers is never touched, so you can freely add badges, a bio, or links around the list.

Step 7: Run it

  1. Open the Actions tab of the repository.
  2. Click Update RSS Feed in the left sidebar.
  3. Click Run workflow, then the green button.
  4. Wait about a minute. The run turns green and a new commit appears.

Open your GitHub profile. Your latest posts are now listed in the README.

From now on, the workflow runs daily at 02:00 UTC and keeps the list fresh automatically. Publish a new post on Smart Writer, and the next morning it is on your profile.

Troubleshooting

  • The workflow fails with "refusing to allow a GitHub App...":

you probably edited a workflow file while an older run was queued. Trigger a fresh run with Run workflow instead of re-running the failed job.

  • The list does not appear: check that the <!-- FEED_START --> and

<!-- FEED_END --> markers exist in your README, exactly as written above.

  • "feed not found" in the logs: your feed URL is wrong. Open it in a

browser first to verify it returns your posts.

  • Nothing changes although the run is green: the feed content has not

changed since the last update, so the workflow skipped the commit. This is normal.

FAQ

Can I show more than 5 posts? Yes. In the workflow file, change the 5 in the generate feed step to the number of posts you want.

Can I combine several feeds? Yes. Add a second secret (for example RSS_FEED_URL_2) and list it in the workflow:


python3 generate_feed.py "${{ secrets.RSS_FEED_URL }}" "${{ secrets.RSS_FEED_URL_2 }}" 5 > feed.md

The script merges all feeds, sorts the posts by date, and shows the newest first.

Does this cost anything? No. Public repositories on GitHub include GitHub Actions for free, well above what a daily two-minute job consumes.

Does the Smart Writer feed expose private notes? No. The feed only contains posts you published to your public blog.