<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vladimir Simić</title>
    <description>The latest articles on DEV Community by Vladimir Simić (@vsimke).</description>
    <link>https://dev.to/vsimke</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3875506%2F53706f7c-a558-4711-97e0-1bf6fb02c8dc.jpg</url>
      <title>DEV Community: Vladimir Simić</title>
      <link>https://dev.to/vsimke</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vsimke"/>
    <language>en</language>
    <item>
      <title>Why Your Laravel + Inertia.js Fetch Requests Fail with 419 After Save</title>
      <dc:creator>Vladimir Simić</dc:creator>
      <pubDate>Thu, 16 Apr 2026 08:52:20 +0000</pubDate>
      <link>https://dev.to/vsimke/why-your-laravel-inertiajs-fetch-requests-fail-with-419-after-save-3lg4</link>
      <guid>https://dev.to/vsimke/why-your-laravel-inertiajs-fetch-requests-fail-with-419-after-save-3lg4</guid>
      <description>&lt;p&gt;After a save or reset action, clicking "Preview" returns a 419 Page Expired — even though the page looks fine and you're clearly authenticated. Here's why, and the two-line fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Setup&lt;/strong&gt;&lt;br&gt;
A settings page uses Inertia.js for save/reset (&lt;code&gt;router.patch&lt;/code&gt; / &lt;code&gt;router.delete&lt;/code&gt;) and a plain &lt;code&gt;fetch()&lt;/code&gt; POST for a live email preview endpoint. The fetch manually reads the CSRF token from the meta tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-CSRF-TOKEN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta[name="csrf-token"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLMetaElement&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works on first load. After a save it breaks with 419.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why It Breaks&lt;/strong&gt;&lt;br&gt;
Laravel's CSRF system works through a session-bound token. On every response, including Inertia partial responses, Laravel rotates the &lt;code&gt;XSRF-TOKEN&lt;/code&gt; cookie to stay in sync with the session.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;meta[name="csrf-token"]&lt;/code&gt; tag is rendered once by Blade at initial page load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"csrf-token"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"{{ csrf_token() }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inertia never touches that tag. It manages CSRF itself via the &lt;code&gt;XSRF-TOKEN&lt;/code&gt; cookie. The same way Axios does. So after an Inertia response, the cookie holds the current token, but the meta tag is stale. Any subsequent &lt;code&gt;fetch()&lt;/code&gt; reading the meta tag sends an outdated token, and Laravel rejects it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Second Bug&lt;/strong&gt;&lt;br&gt;
Even if CSRF were fine, the code calls &lt;code&gt;res.text()&lt;/code&gt; unconditionally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setPreviewHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Laravel returns 419, &lt;code&gt;res.text()&lt;/code&gt; gets the "Page Expired" HTML error page, which gets rendered inside the iframe. The error looks like broken preview content rather than a clear failure, making it very confusing to diagnose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt; &lt;br&gt;
Two changes to &lt;code&gt;fetchPreview&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchPreview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setPreviewLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Read the token Laravel actually keeps fresh — the XSRF-TOKEN cookie&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xsrfToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;XSRF-TOKEN=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateKey&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;same-origin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-XSRF-TOKEN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;xsrfToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// ← correct header name&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activeLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                         &lt;span class="c1"&gt;// ← don't render error HTML in iframe&lt;/span&gt;
      &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;common:failed_to_update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;setPreviewHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;common:failed_to_update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setPreviewLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things changed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;X-CSRF-TOKEN&lt;/code&gt; → &lt;code&gt;X-XSRF-TOKEN&lt;/code&gt;, reading from the cookie Laravel keeps fresh instead of the stale Blade meta tag.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if (!res.ok)&lt;/code&gt; guard - prevents error HTML from leaking into the iframe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why X-XSRF-TOKEN Works&lt;/strong&gt;&lt;br&gt;
Laravel's &lt;code&gt;VerifyCsrfToken&lt;/code&gt; middleware checks for a valid token in two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_token&lt;/code&gt; field in the request body&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-CSRF-TOKEN&lt;/code&gt; header (raw token)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-XSRF-TOKEN&lt;/code&gt; header (encrypted cookie value — automatically decrypted and verified)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cookie is &lt;code&gt;HttpOnly: false&lt;/code&gt; intentionally so JavaScript can read it. Inertia, Axios, and now your fetch all use this same mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaway&lt;/strong&gt;&lt;br&gt;
When mixing Inertia navigation with manual &lt;code&gt;fetch()&lt;/code&gt; calls in the same page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't read &lt;code&gt;meta[name="csrf-token"]&lt;/code&gt; — it's stale after any Inertia response.&lt;/li&gt;
&lt;li&gt;Do read the &lt;code&gt;XSRF-TOKEN&lt;/code&gt; cookie and send it as &lt;code&gt;X-XSRF-TOKEN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Always check &lt;code&gt;res.ok&lt;/code&gt; before rendering a response body anywhere visible to the user.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>laravel</category>
      <category>inertia</category>
      <category>csrf</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built a CLI that turns your git commits into social media posts</title>
      <dc:creator>Vladimir Simić</dc:creator>
      <pubDate>Wed, 15 Apr 2026 08:59:29 +0000</pubDate>
      <link>https://dev.to/vsimke/i-built-a-cli-that-turns-your-git-commits-into-social-media-posts-4573</link>
      <guid>https://dev.to/vsimke/i-built-a-cli-that-turns-your-git-commits-into-social-media-posts-4573</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every week I ship things. Features, fixes, refactors. It's all there in git.&lt;/p&gt;

&lt;p&gt;But writing a post about it? I'd either skip it entirely or spend 20 minutes staring at a blank box writing something that sounded nothing like me.&lt;/p&gt;

&lt;p&gt;I knew what I built. Git knew what I built. The gap was just turning one into the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/vsimke/commitpost" rel="noopener noreferrer"&gt;commitpost&lt;/a&gt; — a CLI that reads your git history, sends it to Claude AI, and writes a post in your voice. Optionally generates a cover image with your actual code as the background.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; commitpost
commitpost generate &lt;span class="nt"&gt;--include-image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One command. Post and image ready to copy-paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Runs &lt;code&gt;git log --author --since&lt;/code&gt; to grab your recent commits&lt;/li&gt;
&lt;li&gt;Sends them to Claude with your tone profile (a writing sample you set once)&lt;/li&gt;
&lt;li&gt;Generates the post text&lt;/li&gt;
&lt;li&gt;Extracts a real changed file from your commits for the cover image&lt;/li&gt;
&lt;li&gt;Syntax-highlights it, applies blur, overlays the headline&lt;/li&gt;
&lt;li&gt;Outputs a 1200×627px PNG ready for LinkedIn, Bluesky, or X&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The interesting technical parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cover image without a browser&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first instinct was Puppeteer. Installed it, no Chrome binary. Pivoted to &lt;a href="https://github.com/vercel/satori" rel="noopener noreferrer"&gt;satori&lt;/a&gt; (Vercel's JSX→SVG renderer) + &lt;code&gt;@resvg/resvg-js&lt;/code&gt; (Rust SVG→PNG) + &lt;code&gt;sharp&lt;/code&gt; for compositing.&lt;/p&gt;

&lt;p&gt;No headless browser. No Electron. Pure Node, works anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blurring code correctly&lt;/strong&gt;&lt;br&gt;
Blurring sounds simple. It wasn't.&lt;/p&gt;

&lt;p&gt;SVG &lt;code&gt;feGaussianBlur&lt;/code&gt; clips at element boundaries — above ~sigma 20, no visible effect. Switched to &lt;code&gt;sharp.blur()&lt;/code&gt;. But blurring a transparent PNG destroys the alpha channel and the layer disappears entirely when composited.&lt;/p&gt;

&lt;p&gt;The fix: render background + code as one solid image first, then blur. Solid pixels blur correctly. Then composite the UI (headline, author) on top as a separate transparent layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code that starts at something meaningful&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Initially the image showed the first 20 lines of a file — which is always imports and require statements. Nobody wants to see &lt;code&gt;import React from 'react'&lt;/code&gt; as their cover image.&lt;/p&gt;

&lt;p&gt;Added &lt;code&gt;findMeaningfulStartLine()&lt;/code&gt; — scans for the first class/function definition per language and starts there instead. Works for JS/TS, PHP, Python, Ruby, Java, Go, Rust, C#.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tone profiles&lt;/strong&gt;&lt;br&gt;
One-time setup: paste a writing sample (old blog post, previous tweet, anything). Stored in &lt;code&gt;~/.commitpost/config.json&lt;/code&gt;. Every generated post runs through that sample as a style guide. Posts sound like you, not like ChatGPT.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta moment
&lt;/h2&gt;

&lt;p&gt;I used commitpost to generate the LinkedIn post announcing commitpost. It worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; commitpost

&lt;span class="c"&gt;# Set your API key (Anthropic)&lt;/span&gt;
commitpost config &lt;span class="nt"&gt;--set-key&lt;/span&gt; sk-ant-...

&lt;span class="c"&gt;# Generate&lt;/span&gt;
commitpost generate

&lt;span class="c"&gt;# With cover image&lt;/span&gt;
commitpost generate &lt;span class="nt"&gt;--include-image&lt;/span&gt; &lt;span class="nt"&gt;--image-style&lt;/span&gt; dark_code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/vsimke/commitpost" rel="noopener noreferrer"&gt;vsimke/commitpost&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/commitpost" rel="noopener noreferrer"&gt;commitpost&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Post length is configurable (&lt;code&gt;--length short/medium/long&lt;/code&gt;), there are 4 image styles, and 5 built-in tone profiles if you don't want to set up your own.&lt;/p&gt;

&lt;p&gt;Open source, MIT. Would love feedback — especially from people using it on non-JS projects.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>node</category>
      <category>cli</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
