<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Dimi Chakarov</title>
    <description>I code, I design, I lead, I write, I follow, I play, I deliver, I understand, I teach. Currently @ Just Eat. Say hi.</description>
    <link>https://dchakarov.com/</link>
    <atom:link href="https://dchakarov.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Fri, 08 May 2026 11:34:27 +0000</pubDate>
    <lastBuildDate>Fri, 08 May 2026 11:34:27 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      
      <item>
        <title>If you think your CTO is losing the fight on AI, get them to read this one.</title>
        <description>&lt;p&gt;If you think your CTO is losing the fight on AI, get them to read this one.&lt;/p&gt;

&lt;div class=&quot;link-preview&quot;&gt;
  &lt;a href=&quot;https://ctosub.com/p/the-ctos-incoming-storms&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
  &lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!UnOK!,w_1200,h_675,c_fill,f_jpg,q_auto:good,fl_progressive:steep,g_auto/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6a9cd725-1d64-4165-aef4-f77eb075d2a0_1024x1024.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; onerror=&quot;this.remove()&quot;&gt;
    &lt;span class=&quot;link-preview-text&quot;&gt;
      &lt;strong&gt;The CTO’s Incoming Storms&lt;/strong&gt;
    &lt;span class=&quot;link-preview-desc&quot;&gt;Let me name the six conversations I’m watching land on CTOs right now, and the stance I’d want you holding in each one.&lt;/span&gt;
      &lt;span class=&quot;link-preview-domain&quot;&gt;ctosub.com&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/div&gt;
</description>
        
          <description>&lt;p&gt;If you think your CTO is losing the fight on AI, get them to read this one.&lt;/p&gt;

</description>
        
        <pubDate>Fri, 08 May 2026 11:26:05 +0000</pubDate>
        <link>https://dchakarov.com/blog/the-ctos-incoming-storms-1126/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/the-ctos-incoming-storms-1126/</guid>
        
        
        <category>notes</category>
        
      </item>
      
    
      
      <item>
        <title>Microsoft accidentally told the truth about AI</title>
        <description>&lt;div class=&quot;video-embed&quot;&gt;
&lt;iframe src=&quot;https://www.youtube.com/embed/4CIlTOnc6I8&quot; title=&quot;Microsoft accidentally told the truth about AI&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
</description>
        
          <description>&lt;div class=&quot;video-embed&quot;&gt;
&lt;iframe src=&quot;https://www.youtube.com/embed/4CIlTOnc6I8&quot; title=&quot;Microsoft accidentally told the truth about AI&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
</description>
        
        <pubDate>Thu, 23 Apr 2026 22:54:33 +0000</pubDate>
        <link>https://dchakarov.com/blog/microsoft-accidentally-told-the-truth-about-ai-2254/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/microsoft-accidentally-told-the-truth-about-ai-2254/</guid>
        
        
        <category>notes</category>
        
      </item>
      
    
      
      <item>
        <title>Splat: Limited-Time Avatar</title>
        <description>&lt;div class=&quot;link-preview&quot;&gt;
  &lt;a href=&quot;https://apps.apple.com/gb/app/mz-maze-adventures/id6746577798?eventid=6762443482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
  &lt;img src=&quot;https://is1-ssl.mzstatic.com/image/thumb/kkzz14kgv5Y9VU5LlJqL4Q/1200x630fo.jpg&quot; alt=&quot;&quot; loading=&quot;lazy&quot; onerror=&quot;this.remove()&quot;&gt;
    &lt;span class=&quot;link-preview-text&quot;&gt;
      &lt;strong&gt;Splat: Limited-Time Avatar&lt;/strong&gt;
    &lt;span class=&quot;link-preview-desc&quot;&gt;A brand-new avatar to celebrate the recent iOS emojis. Free to enable during the event. Don&lt;/span&gt;
      &lt;span class=&quot;link-preview-domain&quot;&gt;apps.apple.com&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/div&gt;
