<?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.us-east-2.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>When a Single URL Stops Being Enough: Multi-Table Pages in Inertia.js</title>
      <dc:creator>Vladimir Simić</dc:creator>
      <pubDate>Tue, 23 Jun 2026 08:54:19 +0000</pubDate>
      <link>https://dev.to/vsimke/when-a-single-url-stops-being-enough-multi-table-pages-in-inertiajs-45o7</link>
      <guid>https://dev.to/vsimke/when-a-single-url-stops-being-enough-multi-table-pages-in-inertiajs-45o7</guid>
      <description>&lt;h1&gt;
  
  
  Two Tables, One URL: Solving Inertia.js State Collisions
&lt;/h1&gt;

&lt;p&gt;If you've worked with &lt;strong&gt;Inertia.js + TanStack Table&lt;/strong&gt; for server-side filtering and pagination, you know how satisfying the pattern feels.&lt;/p&gt;

&lt;p&gt;A single &lt;code&gt;router.get()&lt;/code&gt; call gives you a lot for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filter state lives in the URL&lt;/li&gt;
&lt;li&gt;Links are shareable&lt;/li&gt;
&lt;li&gt;Browser back/forward works out of the box&lt;/li&gt;
&lt;li&gt;Sorting, pagination, and filtering are all server-driven&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a great default — until you need &lt;strong&gt;two independent tables on the same page.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Picture a typical admin dashboard with a Users table and an Orders table. Both support filtering, sorting, and pagination. Naturally, you reach for the same Inertia pattern you've been using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&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="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sortby&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works beautifully for one table. With two, the URL becomes a shared state container — and things start to collide.&lt;/p&gt;

&lt;p&gt;When the Users table navigates to page 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;/dashboard?users_page=3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens to the Orders filters? Without extra care, they get reset or overwritten.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 1: Namespace Your Parameters
&lt;/h2&gt;

&lt;p&gt;The most obvious fix is prefixing every parameter by table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;users_page&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&amp;amp;users_sort=name&lt;/span&gt;
&lt;span class="py"&gt;orders_page&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&amp;amp;orders_status=paid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. But every filter hook, every query builder, and every table abstraction now needs to understand namespaced parameters. In my experience, that complexity compounds quickly and adds friction faster than it adds value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Option 2: Give Each Table Its Own Request
&lt;/h2&gt;

&lt;p&gt;Instead of routing all table interactions through &lt;code&gt;router.get()&lt;/code&gt;, I switched to &lt;code&gt;axios.get()&lt;/code&gt; per table.&lt;/p&gt;

&lt;p&gt;Each table gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its own dedicated endpoint&lt;/li&gt;
&lt;li&gt;Its own local state&lt;/li&gt;
&lt;li&gt;Its own independent requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No shared URL, no collisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hook
&lt;/h2&gt;

&lt;p&gt;The core of the pattern is a reusable &lt;code&gt;useDataTable&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useDataTable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;initialData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;columns&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tableData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTableData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sorting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSorting&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;columnFilters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setColumnFilters&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPagination&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;pageIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&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;fetchData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDebounce&lt;/span&gt;&lt;span class="p"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;columnFilters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sortby&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sorting&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sorting&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;desc&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&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;asc&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="p"&gt;})&lt;/span&gt;

    &lt;span class="nf"&gt;setTableData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;fetchData&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;span class="nx"&gt;sorting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;columnFilters&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useReactTable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tableData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rowCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tableData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;manualFiltering&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="na"&gt;manualPagination&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="na"&gt;manualSorting&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="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why This Works Well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tables are truly independent.&lt;/strong&gt; Paginate one, and the other doesn't flinch. That's exactly the behavior you want from an admin dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first render still comes from Inertia.&lt;/strong&gt; Initial data is hydrated from Inertia props:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tableData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTableData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No loading spinner on mount, no client-side waterfall. The page feels fast from the start — the best of both worlds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSRF is handled automatically.&lt;/strong&gt; Because axios reads Laravel's &lt;code&gt;XSRF&lt;/code&gt; cookie by default, there's no extra ceremony. It just works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tradeoff
&lt;/h2&gt;

&lt;p&gt;This pattern makes a deliberate trade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you gain:&lt;/strong&gt; independent tables, simpler code than full query namespacing, and cleaner admin pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you give up:&lt;/strong&gt; filter state is no longer in the URL. Filters aren't bookmarkable. Shared links won't preserve table state.&lt;/p&gt;

&lt;p&gt;Whether that matters depends entirely on your use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use Each Approach
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stick with &lt;code&gt;router.get()&lt;/code&gt; + URL state when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building a public-facing search or filter page&lt;/li&gt;
&lt;li&gt;Users need to share or bookmark filtered views&lt;/li&gt;
&lt;li&gt;Deep-linking is part of the product (product catalogs, reporting screens, search results)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Switch to axios per table when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's internal admin tooling&lt;/li&gt;
&lt;li&gt;The page has multiple independent data views&lt;/li&gt;
&lt;li&gt;URL shareability simply isn't a requirement&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  This Isn't a Criticism of Inertia
&lt;/h2&gt;

&lt;p&gt;I almost called this post &lt;em&gt;"When Inertia isn't enough"&lt;/em&gt; — but that framing misses the point.&lt;/p&gt;

&lt;p&gt;Inertia is excellent. This is just a case where a single URL stops being the right state container for the job. Recognizing that distinction is the whole insight.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Are You Handling This?
&lt;/h2&gt;

&lt;p&gt;If you're managing multiple server-side tables with Inertia, I'd love to know your approach. Are you namespacing query params? Using local state and axios? Something smarter?&lt;/p&gt;

&lt;p&gt;Drop a comment — if this is a common enough pain point, I'm considering packaging the hook or writing a more detailed follow-up.&lt;/p&gt;

</description>
      <category>fullstack</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <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;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fvsimke%2Fcommitpost%2Fmain%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fvsimke%2Fcommitpost%2Fmain%2Fdemo.gif" alt="commitpost demo" width="760" height="380"&gt;&lt;/a&gt;&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>
