sync(medium): extract post summary from body
authorxangelo <me@xangelo.ca>
Tue, 22 Jul 2025 04:41:22 +0000 (00:41 -0400)
committerxangelo <me@xangelo.ca>
Tue, 22 Jul 2025 04:41:22 +0000 (00:41 -0400)
The first line of the medium post is a short, pithy summary of the post
formatted as a blockquote. We strip this from the final post and assign
it to frontmatter

.github/scripts/medium_to_hugo.py
.github/workflows/medium-sync.yml
.gitignore
content/posts/medium/code-reviews-are-a-failure.md
content/posts/medium/posse-has-it-backwards.md

index f90d54e0d26d9bb3f0809fa733148a2abb9c8ded..ee64e239f5983f6ae3c06443e3fd6ea842461e0a 100644 (file)
@@ -2,6 +2,7 @@ import feedparser
 import os
 import re
 import frontmatter
+import sys
 from markdownify import markdownify as md
 import html
 from datetime import datetime
@@ -12,6 +13,13 @@ RSS_URL = "https://medium.com/feed/@xangelo"
 OUTPUT_DIR = "content/posts/medium"
 EXISTING_SLUGS = {f[:-3] for f in os.listdir(OUTPUT_DIR) if f.endswith(".md")}
 
+# default to false, but read from --force flag if it's set
+FORCE_REBUILD = False
+
+if len(sys.argv) > 1 and sys.argv[1] == "--force":
+    FORCE_REBUILD = sys.argv[2] == "true"
+
+
 def slugify(title):
     return re.sub(r"[^\w-]", "", re.sub(r"\s+", "-", title.lower())).strip("-")
 
@@ -27,14 +35,15 @@ def resolve_medium_media_links(content):
         medium_url = match.group(1)
         try:
             # Follow the Medium media link to see where it redirects
-            response = requests.get(medium_url, allow_redirects=True, timeout=10)
+            print(f"Resolving Medium media link: {medium_url}")
+            response = requests.head(medium_url, allow_redirects=True, timeout=10)
             final_url = response.url
             
             # Check if the final URL is a GitHub Gist
             parsed_url = urlparse(final_url)
             if parsed_url.netloc == 'gist.github.com':
                 print(f"Resolved Medium media link: {medium_url} -> {final_url}")
-                return f"<script src=\"{final_url}\"></script>"
+                return f"<script src=\"{final_url}.js\"></script>"
             else:
                 print(f"Medium media link does not resolve to GitHub Gist: {medium_url} -> {final_url}")
                 return match.group(0)  # Return original if not a gist
@@ -47,33 +56,43 @@ def resolve_medium_media_links(content):
 
 feed = feedparser.parse(RSS_URL)
 
-print(f"Parsing {RSS_URL}, {len(feed.entries)} posts")
+print(f"Parsing {RSS_URL}, {len(feed.entries)} posts, force rebuild: {FORCE_REBUILD}")
 
 for entry in feed.entries:
     slug = slugify(entry.title)
-    if slug in EXISTING_SLUGS:
+    if slug in EXISTING_SLUGS and not FORCE_REBUILD:
         continue
 
     content_html = entry.get("content", [{}])[0].get("value", "") or entry.get("summary", "")
     markdown_content = md(html.unescape(content_html))
 
-    post = frontmatter.Post(markdown_content)
+    formatted_content = resolve_medium_media_links(markdown_content)
+
+    # Extract the first line as the summary and strip it from content
+    lines = formatted_content.strip().split('\n')
+    extracted_summary = lines[0].strip()
+    remaining_content = '\n'.join(lines[1:]).lstrip('\n')
+
+    # remove the blockquote from the summary
+    extracted_summary = re.sub('> *', '', extracted_summary).strip()
+    # remove all other markdown formatting from the summary
+    extracted_summary = re.sub(r'(\*\*|\*|__|_|`|~~|<[^>]+>|\[([^\]]+)\]\([^)]+\))', r'\2', extracted_summary).strip()
+
+    post = frontmatter.Post(remaining_content)
     post["title"] = entry.title
+    post["summary"] = extracted_summary
     post["date"] = entry.published
     post["slug"] = slug
     post["draft"] = False
     post["medium_link"] = entry.link
 
-    # Resolve Medium media links to GitHub Gists
-    post["content"] = resolve_medium_media_links(post["content"])
-
     # the last line of a post is a stat line that looks like this:
     # ![](https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ca9ab4d5b529)
     # we should strip these out so that they don't count towards the viewer count on medium
-    post["content"] = post["content"].replace("![](https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ca9ab4d5b529)", "")
+    post.content = post.content.replace("![](https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ca9ab4d5b529)", "")
 
     # add a line to the bottom of the post to indicate that it's from Medium
-    post["content"] += "\n\n---\n\nThis was originally published on Medium - " + entry.link
+    post.content += "\n\n---\n\nThis was originally published on Medium - " + entry.link
 
     output_path = os.path.join(OUTPUT_DIR, f"{slug}.md")
     with open(output_path, "w", encoding="utf-8") as f:
