<?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: Zntb</title>
    <description>The latest articles on DEV Community by Zntb (@zntb).</description>
    <link>https://dev.to/zntb</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%2F1346104%2F2e9d5b95-45fd-4139-b9b4-550470bcc762.png</url>
      <title>DEV Community: Zntb</title>
      <link>https://dev.to/zntb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zntb"/>
    <language>en</language>
    <item>
      <title>I Built a Drag-and-Drop GitHub Profile README Builder with Next.js 16 &amp; React 19</title>
      <dc:creator>Zntb</dc:creator>
      <pubDate>Tue, 31 Mar 2026 05:34:11 +0000</pubDate>
      <link>https://dev.to/zntb/i-built-a-drag-and-drop-github-profile-readme-builder-with-nextjs-16-react-19-3h66</link>
      <guid>https://dev.to/zntb/i-built-a-drag-and-drop-github-profile-readme-builder-with-nextjs-16-react-19-3h66</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;A deep-dive into building a fully visual, self-hosted README builder — covering architecture decisions, the self-hosted stats API, drag-and-drop with dnd-kit, and how the block system works.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your GitHub profile README is often the first thing a recruiter, collaborator, or open-source maintainer sees. It's prime real estate — yet most developers either leave it blank or spend an afternoon wrestling with raw Markdown, hunting for widget URLs, and tweaking layouts by trial and error.&lt;/p&gt;

&lt;p&gt;I wanted to fix that. So I built &lt;strong&gt;GitHub Profile README Builder&lt;/strong&gt;: a fully visual, drag-and-drop editor where you configure your profile in a live preview canvas and export a production-ready &lt;code&gt;README.md&lt;/code&gt; in one click.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://github-profile-maker.vercel.app/" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
⭐ &lt;strong&gt;&lt;a href="https://github.com/zntb/github-profile-maker" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drag-and-drop canvas&lt;/strong&gt; — reorder blocks effortlessly with smooth dnd-kit animations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;25+ block types&lt;/strong&gt; — headings, paragraphs, skill icons, social badges, typing animations, collapsibles, code blocks, GitHub stats cards, activity graphs, trophies, visitor counters, quotes, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;65+ themes&lt;/strong&gt; — Tokyo Night, Dracula, Catppuccin Mocha, Nord, Gruvbox, GitHub Dark, and many others&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;11 starter templates&lt;/strong&gt; — Animated Developer, Full Stack Engineer, Data Scientist, DevOps / Cloud, Student, Cybersecurity Researcher, Game Developer, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live preview&lt;/strong&gt; — renders as close to GitHub's actual display as possible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted GitHub Stats API&lt;/strong&gt; — your own Next.js route handlers generate stat SVGs, so no third-party rate limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click export&lt;/strong&gt; — copy to clipboard or download &lt;code&gt;README.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully responsive&lt;/strong&gt; — a dedicated layout for desktop, tablet, and mobile&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend     Next.js 16 (App Router) · React 19 · TypeScript 5
Styling      Tailwind CSS v4 · tw-animate-css · shadcn/ui (radix-nova)
State        Zustand 5
DnD          dnd-kit (sortable)
Icons        Lucide React
Theming      next-themes
Toasts       Sonner
API          Next.js Route Handlers · GitHub REST &amp;amp; GraphQL APIs
Fonts        Outfit · JetBrains Mono
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Architecture: The Block System
&lt;/h2&gt;

&lt;p&gt;The entire editor is built around a simple, composable block model. Every element on the canvas is a &lt;code&gt;Block&lt;/code&gt;:&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="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BlockType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Block&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;&lt;code&gt;BlockType&lt;/code&gt; is a union of all supported block types — from layout primitives like &lt;code&gt;'container'&lt;/code&gt; and &lt;code&gt;'spacer'&lt;/code&gt; to GitHub-specific widgets like &lt;code&gt;'stats-card'&lt;/code&gt; and &lt;code&gt;'activity-graph'&lt;/code&gt;. The &lt;code&gt;children&lt;/code&gt; field enables nested blocks (collapsible sections, containers, stats rows).&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a New Block
&lt;/h3&gt;