</description>
        
          <description>&lt;div class=&quot;link-preview&quot;&gt;
  &lt;a href=&quot;https://apps.apple.com/gb/app/mz-maze-adventures/id6746577798?eventid=6762443482&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
  &lt;img src=&quot;https://is1-ssl.mzstatic.com/image/thumb/kkzz14kgv5Y9VU5LlJqL4Q/1200x630fo.jpg&quot; alt=&quot;&quot; loading=&quot;lazy&quot; onerror=&quot;this.remove()&quot;&gt;
    &lt;span class=&quot;link-preview-text&quot;&gt;
      &lt;strong&gt;Splat: Limited-Time Avatar&lt;/strong&gt;
    &lt;span class=&quot;link-preview-desc&quot;&gt;A brand-new avatar to celebrate the recent iOS emojis. Free to enable during the event. Don&lt;/span&gt;
      &lt;span class=&quot;link-preview-domain&quot;&gt;apps.apple.com&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/div&gt;
</description>
        
        <pubDate>Wed, 22 Apr 2026 18:33:11 +0000</pubDate>
        <link>https://dchakarov.com/blog/splat-limited-time-avatar-1833/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/splat-limited-time-avatar-1833/</guid>
        
        
        <category>notes</category>
        
      </item>
      
    
      
      <item>
        <title>If you&apos;re not following BK Coffeeshop ☕️ you&apos;re missing out!</title>
        <description>&lt;p&gt;If you’re not following BK Coffeeshop ☕️ you’re missing out!&lt;/p&gt;

&lt;div class=&quot;link-preview&quot;&gt;
  &lt;a href=&quot;https://vm.tiktok.com/ZNRqXYBCf/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
    &lt;span class=&quot;link-preview-text&quot;&gt;
      &lt;strong&gt;TikTok - Make Your Day&lt;/strong&gt;
      &lt;span class=&quot;link-preview-domain&quot;&gt;vm.tiktok.com&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/div&gt;
</description>
        
          <description>&lt;p&gt;If you’re not following BK Coffeeshop ☕️ you’re missing out!&lt;/p&gt;

</description>
        
        <pubDate>Tue, 21 Apr 2026 10:11:52 +0000</pubDate>
        <link>https://dchakarov.com/blog/tiktok-make-your-day-1011/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/tiktok-make-your-day-1011/</guid>
        
        
        <category>notes</category>
        
      </item>
      
    
      
      <item>
        <title>I have added a tip jar to my maze game! This way I can try to keep it ad-free an</title>
        <description>&lt;p&gt;I have added a tip jar to my maze game! This way I can try to keep it ad-free and enjoyable! As a special thank you, anyone who donates will unlock an avatar in game!&lt;/p&gt;

&lt;div class=&quot;link-preview&quot;&gt;
  &lt;a href=&quot;https://apps.apple.com/gb/app/mz-maze-adventures/id6746577798&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
  &lt;img src=&quot;https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/f5/70/c9/f570c95b-bf1d-a737-4514-f971acd84e30/Placeholder.mill/1200x630wa.jpg&quot; alt=&quot;&quot; loading=&quot;lazy&quot; onerror=&quot;this.remove()&quot;&gt;
    &lt;span class=&quot;link-preview-text&quot;&gt;
      &lt;strong&gt;MZ: Maze Adventures App - App Store&lt;/strong&gt;
    &lt;span class=&quot;link-preview-desc&quot;&gt;Download MZ: Maze Adventures by Dimitar Chakarov on the App Store. See screenshots, ratings and reviews, user tips and more games like MZ: Maze Adv...&lt;/span&gt;
      &lt;span class=&quot;link-preview-domain&quot;&gt;apps.apple.com&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/div&gt;
</description>
        
          <description>&lt;p&gt;I have added a tip jar to my maze game! This way I can try to keep it ad-free and enjoyable! As a special thank you, anyone who donates will unlock an avatar in game!&lt;/p&gt;