index 07ad8308363a3fdb3f12f90e59e2ea46f8395784..f300eb2020f9e3f0727512518630ca3239010cf2 100644 (file)
@@ -1,7 +1,12 @@
 name: Sync Medium to Hugo
 
 on:
-  workflow_dispatch: {}
+  workflow_dispatch:
+    inputs:
+      force:
+        description: "Force sync even if there are no changes"
+        required: false
+        default: false
   schedule:
     - cron: "*/30 * * * *" # Runs every 30m
 
@@ -23,14 +28,14 @@ jobs:
         run: pip install feedparser markdownify python-frontmatter requests
 
       - name: Run sync script
-        run: python .github/scripts/medium_to_hugo.py
+        run: python .github/scripts/medium_to_hugo.py --force ${{ inputs.force }}
 
       - name: Commit and push changes
         id: commit
         run: |
           git config user.name "Medium Sync Bot"
           git config user.email "actions@github.com"
-          git add content/posts/
+          git add content/posts/medium/
 
           # Check if there are any changes to commit
           if git diff --quiet && git diff --staged --quiet; then
index 8f602985176f3e8654b7266ea0f1ee1502a4581a..4882f35f8e1b38e99da9657af9baf0d88a265b39 100644 (file)
@@ -1,2 +1,3 @@
 public/*
 .DS_Store
+venv/
\ No newline at end of file
index c27aabb0ff309032d3a95988682c1e08a19a18d1..9863e6e2c581fd8881dc2df83c938a78fe7852bf 100644 (file)
@@ -3,25 +3,32 @@ date: Fri, 11 Feb 2022 04:34:25 GMT
 draft: false
 medium_link: https://xangelo.medium.com/code-reviews-are-a-failure-36b72a659de4?source=rss-d5a790d38792------2
 slug: code-reviews-are-a-failure
+summary: As a new startup with one or two engineers on staff, you’re very likely not
+  doing code reviews. Engineers at this stage have a very deep understanding of the
+  code — after all, they’ve probably written most of it. When it’s time for a new
+  feature, these initial developers know exactly how they’re going to implement it
+  given the architecture of their code base. Chances are, they keep their own work
+  in a branch, and open a Pull Request or Merge Request, but they aren’t asking someone
+  to take a look at it. Instead they’re making sure their changes work and they’re
+  merging it in themselves. Often they’ll do this many times a day as they crank out
+  features and bug fixes.
 title: Code Reviews are a Failure
 ---
 
-As a new startup with one or two engineers on staff, you’re very likely not doing code reviews. Engineers at this stage have a very deep understanding of the code — after all, they’ve probably written most of it. When it’s time for a new feature, these initial developers know exactly how they’re going to implement it given the architecture of their code base. Chances are, they keep their own work in a branch, and open a Pull Request or Merge Request, but they aren’t asking someone to take a look at it. Instead they’re making sure their changes work and they’re merging it in themselves. Often they’ll do this many times a day as they crank out features and bug fixes.
-
 At some point things are going better than they were and this small group of engineers start adding more! Now you have 5 or 6 engineers, all with varying familiarity of your code base. This is generally the first time Code Reviews come about — and normally for good reason. Often someone has pushed some code to production that has broken things and the developers take a step back and realize that maybe before they push code, it’s best if they have others review it. Perhaps bugs like this can be caught next time. And so they come up with rules and reasons for why they need Code Reviews. Non technical managers think “Ah, this won’t happen again — we’re instituting code reviews now!”
 
 We’ve all seen the reasons for Code Reviews:
 
-- Find bugs further downstream
-- Propagation of Knowledge
-- Team Ownership
-- Double check functionality/architecture
+* Find bugs further downstream
+* Propagation of Knowledge
+* Team Ownership
+* Double check functionality/architecture
 
 These are nonsense — Code Reviews in isolation almost always end up with the following results:
 
-- Reviews languishing in a “Ready for Review” state
-- Drastic code architecture changes
-- Being “Approved” based on social standing of the developer opening the request
+* Reviews languishing in a “Ready for Review” state
+* Drastic code architecture changes
+* Being “Approved” based on social standing of the developer opening the request
 
 Code Reviews are often seen as some kind of magic bullet to catching errors before they get merged into code bases. The ideal is that a developer gets a ticket, makes some code changes, and then shares those changes with everyone else on the team for feedback. The idea is that other developers, with perhaps more context, can catch potential issues or side-effects in the code that the developer doing the work may not have even known about.
 
@@ -61,8 +68,7 @@ Instead you end up open a code review that sits in review for days while enginee
 
 See the problem isn’t that the Code Review is bad — it’s that the Code Review is the first time anyone has actually looked at the code related to the problem.
 
-## The Solution to Code Reviews
-
+## The Solution to Code Reviews  
 There isnt one.
 
 All planning up front without a deadline isn’t helpful. All work without planning is pointless. But where your team draws the line between planning that’s “good enough” and the length of time attributed to feature development changes frequently. It changes based on team composition, it changes based on the company state, it changes based on the market your company operates in. The only thing that’s certain is that the amount of planning from feature to feature will be different.
@@ -75,10 +81,10 @@ Once the planning is done and a developer completes the code change, the Code Re
 
 Unit tests, Integration Tests, Synthetic/BlackBox Tests — all of these can help ease the time code spends stuck in code reviews. By minimizing the time spent in code reviews, and maximizing the time spent in planning instead we can achieve things like:
 
-- Actually find bugs further downstream and upstream
-- Propagation of Knowledge throughout the team
-- Team Ownership of a feature
-- Double check functionality/architecture
+* Actually find bugs further downstream and upstream
+* Propagation of Knowledge throughout the team
+* Team Ownership of a feature
+* Double check functionality/architecture
 
 How fun.
 
@@ -86,4 +92,4 @@ How fun.
 
 ---
 
-This was originally published on Medium - https://medium.com/@xangelo/posse-has-it-backwards-ca9ab4d5b529
+This was originally published on Medium - https://xangelo.medium.com/code-reviews-are-a-failure-36b72a659de4?source=rss-d5a790d38792------2
\ No newline at end of file
index 458e49a00d5f9f36baa9168f20edaeaaf011686d..68ea3cc538c57201d193a1bcf81f155f2fe40f16 100644 (file)
@@ -3,8 +3,8 @@ date: Mon, 21 Jul 2025 17:52:35 GMT
 draft: false
 medium_link: https://xangelo.medium.com/posse-has-it-backwards-ca9ab4d5b529?source=rss-d5a790d38792------2
 slug: posse-has-it-backwards
+summary: The Presentation of Self in Every Blog post
 title: POSSE Has it Backwards
-summary: The Presentation of Self in Every Blog Post
 ---
 
 I’ve been blogging for a number of years now, even though my website makes it seem like I stopped. You can check the wayback machine and see posts from [2011](https://web.archive.org/web/20110507234835/http://xangelo.ca) before composer was a thing in the PHP world, or you can jump back to [2005](https://web.archive.org/web/20050415040309/http://www.xangelo.com/) when I was more focused on the design of the website than the content itself. I’ve tried almost every blogging platform out there, and a few that never made it off my hard-drive.
@@ -25,7 +25,7 @@ I am not the same person I am on Facebook as I am on Mastodon.
 
 I am not the same person I am on Instagram as I am on Twitch.
 
-The truth is that I shift tone and intention in every room I am in, based on the room itself. Erving Goffman talks about this extensively in his book “**_The Presentation of Self in Everyday Life”;_** As the backdrop of our stage changes we present different sides of ourselves. This isn’t wrong or incorrect, and it isn’t being “fake”. This is the truth of who we are as people.
+The truth is that I shift tone and intention in every room I am in, based on the room itself. Erving Goffman talks about this extensively in his book “***The Presentation of Self in Everyday Life”;*** As the backdrop of our stage changes we present different sides of ourselves. This isn’t wrong or incorrect, and it isn’t being “fake”. This is the truth of who we are as people.
 
 The sorts of comments I leave on a LinkedIn post are vastly different from what I leave on X. They’re both facets of who I am, but they exist within the bounds of those systems. The systems inform how I interact with them.
 
@@ -59,16 +59,16 @@ Your website isn’t the orgin, it’s the destination. It’s a museum and coll
 
 The same tools that power POSSE can power EPOSS, you just need to point them the other way.
 
-For example, this post, even though it shows up on Medium, also appears on my website (https://xangelo.ca). It does this because Medium supports RSS and I can use that RSS feed to generate a post in markdown and feed it back to Hugo. This particular script looks at the RSS feed defined at RSS_URL and writes markdown versions of it to content/posts/medium so that I can track which posts are being imported: <https://github.com/AngeloR/angelor.github.io/blob/main/.github/scripts/medium_to_hugo.py>
+For example, this post, even though it shows up on Medium, also appears on my website (<https://xangelo.ca/posts/medium/posse-has-it-backwards/>). It does this because Medium supports RSS and I can use that RSS feed to generate a post in markdown and feed it back to Hugo. This particular script looks at the RSS feed defined at RSS\_URL and writes markdown versions of it to content/posts/medium so that I can track which posts are being imported: <https://github.com/AngeloR/angelor.github.io/blob/main/.github/scripts/medium_to_hugo.py>
 
-<https://medium.com/media/c7634bd7099d8b4a3c68e75789d29869/href>
+<script src="https://gist.github.com/AngeloR/cce5451ab00183e7dfeef5cc31ffefbe.js"></script>
 
 I’ve connected this to my github actions to run this script every 30 minutes to pull in new content from medium.
 
 EPOSS isn’t the antithesis of POSSE, it’s an evoluion.
 
-![](https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ca9ab4d5b529)
+
 
 ---
 
-This was originally published on Medium - https://medium.com/@xangelo/posse-has-it-backwards-ca9ab4d5b529
+This was originally published on Medium - https://xangelo.medium.com/posse-has-it-backwards-ca9ab4d5b529?source=rss-d5a790d38792------2
\ No newline at end of file