&lt;p&gt;The pattern is deliberate and consistent. Adding a new block type takes about 10 minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the type to the &lt;code&gt;BlockType&lt;/code&gt; union in &lt;code&gt;lib/types.ts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Define &lt;code&gt;defaultProps&lt;/code&gt; in &lt;code&gt;BLOCK_CATEGORIES&lt;/code&gt; (used by the sidebar)&lt;/li&gt;
&lt;li&gt;Add a canvas preview in &lt;code&gt;components/builder/block-preview.tsx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add config fields in &lt;code&gt;components/builder/config/block-config-fields.tsx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add a live preview render in &lt;code&gt;components/builder/live-preview.tsx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add a Markdown render case in &lt;code&gt;lib/markdown.ts&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This separation of concerns keeps each concern focused and testable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Self-Hosted Stats API
&lt;/h2&gt;

&lt;p&gt;Most README stat widgets depend on external services with rate limits. Instead, the builder ships its own route handlers that generate stat SVGs server-side using your &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /api/stats      → GitHub stats card SVG
GET /api/streak     → Streak stats SVG
GET /api/top-langs  → Top languages SVG
GET /api/activity   → 30-day contribution graph SVG
GET /api/trophies   → Trophy grid SVG
GET /api/quotes     → Random dev quote SVG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All routes hit the GitHub GraphQL API, apply theme colors, and return inline SVGs. A simple in-memory cache (5-minute TTL) prevents hammering the API on every preview refresh:&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;class&lt;/span&gt; &lt;span class="nc"&gt;GitHubCache&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CacheEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&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;key&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;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&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;When you export your README, URLs point to your deployed instance — so the stat images are always live and under your control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drag and Drop with dnd-kit
&lt;/h2&gt;