</description>
        
        <pubDate>Thu, 16 Apr 2026 18:20:27 +0000</pubDate>
        <link>https://dchakarov.com/blog/mz-maze-adventures-app-app-store-1820/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/mz-maze-adventures-app-app-store-1820/</guid>
        
        
        <category>notes</category>
        
      </item>
      
    
      
      <item>
        <title>A Refined Authentication Experience | Dan Lages</title>
        <description>&lt;div class=&quot;link-preview&quot;&gt;
  &lt;a href=&quot;https://danlages.com/engineering/authentication/2026/01/04/refined-authentication-experience.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
  &lt;img src=&quot;/assets/images/logo.svg&quot; alt=&quot;&quot; loading=&quot;lazy&quot; onerror=&quot;this.remove()&quot;&gt;
    &lt;span class=&quot;link-preview-text&quot;&gt;
      &lt;strong&gt;A Refined Authentication Experience | Dan Lages&lt;/strong&gt;
    &lt;span class=&quot;link-preview-desc&quot;&gt;In a large-scale consumer application, it is vital that the authentication experience is both secure and intuitive. The Just Eat Takeaway applicati...&lt;/span&gt;
      &lt;span class=&quot;link-preview-domain&quot;&gt;danlages.com&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/div&gt;
</description>
        
          <description>&lt;div class=&quot;link-preview&quot;&gt;
  &lt;a href=&quot;https://danlages.com/engineering/authentication/2026/01/04/refined-authentication-experience.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
  &lt;img src=&quot;/assets/images/logo.svg&quot; alt=&quot;&quot; loading=&quot;lazy&quot; onerror=&quot;this.remove()&quot;&gt;
    &lt;span class=&quot;link-preview-text&quot;&gt;
      &lt;strong&gt;A Refined Authentication Experience | Dan Lages&lt;/strong&gt;
    &lt;span class=&quot;link-preview-desc&quot;&gt;In a large-scale consumer application, it is vital that the authentication experience is both secure and intuitive. The Just Eat Takeaway applicati...&lt;/span&gt;
      &lt;span class=&quot;link-preview-domain&quot;&gt;danlages.com&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/div&gt;
</description>
        
        <pubDate>Mon, 30 Mar 2026 12:27:39 +0000</pubDate>
        <link>https://dchakarov.com/blog/a-refined-authentication-experience-dan-lages-1227/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/a-refined-authentication-experience-dan-lages-1227/</guid>
        
        
        <category>notes</category>
        
      </item>
      
    
      
      <item>
        <title>I got inspired by my friend Costa’s write-up and decided to try it on my own blo</title>
        <description>&lt;p&gt;I got inspired by my friend Costa’s write-up and decided to try it on my own blog. A more detailed article coming soon… for now, enjoy the result&lt;/p&gt;

&lt;div class=&quot;link-preview&quot;&gt;
  &lt;a href=&quot;https://www.costafotiadis.com/things/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
  &lt;img src=&quot;https://www.costafotiadis.com/content/images/size/w1200/2026/03/Gemini_Generated_Image_hejjiehejjiehejj.png&quot; alt=&quot;&quot; loading=&quot;lazy&quot; onerror=&quot;this.remove()&quot;&gt;
    &lt;span class=&quot;link-preview-text&quot;&gt;
      &lt;strong&gt;Things&lt;/strong&gt;
    &lt;span class=&quot;link-preview-desc&quot;&gt;Or how I made a telegram bot update my website&lt;/span&gt;
      &lt;span class=&quot;link-preview-domain&quot;&gt;costafotiadis.com&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/div&gt;
</description>
        
          <description>&lt;p&gt;I got inspired by my friend Costa’s write-up and decided to try it on my own blog. A more detailed article coming soon… for now, enjoy the result&lt;/p&gt;

</description>
        
        <pubDate>Sun, 29 Mar 2026 19:36:12 +0000</pubDate>
        <link>https://dchakarov.com/blog/things-1936/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/things-1936/</guid>
        
        
        <category>notes</category>
        
      </item>
      
    
      
      <item>
        <title>AI is making CEOs delusional</title>
        <description>&lt;div class=&quot;video-embed&quot;&gt;
&lt;iframe src=&quot;https://www.youtube.com/embed/Q6nem-F8AG8&quot; title=&quot;AI is making CEOs delusional&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
</description>
        
          <description>&lt;div class=&quot;video-embed&quot;&gt;
&lt;iframe src=&quot;https://www.youtube.com/embed/Q6nem-F8AG8&quot; title=&quot;AI is making CEOs delusional&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
</description>
        
        <pubDate>Sun, 29 Mar 2026 19:06:52 +0000</pubDate>
        <link>https://dchakarov.com/blog/ai-is-making-ceos-delusional-1906/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/ai-is-making-ceos-delusional-1906/</guid>
        
        
        <category>notes</category>
        
      </item>
      
    
      
      <item>
        <title>Universal Links are hard. Good that we have Alberto to help us out!</title>
        <description>&lt;p&gt;Universal Links are hard. Good that we have Alberto to help us out!&lt;/p&gt;

&lt;div class=&quot;link-preview&quot;&gt;
  &lt;a href=&quot;https://albertodebortoli.com/2026/01/15/universal-links-at-scale-the-challenges-nobody-talks-about/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;
  &lt;img src=&quot;https://albertodebortoli.com/content/images/size/w1200/2026/01/0_5RZ69ENMLrgyZwGV.webp&quot; alt=&quot;&quot; loading=&quot;lazy&quot; onerror=&quot;this.remove()&quot;&gt;
    &lt;span class=&quot;link-preview-text&quot;&gt;
      &lt;strong&gt;Universal Links At Scale: The Challenges Nobody Talks About&lt;/strong&gt;
    &lt;span class=&quot;link-preview-desc&quot;&gt;A deep dive into the practical challenges of implementing, testing, and maintaining Universal Links at scale&lt;/span&gt;
      &lt;span class=&quot;link-preview-domain&quot;&gt;albertodebortoli.com&lt;/span&gt;
    &lt;/span&gt;
  &lt;/a&gt;
&lt;/div&gt;
</description>
        
          <description>&lt;p&gt;Universal Links are hard. Good that we have Alberto to help us out!&lt;/p&gt;

</description>
        
        <pubDate>Sun, 29 Mar 2026 18:31:28 +0000</pubDate>
        <link>https://dchakarov.com/blog/universal-links-at-scale-the-challenges-nobody-tal-1831/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/universal-links-at-scale-the-challenges-nobody-tal-1831/</guid>
        
        
        <category>notes</category>
        
      </item>
      
    
      
      <item>
        <title>How I Built a Telegram Bot to Post Notes to My Jekyll Blog</title>
        <description>&lt;p&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;‘ve been wanting to add a “micro-blog” to my site for a while – a place for quick thoughts, links I’ve found interesting, or a video worth sharing. Not a full article, just a line or two with a link. The kind of thing you’d share in a group chat.&lt;/p&gt;

&lt;p&gt;Posting on my site isn’t that hard right now. I duplicate the last post (as an MD file), rename it, open it in Panda, and start writing. Once I’m done, I open Fork and push my changes to GitHub. A few seconds later, the article is live.&lt;/p&gt;

&lt;p&gt;That’s all good when I have my laptop with me. If I only have my phone, I would have to do all that in the GitHub web interface or develop a complicated workflow in order to use an app on the phone. Another problem is styling. The current blog theme works for long technical articles. To a lesser extent, it also works for a short story or a review. It doesn’t work for link posts or for hot takes.&lt;/p&gt;