&lt;p&gt;The canvas uses &lt;code&gt;@dnd-kit/sortable&lt;/code&gt; for block reordering. Blocks are wrapped in a &lt;code&gt;useSortable&lt;/code&gt; hook, giving each one a stable drag handle and smooth CSS transforms during drag:&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;attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNodeRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isDragging&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;useSortable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CSSProperties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CSS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;transition&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;The &lt;code&gt;PointerSensor&lt;/code&gt; has an activation constraint of 8px to prevent accidental drags when clicking to select a block:&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="nf"&gt;useSensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PointerSensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;activationConstraint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&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;
  
  
  Markdown Rendering
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;renderMarkdown&lt;/code&gt; function in &lt;code&gt;lib/markdown.ts&lt;/code&gt; walks the block array and converts each block to its Markdown equivalent. The interesting case is adjacent half-width stats cards — these need to be grouped into a single &lt;code&gt;&amp;lt;div align="center"&amp;gt;&lt;/code&gt; row:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageTag&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isHalfWidthCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&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;nextBlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextImageTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;nextBlock&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isHalfWidthCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;getHalfWidthCardImageTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextBlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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="nx"&gt;nextImageTag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&amp;lt;div align="center"&amp;gt;\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="s2"&gt;`  &amp;lt;img src="..." width="50%" alt="GitHub Stats" /&amp;gt;\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="s2"&gt;`  &amp;lt;img src="..." width="50%" alt="Top Languages" /&amp;gt;\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="s2"&gt;`&amp;lt;/div&amp;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;i&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="c1"&gt;// skip the next block — it's already rendered&lt;/span&gt;
    &lt;span class="k"&gt;continue&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;This produces clean, side-by-side stat cards in the exported Markdown without any wrapper framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Management with Zustand
&lt;/h2&gt;

&lt;p&gt;The builder state lives in a single Zustand store. Only the &lt;code&gt;username&lt;/code&gt; field is persisted to &lt;code&gt;localStorage&lt;/code&gt; — the block canvas resets on refresh by design (to keep things simple and avoid stale state bugs):&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useBuilderStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BuilderState&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;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;get&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="na"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;selectedBlockId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// ...actions&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github-readme-builder-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;partialize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&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="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store actions handle deeply nested operations like &lt;code&gt;removeBlock&lt;/code&gt; and &lt;code&gt;updateBlock&lt;/code&gt;, which recursively walk the &lt;code&gt;children&lt;/code&gt; tree to find and update the target block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;The project uses Jest with ts-jest for unit tests covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lib/markdown.ts&lt;/code&gt; — every block type's Markdown output, including complex scenarios like adjacent half-width cards and stats-row children&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lib/github.ts&lt;/code&gt; — &lt;code&gt;calculateStreakStats&lt;/code&gt;, &lt;code&gt;calculateRank&lt;/code&gt;, &lt;code&gt;fetchUserStats&lt;/code&gt;, &lt;code&gt;fetchLanguageStats&lt;/code&gt;, and more&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lib/store.ts&lt;/code&gt; — all store actions including nested block operations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lib/themes.ts&lt;/code&gt; — theme resolution and alias handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A pre-commit hook runs the full test suite, ESLint, Prettier format check, and TypeScript type-check before every commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Responsive Layout
&lt;/h2&gt;

&lt;p&gt;The builder uses three distinct layouts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Desktop (lg+)&lt;/strong&gt;: fixed left sidebar (block library) + scrollable canvas + fixed right panel (config or preview)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tablet (md–lg)&lt;/strong&gt;: canvas fills the screen, sidebar and config panel open as &lt;code&gt;Sheet&lt;/code&gt; overlays triggered by floating buttons&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile&lt;/strong&gt;: a bottom tab bar switches between Blocks, Canvas, and Preview views; the config panel slides up as a bottom sheet&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few things on the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Undo/redo&lt;/strong&gt; — the block array is already serializable, so it's a natural fit for a history stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom block templates&lt;/strong&gt; — save your own configured blocks for reuse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More block types&lt;/strong&gt; — GitHub language badges, contribution heatmaps, Spotify now-playing, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Theme Builder&lt;/strong&gt; - Allow users to create custom themes beyond the 65+ pre-built options with a visual color picker for stats cards, borders, and backgrounds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live demo&lt;/strong&gt;: &lt;a href="https://github-profile-maker.vercel.app/" rel="noopener noreferrer"&gt;github-profile-maker.vercel.app&lt;/a&gt;&lt;br&gt;&lt;br&gt;
⭐ &lt;strong&gt;Star the repo&lt;/strong&gt;: &lt;a href="https://github.com/zntb/github-profile-maker" rel="noopener noreferrer"&gt;github.com/zntb/github-profile-maker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is &lt;strong&gt;MIT&lt;/strong&gt;-licensed and contributions are welcome. If you add a new block type or template, open a PR — I'd love to include it.&lt;/p&gt;

&lt;p&gt;Questions, feedback, or ideas? Drop them in the comments. 👇&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>react</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Adding 2FA to OAuth Logins in Next.js 16 with Better Auth</title>
      <dc:creator>Zntb</dc:creator>
      <pubDate>Tue, 27 Jan 2026 07:17:59 +0000</pubDate>
      <link>https://dev.to/zntb/adding-2fa-to-oauth-logins-in-nextjs-16-with-better-auth-2eep</link>
      <guid>https://dev.to/zntb/adding-2fa-to-oauth-logins-in-nextjs-16-with-better-auth-2eep</guid>
      <description>&lt;p&gt;OAuth providers like Google and GitHub are excellent for reducing friction during onboarding. They handle the identity verification so you don't have to. The challenge arises when you want to add an extra layer of security, like Two-Factor Authentication (2FA), to these accounts.&lt;/p&gt;

&lt;p&gt;Most standard 2FA implementations rely on a password confirmation step to enable or disable settings. OAuth users don't have a local password. This creates a gap in the security flow.&lt;/p&gt;

&lt;p&gt;I recently implemented a solution using &lt;code&gt;better-auth&lt;/code&gt; in a Next.js 16 application that bridges this gap. It allows OAuth users to enable TOTP (Time-based One-Time Password) and manage trusted devices without needing a password fallback.&lt;/p&gt;

&lt;p&gt;Here is how I structured the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Concept
&lt;/h2&gt;

&lt;p&gt;We need to treat OAuth 2FA differently from standard email/password 2FA. Since we cannot validate a password, we validate the active OAuth session. The flow relies on three main parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A custom API route to handle TOTP logic.&lt;/li&gt;
&lt;li&gt;A modified OAuth callback to intercept logins.&lt;/li&gt;
&lt;li&gt;Client components to handle the verification UX.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Backend Logic
&lt;/h2&gt;

&lt;p&gt;The heavy lifting happens in a custom route handler. I created &lt;code&gt;src/app/api/auth/oauth-two-factor/route.ts&lt;/code&gt;. This endpoint acts as a dispatcher for 2FA actions specifically for social login users.&lt;/p&gt;

&lt;p&gt;It handles enabling 2FA, verifying the code, and generating backup codes.&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="c1"&gt;// src/app/api/auth/oauth-two-factor/route.ts&lt;/span&gt;

&lt;span class="c1"&gt;// Enabling 2FA&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;enableTwoFactorForOAuthUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Verifying a code&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValid&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;verifyTotpForOAuthUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Generating backup codes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backupCodes&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;generateBackupCodesForOAuthUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These functions, located in &lt;code&gt;src/lib/two-factor.ts&lt;/code&gt;, interact directly with the database. They store secrets in an encrypted format and ensure backup codes are generated securely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intercepting the Login
&lt;/h2&gt;

&lt;p&gt;When a user logs in via Google, &lt;code&gt;better-auth&lt;/code&gt; handles the callback. We need to hook into this process. I modified &lt;code&gt;src/app/api/auth/oauth-callback/route.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We check the user's profile immediately after authentication. If &lt;code&gt;twoFactorEnabled&lt;/code&gt; is true, we pause. We check if the current device is "trusted" via a cookie.&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="c1"&gt;// src/app/api/auth/oauth-callback/route.ts&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;twoFactorEnabled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;trustedDeviceEnabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// If trusted, set the verification cookie and let them in&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;two-factor-verified&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;true&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If not trusted or no cookie, redirect to verification page&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This logic ensures that a user with 2FA enabled cannot bypass the second step unless they have previously verified the specific device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verification UI
&lt;/h2&gt;

&lt;p&gt;The user gets redirected to &lt;code&gt;src/app/auth/verify-2fa/page.tsx&lt;/code&gt; if a check is required. This page is a server component that validates the session exists before loading the client form.&lt;/p&gt;

&lt;p&gt;The client component, &lt;code&gt;verify-2fa-client.tsx&lt;/code&gt;, handles the user input. It sends the TOTP code to our custom API route.&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="c1"&gt;// src/components/two-factor-verify-form.tsx&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/oauth-two-factor&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;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;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;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;verificationCode&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;If the API returns success, we set a &lt;code&gt;two-factor-verified&lt;/code&gt; cookie. This cookie acts as the "proof" that the second factor was passed. The user is then redirected to their dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling 2FA on the Profile
&lt;/h2&gt;

&lt;p&gt;We also need a way for users to turn this feature on. Inside the user profile, I added a component that generates a QR code.&lt;/p&gt;

&lt;p&gt;Since we don't ask for a password, we rely on the active session. When the user clicks "Enable", we hit the API.&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="c1"&gt;// src/components/two-factor-enable-form.tsx&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/oauth-two-factor&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;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;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;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Next.js Better Auth&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server returns a secret and a QR code URL. The user scans it, enters a code to confirm, and the database updates to reflect that 2FA is now active.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trusted Devices
&lt;/h2&gt;

&lt;p&gt;Repeatedly entering codes is annoying. To solve this, we implemented a toggle for "Trusted Devices".&lt;/p&gt;

&lt;p&gt;When verification is successful, the user can choose to trust the device. This sets a long-lived cookie (usually 30 days). The OAuth callback looks for this cookie on subsequent logins. If present and valid, it bypasses the manual code entry.&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="c1"&gt;// src/components/trusted-device-toggle.tsx&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/oauth-two-factor&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;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;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;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toggleTrustedDevice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newValue&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;
  
  
  Security Considerations
&lt;/h2&gt;

&lt;p&gt;This flow maintains high security standards. All API routes require an active session. You cannot hit the 2FA endpoints without being logged in first.&lt;/p&gt;

&lt;p&gt;Secrets are stored encrypted. Backup codes are single-use; they are regenerated whenever a user requests new ones. The cookies used for trusted devices are HTTP-only and Secure, preventing client-side script access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;By decoupling the 2FA logic from password validation, we allow OAuth users to enjoy the same security benefits as email/password users. The combination of custom route handlers and session checks in Next.js 16 provides a robust solution.&lt;/p&gt;

&lt;h1&gt;
  
  
  nextjs #authentication #typescript #websecurity
&lt;/h1&gt;

</description>
      <category>javascript</category>
      <category>nextjs</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Developer Toolkit Pro</title>
      <dc:creator>Zntb</dc:creator>
      <pubDate>Tue, 06 Jan 2026 12:34:52 +0000</pubDate>
      <link>https://dev.to/zntb/developer-toolkit-pro-5db2</link>
      <guid>https://dev.to/zntb/developer-toolkit-pro-5db2</guid>
      <description>&lt;h2&gt;
  
  
  🚀 Introducing Developer Toolkit Pro: A Comprehensive Suite of Developer Utilities
&lt;/h2&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4925jxtbb9alq8rbw2f.webp" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4925jxtbb9alq8rbw2f.webp" alt="Screenshot" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer-toolkit-pro.vercel.app/" rel="noopener noreferrer"&gt;Website&lt;/a&gt; &lt;br&gt;
&lt;a href="https://x.com/codetibo" rel="noopener noreferrer"&gt;X&lt;/a&gt; &lt;br&gt;
&lt;a href="//codetibo@proton.me"&gt;Email&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hey dev community! 👋&lt;/p&gt;

&lt;p&gt;I've been working on a project that I think many of you will find useful: &lt;strong&gt;Next.js Dev Tools&lt;/strong&gt; – a collection of 50+ developer utilities built with Next.js 16, React 19, and Tailwind CSS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Inside?
&lt;/h2&gt;

&lt;p&gt;The platform includes tools for virtually every development workflow:&lt;/p&gt;

&lt;h3&gt;
  
  
  🔧 Code &amp;amp; Development Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Client&lt;/strong&gt; – Test and debug APIs with a clean interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Playground&lt;/strong&gt; – Write and execute code snippets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Diff Checker&lt;/strong&gt; – Compare code changes visually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regex Tester&lt;/strong&gt; – Test and debug regular expressions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON Formatter/Validator&lt;/strong&gt; – Format, validate, and prettify JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Formatter&lt;/strong&gt; – Beautify your SQL queries&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎨 CSS &amp;amp; Design Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSS Animations Generator&lt;/strong&gt; – Create smooth animations with preview&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Box Shadow Generator&lt;/strong&gt; – Generate perfect box shadows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neumorphism Generator&lt;/strong&gt; – Create neumorphic UI elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexbox Generator&lt;/strong&gt; – Visual flexbox layout builder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grid Generator&lt;/strong&gt; – CSS Grid layout generator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradient Generator&lt;/strong&gt; – Beautiful gradient creator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color Palette&lt;/strong&gt; – Generate and manage color schemes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clip Path Generator&lt;/strong&gt; – Create complex CSS clip-paths&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS to Tailwind Converter&lt;/strong&gt; – Convert CSS to Tailwind classes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔐 Security &amp;amp; Encoding Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hash Generator&lt;/strong&gt; – Generate MD5, SHA-1, SHA-256, SHA-512 hashes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base64 Encoder/Decoder&lt;/strong&gt; – Encode and decode Base64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL Encoder&lt;/strong&gt; – URL-safe encoding/decoding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT Decoder&lt;/strong&gt; – Decode and analyze JWT tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BCRYPT Generator&lt;/strong&gt; – Generate secure password hashes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔢 Conversion &amp;amp; Calculation Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Number Base Converter&lt;/strong&gt; – Convert between binary, decimal, hex, octal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PX to REM Converter&lt;/strong&gt; – Convert pixel values to REM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV to JSON Converter&lt;/strong&gt; – Convert CSV data to JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON to TypeScript&lt;/strong&gt; – Generate TypeScript interfaces from JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YAML to JSON Converter&lt;/strong&gt; – Convert YAML to JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unix Timestamp Converter&lt;/strong&gt; – Convert timestamps&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🖼️ Image Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image Resizer&lt;/strong&gt; – Resize images easily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Format Converter&lt;/strong&gt; – Convert between formats&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG to JSX&lt;/strong&gt; – Convert SVG to React JSX&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG Optimizer&lt;/strong&gt; – Optimize SVG files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Favicon Generator&lt;/strong&gt; – Generate favicons for all platforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QR Generator&lt;/strong&gt; – Create QR codes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Placeholder Generator&lt;/strong&gt; – Generate placeholder images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSX to Image&lt;/strong&gt; – Convert JSX to images&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📊 Analysis &amp;amp; Debugging Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SEO Audit&lt;/strong&gt; – Analyze pages for SEO issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Vitals Calculator&lt;/strong&gt; – Calculate Core Web Vitals scores&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tech Stack Detector&lt;/strong&gt; – Detect technologies used by websites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Status Lookup&lt;/strong&gt; – Quick reference for HTTP status codes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WCAG Contrast Checker&lt;/strong&gt; – Check color contrast for accessibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hex Compare&lt;/strong&gt; – Compare hex colors&lt;/li&gt;
&lt;/ul&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fargd8sgbguhatww00nob.webp" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fargd8sgbguhatww00nob.webp" alt="Og Image Maker" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🔄 Version Control &amp;amp; DevOps Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Git Command Generator&lt;/strong&gt; – Generate Git commands from UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose Builder&lt;/strong&gt; – Visual Docker Compose editor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repo Analyzer&lt;/strong&gt; – Analyze GitHub repositories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron Calculator&lt;/strong&gt; – Understand and test cron expressions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📝 Content &amp;amp; Documentation Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markdown Editor&lt;/strong&gt; – Full-featured markdown editor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mermaid Diagram Generator&lt;/strong&gt; – Create flowcharts and diagrams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;README Generator&lt;/strong&gt; – Generate professional README files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lorem Ipsum Generator&lt;/strong&gt; – Generate placeholder text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Generator&lt;/strong&gt; – Generate mock data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌐 GraphQL &amp;amp; API Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GraphQL Generator&lt;/strong&gt; – Generate GraphQL schemas and queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket Tester&lt;/strong&gt; – Test WebSocket connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case Converter&lt;/strong&gt; – Convert text case (camel, snake, kebab, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  💾 Storage &amp;amp; Data Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage Inspector&lt;/strong&gt; – Inspect browser storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text Difference&lt;/strong&gt; – Compare text differences&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework:&lt;/strong&gt; Next.js 15 (App Router)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI Library:&lt;/strong&gt; React 19&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling:&lt;/strong&gt; Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Management:&lt;/strong&gt; Zustand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Editor:&lt;/strong&gt; Monaco Editor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Charts:&lt;/strong&gt; Recharts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme:&lt;/strong&gt; Dark/Light mode support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Components:&lt;/strong&gt; Radix UI primitives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animations:&lt;/strong&gt; Framer Motion&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Out! 🧪
&lt;/h2&gt;

&lt;p&gt;I'm launching this project and would love your help to make it better!&lt;/p&gt;

&lt;h3&gt;
  
  
  How You Can Help:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test the tools&lt;/strong&gt; – Try out any of the 50+ utilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report bugs&lt;/strong&gt; – Found something broken? Let me know!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suggest features&lt;/strong&gt; – What tools would you like to see?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share feedback&lt;/strong&gt; – UI/UX improvements, performance tips&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Access the Application
&lt;/h2&gt;

&lt;p&gt;🌐 &lt;strong&gt;&lt;a href="https://developer-toolkit-pro.vercel.app/" rel="noopener noreferrer"&gt;Next.js Dev Tools&lt;/a&gt;&lt;/strong&gt; – Start exploring now!&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This is just the beginning. I have plans to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add more tools based on community feedback&lt;/li&gt;
&lt;li&gt;Improve performance and accessibility&lt;/li&gt;
&lt;li&gt;Add keyboard shortcuts for power users&lt;/li&gt;
&lt;li&gt;Implement tool favorites and history&lt;/li&gt;
&lt;li&gt;Create tool collections/workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Let's Connect!
&lt;/h2&gt;

&lt;p&gt;Have ideas for new tools or found a bug? Drop a comment below or reach out!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#WebDev #NextJS #DeveloperTools #OpenSource #React&lt;/strong&gt;&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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0au0jl2uin92z7pli2ir.webp" 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%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0au0jl2uin92z7pli2ir.webp" alt="Image2" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: This project is currently in active development. Your feedback and suggestions are invaluable in shaping its future!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