&lt;p&gt;Then my friend Costa wrote about &lt;a href=&quot;https://www.costafotiadis.com/things/&quot;&gt;how he built something similar&lt;/a&gt; using a Telegram bot, and I decided to build my own version. His setup uses Railway to host the bot and appends entries to a single page. Mine is slightly different: each note becomes its own Jekyll post, and the bot runs locally on my Mac. That way, I avoid having to sign up for a new service.&lt;/p&gt;

&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;
&lt;p&gt;The setup is simple:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;I send a message to my Telegram bot (from my phone, desktop, wherever)&lt;/li&gt;
  &lt;li&gt;A Python script running on my Mac picks it up&lt;/li&gt;
  &lt;li&gt;The script creates a Jekyll post file via the GitHub Contents API&lt;/li&gt;
  &lt;li&gt;GitHub Pages rebuilds the site automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No servers, no containers, no CI pipelines. The bot talks directly to GitHub’s REST API to create files in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_posts/&lt;/code&gt; directory, and GitHub Pages does the rest.&lt;/p&gt;

&lt;h2 id=&quot;the-bot&quot;&gt;The Bot&lt;/h2&gt;
&lt;p&gt;The bot is about 150 lines of Python code, built on two frameworks: a Python wrapper for the Telegram API called &lt;a href=&quot;https://github.com/python-telegram-bot/python-telegram-bot&quot;&gt;python-telegram-bot&lt;/a&gt; and &lt;a href=&quot;https://www.python-httpx.org/&quot;&gt;httpx&lt;/a&gt;. It handles three types of messages (for now):&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Just a link&lt;/strong&gt; - fetches the page’s Open Graph metadata (title, description, image) and creates a note with a link preview card&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;A link + my thoughts&lt;/strong&gt; - As above, but use my notes as the post body&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Plain text&lt;/strong&gt; - saved as a quick thought, no link involved&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;creating-posts-via-the-github-api&quot;&gt;Creating Posts via the GitHub API&lt;/h3&gt;
&lt;p&gt;The clever bit (borrowed from Costa’s approach) is that the bot never touches a local git repo. It uses the &lt;a href=&quot;https://docs.github.com/en/rest/repos/contents&quot;&gt;GitHub Contents API&lt;/a&gt; to create files directly:&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;gh_create_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://api.github.com/repos/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GITHUB_REPO&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/contents/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;base64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b64encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;utf-8&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GH_HEADERS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;raise_for_status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PUT&lt;/code&gt; request creates the file and commits it. GitHub Pages picks up the new commit and rebuilds the site within a couple of minutes.&lt;/p&gt;

&lt;h3 id=&quot;fetching-link-previews&quot;&gt;Fetching Link Previews&lt;/h3&gt;
&lt;p&gt;When you share a link in Telegram, WhatsApp, or Slack, you get a nice preview card with the page’s title, description, and image. I wanted the same thing on my blog.&lt;/p&gt;

&lt;p&gt;The bot fetches Open Graph metadata from the URL:&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fetch_og_metadata&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;follow_redirects&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;User-Agent&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Mozilla/5.0 ...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;page&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_get_meta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;og:title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;twitter:title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_get_meta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;og:description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;twitter:description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;image&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_get_meta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;og:image&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;twitter:image&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It then generates an HTML preview card that gets embedded in the post. One gotcha: sites behind Cloudflare (like Medium) return a “Just a moment…” challenge page instead of real content. The bot detects these bad titles and falls back to extracting a human-readable title from the URL path.&lt;/p&gt;

&lt;h3 id=&quot;security&quot;&gt;Security&lt;/h3&gt;
&lt;p&gt;Since the bot publishes directly to my site, I needed some restrictions. Posts are only allowed from my Telegram user ID. And all the keys to connect to GitHub and Telegram are stored on my Mac.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;ALLOWED_USER_ID&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;ALLOWED_USER_ID&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;handle_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;effective_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ALLOWED_USER_ID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reply_text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Not authorised.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# ... create the post
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-jekyll-side&quot;&gt;The Jekyll Side&lt;/h2&gt;
&lt;p&gt;I needed a few tweaks on my blog templates to make this work.&lt;/p&gt;
&lt;h3 id=&quot;a-separate-layout-for-notes&quot;&gt;A Separate Layout for Notes&lt;/h3&gt;
&lt;p&gt;Regular blog posts use my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post.html&lt;/code&gt; layout, which shows a featured image, read time estimate, and prev/next navigation. That’s too heavy for a one-liner. I created a minimal &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;note.html&lt;/code&gt; layout:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;---
layout: default
---

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;post note&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;meta&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;{{ page.date | date: &apos;%B %d, %Y&apos; }} &lt;span class=&quot;ni&quot;&gt;&amp;amp;middot;&lt;/span&gt; Note&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  {{ content }}
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No title heading (the body is the content), no read time, no post navigation. Just the date and the text.&lt;/p&gt;

&lt;h3 id=&quot;a-notes-tab&quot;&gt;A Notes Tab&lt;/h3&gt;
&lt;p&gt;I added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notes.md&lt;/code&gt; page that lists only posts in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notes&lt;/code&gt; category, and added it to my site navigation. The filter is a simple Liquid condition:&lt;/p&gt;
&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{% for post in site.posts %}
  {% if post.categories contains &apos;notes&apos; %}
    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- show the note --&amp;gt;&lt;/span&gt;
  {% endif %}
{% endfor %}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notes also appear on the homepage alongside regular posts. The homepage template detects notes and shows them differently – just the date and content text, without the usual title-as-link treatment.&lt;/p&gt;

&lt;h3 id=&quot;link-preview-cards&quot;&gt;Link Preview Cards&lt;/h3&gt;
&lt;p&gt;The preview cards are styled with CSS to look like the link previews you see in messaging apps – a border, rounded corners, the page’s image on top, and the title/description/domain below. The key CSS:&lt;/p&gt;
&lt;div class=&quot;language-scss highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;.link-preview&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;darken&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;white&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;15%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border-radius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;overflow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.5rem&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;img&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;100%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;max-height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;300px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;object-fit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cover&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One thing to watch out for: Jekyll uses kramdown for Markdown processing, and kramdown can mangle raw HTML if you’re not careful. The fix is to wrap the HTML card in kramdown’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{::nomarkdown}...{:/nomarkdown}&lt;/code&gt; tags, which tells it to pass the HTML through untouched.&lt;/p&gt;

&lt;h2 id=&quot;running-it-locally&quot;&gt;Running It Locally&lt;/h2&gt;
&lt;p&gt;Costa uses &lt;a href=&quot;https://railway.com/&quot;&gt;Railway&lt;/a&gt; to host his bot. I didn’t want to sign up for another service, so I run mine locally on my Mac using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;launchd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.plist&lt;/code&gt; file tells launchd to start the bot on login and restart it if it crashes:&lt;/p&gt;
&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span class=&quot;nt&quot;&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;string&amp;gt;&lt;/span&gt;com.dchakarov.notebot&lt;span class=&quot;nt&quot;&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class=&quot;nt&quot;&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/path/to/my-bot/.venv/bin/python&lt;span class=&quot;nt&quot;&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/path/to/my-bot/bot.py&lt;span class=&quot;nt&quot;&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RunAtLoad&lt;span class=&quot;nt&quot;&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;key&amp;gt;&lt;/span&gt;KeepAlive&lt;span class=&quot;nt&quot;&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Install it with:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cp &lt;/span&gt;com.dchakarov.notebot.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.dchakarov.notebot.plist
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The bot runs as long as my Mac is on. Since Telegram uses polling, it works regardless of whether I send the message from my phone, tablet, or desktop. Messages sent while the Mac is off simply queue up in Telegram and get processed the next time the bot starts. And yes, my Mac is a laptop, so my thoughts don’t go live immediately. So what?&lt;/p&gt;

&lt;h2 id=&quot;setting-it-up-yourself&quot;&gt;Setting It Up Yourself&lt;/h2&gt;
&lt;p&gt;If you want to build your own, here’s what you need:&lt;/p&gt;

&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;ol&gt;
  &lt;li&gt;A Jekyll blog on GitHub Pages&lt;/li&gt;
  &lt;li&gt;Python 3.9+&lt;/li&gt;
  &lt;li&gt;A Telegram account&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;steps&quot;&gt;Steps&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;0. Think of a good name for your bot.&lt;/strong&gt; Don’t be lame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a Telegram bot.&lt;/strong&gt; Message &lt;a href=&quot;https://t.me/BotFather&quot;&gt;@BotFather&lt;/a&gt; on Telegram, send &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/newbot&lt;/code&gt;, and follow the prompts. Copy the bot token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Get your Telegram user ID.&lt;/strong&gt; Message &lt;a href=&quot;https://t.me/userinfobot&quot;&gt;@userinfobot&lt;/a&gt; on Telegram. It replies with your numeric ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Create a GitHub personal access token.&lt;/strong&gt; Go to &lt;a href=&quot;https://github.com/settings/tokens?type=beta&quot;&gt;GitHub Settings &amp;gt; Developer settings &amp;gt; Fine-grained tokens&lt;/a&gt;. Create a token scoped to your blog repository with “Contents: Read and write” permission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Set up the bot.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;cool-bot &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;cool-bot
python3 &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; venv .venv
.venv/bin/pip &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;python-telegram-bot httpx python-dotenv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;5. Create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env&lt;/code&gt; file&lt;/strong&gt; with your credentials:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;TELEGRAM_BOT_TOKEN=your-token-here
GITHUB_TOKEN=your-github-pat
GITHUB_REPO=yourusername/yourusername.github.io
ALLOWED_USER_ID=your-telegram-id
SITE_URL=https://yoursite.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;6. Add the bot script&lt;/strong&gt; - you can find the full source code here: &lt;a href=&quot;https://github.com/dchakarov/telegram-notes-bot&quot;&gt;Telegram notes bot&lt;/a&gt;. Feel free to contribute!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Test it.&lt;/strong&gt; Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.venv/bin/python bot.py&lt;/code&gt;, send a test message to your bot, and check your GitHub repo for the new file. Since one of the features of this setup is that posts get published immediately, be ready to delete your test posts from GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Set up launchd&lt;/strong&gt; to run it automatically once you’re happy (see the section above).&lt;/p&gt;

&lt;h2 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h2&gt;
&lt;p&gt;I didn’t learn Python, that’s for sure. With Costa’s article and code on one side and Claude on the other, implementing this setup was a fun journey. Testing something that publishes live is challenging, so I had to pull locally and delete the test posts rather quickly on each iteration.
There are still a few wrinkles to fix, and a few that will never get fixed, but overall, I am quite pleased with the result. Expect more frequent, shorter posts in the near future.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href=&quot;/notes&quot;&gt;Notes&lt;/a&gt; section to see it in action.&lt;/p&gt;

&lt;p&gt;Hero image by &lt;a href=&quot;https://unsplash.com/@dtravisphd?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText&quot;&gt;David Travis&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/brown-fountain-pen-on-notebook-5bYxXawHOQg?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
</description>
        
          <description>&lt;p&gt;&lt;span class=&quot;dropcap&quot;&gt;I&lt;/span&gt;‘ve been wanting to add a “micro-blog” to my site for a while – a place for quick thoughts, links I’ve found interesting, or a video worth sharing. Not a full article, just a line or two with a link. The kind of thing you’d share in a group chat.&lt;/p&gt;

</description>
        
        <pubDate>Sat, 28 Mar 2026 08:00:00 +0000</pubDate>
        <link>https://dchakarov.com/blog/telegram-bot-jekyll-notes/</link>
        <guid isPermaLink="true">https://dchakarov.com/blog/telegram-bot-jekyll-notes/</guid>
        
        
      </item>
      
    
  </channel>
</rss>
