<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Jakub Mazur's Blog</title>
        <link>https://jakmaz.com</link>
        <description>Thoughts on coding, technology, and personal growth</description>
        <lastBuildDate>Thu, 30 Apr 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Feed for Node.js</generator>
        <language>en</language>
        <copyright>All rights reserved 2026, Jakub Mazur</copyright>
        <item>
            <title><![CDATA[The Personal Backend I Wish I Had Sooner]]></title>
            <link>https://jakmaz.com/blog/personal-backend</link>
            <guid isPermaLink="false">https://jakmaz.com/blog/personal-backend</guid>
            <pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How I built a personal life dashboard that owns my data]]></description>
            <content:encoded><![CDATA[<p>I've always been obsessed with tracking things in my life - books, movies, habits, finances, workouts. But for every new thing that I wanted to track, or every new tool that I needed, I had to download another app or service. Some of them I even had to pay for.</p>
<p>My data was scattered across apps, scripts, spreadsheets and notes. None of it was connected in any way. And most of it wasn't even owned by me, I couldn't export my habits data and query it myself.</p>
<h2>Atlas - What I Built</h2>
<p>I started small, just a simple habit tracker to see whether I did my flashcards and guitar practice.</p>
<p>Then one day I went for a run, got annoyed at Strava (again), and thought: why not just add runs to my own dashboard?</p>
<p>And that's how it went. Runs turned into workouts, books, movies, games, subscriptions. Everything lives in one system - one database, one API, multiple ways to interact with it.</p>
<p><img src="/images/blog/home-example.png" alt="home-example.png"></p>
<h2>What It Can Do</h2>
<p>Right now, my system lets me:</p>
<ul>
<li>Track finances, subscriptions, and portfolio value</li>
<li>Manage books, movies, and games with ratings and history</li>
<li>Analyze runs and workouts</li>
<li>Aggregate YouTube videos and articles</li>
<li>Learn with flashcards</li>
<li>Run small custom tools like timers</li>
</ul>
<p>Everything is queryable and connected.</p>
<p><img src="/images/blog/news-flashcards-example.png" alt="news-flashcards-example.png"></p>
<h2>Alfred - My True Assistant</h2>
<p>Once everything lives in one place, a different interface becomes possible.</p>
<p>I built a simple agent that can query my database and call API endpoints.</p>
<p>Instead of navigating UIs, I just ask:</p>
<ul>
<li>"What books did I read this year?"</li>
<li>"What are my highest-rated movies?"</li>
<li>"Add a flashcard for <em>Informatieverwerkingssystemenbeheer</em> (I love Dutch)"</li>
</ul>
<p><img src="/images/blog/alfred-example.png" alt="alfred-example.png"></p>
<p>This only works because the data is mine, structured, and accessible. It turns an LLM into a real assistant, just like Alfred from Batman.</p>
<h2>Why Bother, It Seems Like a Lot of Work</h2>
<p>You may be thinking: this must have taken a huge amount of time to build.</p>
<p>It didn't. Most of the functionality came together over a single weekend. AI makes this kind of thing actually feasible. If I had to carefully design and implement every part of this system by hand, I probably wouldn't even start.</p>
<p>But today I can spin up <a href="https://opencode.ai">OpenCode</a>, and in a matter of minutes end up with tools I'll use for years.</p>
<p>One day I decided I didn't want to use Anki anymore. Within an hour, I had all my data migrated into a system I fully control. It cost me about $1 in inference (thanks, QWEN) and now I can extend it however I want.</p>
<p>Under the hood, the stack is very simple:</p>
<ul>
<li><a href="https://sqlite.org/">SQLite</a> database</li>
<li><a href="https://bun.sh/">Bun</a> + <a href="https://hono.dev/">Hono</a> backend</li>
<li><a href="https://react.dev/">React</a> frontend, CLI, <a href="https://www.raycast.com/">Raycast</a></li>
<li><a href="https://sdk.vercel.ai/docs">Vercel's AI SDK</a> for AI integration</li>
<li>Hosted on a <a href="https://www.raspberrypi.com/">Raspberry Pi</a> through <a href="https://tailscale.com/">Tailscale</a></li>
</ul>
<p>That's another advantage. Because it's personal, you can keep it simple - you don't need insane kubernetes architecture or analytics.</p>
<h2>Join Me</h2>
<p>I strongly believe that personal software should be personal. A one-size-fits-all SaaS will never fit you as well as something you control. And the barrier to building it is lower than it's ever been.</p>
<p>Since this project is meant to be adjusted perfectly to you, I'm not sharing a codebase. Instead, I created a starting prompt that captures the idea and architecture.</p>
<p>Feel free to paste it into any coding agent you like — Lovable, v0, Claude Code, or any other. The output should give you a solid starting point for iterating.</p>
<p><a href="https://gist.github.com/jakmaz/d4bf57edd4b9faba5ee938ae1e8a36b5">Personal Backend Prompt - GitHub Gist</a></p>
<p>Build something that actually fits your life. And if you want to share it, I'd love to see it.</p>]]></content:encoded>
            <author>contact@jakmaz.com (Jakub Mazur)</author>
            <category>personal</category>
            <category>productivity</category>
            <category>automation</category>
            <enclosure url="https://jakmaz.com/og/personal-backend.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Why You Can’t Finish Your Projects]]></title>
            <link>https://jakmaz.com/blog/unfinished-projects</link>
            <guid isPermaLink="false">https://jakmaz.com/blog/unfinished-projects</guid>
            <pubDate>Mon, 20 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[A personal take on the joy of beginnings and the struggle of endings]]></description>
            <content:encoded><![CDATA[<p>I have always been coding, working on new things, watching my contributor graph fill out over the year. I knew I was doing stuff, but when it came to making my resume, filling out the projects section turned out to be harder than I expected. I had over 50 repositories by this time, and an even longer list of ideas, but none reached the finish line. I wasn't proud of a single project. Not a single one was truly finished.</p>
<h2>The Joy of Starting Something New</h2>
<p>Starting a new project is always fun. You have an amazing idea (one that you still truly believe is amazing), you create some initial code, maybe a website, maybe just a README, and push it to GitHub. Then you spend a day or two adding features. But over time, it becomes less exciting. You face your first barriers, you realize how much work is required. And then a new idea comes along! You abandon the previous project entirely and are already pushing a new repo to GitHub, creating an infinite loop. Or, after a while, you just get bored by the amount of work needed to really polish a project and leave it unfinished.</p>
<h2>The Brain Behind the Loop</h2>
<p>There’s a reason starting projects feels amazing while finishing them feels… like a chore.</p>
<h3>Dopamine Hits from Novelty</h3>
<p>Our brains love dopamine, and novelty is one of its strong triggers. Each new idea or repo gives a small rush of satisfaction. We feel the immediate hit of accomplishment, even from the smallest things. That’s why first commits, first UI, or first working features feel so magical. But the excitement fades as the work becomes repetitive and requires persistence.</p>
<h3>Perfectionism and Overwhelm</h3>
<p>Completing a project often means polishing it, fixing all the edge cases, and writing documentation - the type of work that is boring and not immediately rewarding. Who will really notice that your app doesn’t break in some insanely rare edge case? Our brains prefer the creative spark over the grind, so perfectionism can make finishing feel impossible.</p>
<h3>The Illusion of Progress</h3>
<p>Even though your contributor graph shows you’re active, there’s a huge difference between activity and completion. This can lead to guilt or dissatisfaction because you did a lot, but there’s nothing tangible to show for it. That’s exactly how I felt, and it was the moment I decided to take actionable steps.</p>
<h2>Slowing Down</h2>
<p>I knew I had to focus on the most important things and push them to the very end, so I could feel a real sense of accomplishment, not just a momentary one that later leads to dissatisfaction. I decided to set strict rules for myself:</p>
<h3>Only two ongoing projects</h3>
<p>From now on, I will only work on <strong>two projects at a time</strong>. I find this number is a sweet spot: it gives flexibility while still limiting distraction. I will try my best to finish them. I’ve already caught myself thinking about new ideas, but instead of starting them and falling back into the loop, I add them to my development ideas list. Describing the idea in the note gives me a small dopamine hit, and then I return to my current work.</p>
<h3>Archiving projects</h3>
<p>Starting a new project doesn’t bind me to finishing it. Some projects turn out not to be worth it, they can require huge amounts of work with little return. That’s why I allow myself to abandon projects. But here’s the thing: abandoning now means <strong>never returning to them</strong>. Previously, I would leave projects unfinished, still thinking I might get back to them. Now, leaving a project means completely archiving the repository and letting it go. The power of archiving isn’t just practical, it frees mental space and stops the guilt loop.</p>
<h2>Current Focus</h2>
<p>Following these rules, I am now focusing on two projects. If you read this blog in the distant future, I hope you click on them and see polished, complete work:</p>
<ul>
<li><a href="https://github.com/jakmaz/arcade">arcade</a> - classic games in your terminal</li>
<li><a href="https://github.com/jakmaz/mini-systems/tree/main/mini-lang">mini-lang</a> - simple interpreter written in go</li>
</ul>
<h3>Join me!</h3>
<p>If you’ve ever felt the same frustration, that sense of lack of accomplishment, consider this your sign. And remember: done doesn’t have to mean perfect - just 90% is enough to feel proud.</p>]]></content:encoded>
            <author>contact@jakmaz.com (Jakub Mazur)</author>
            <category>personal</category>
            <category>productivity</category>
            <enclosure url="https://jakmaz.com/og/unfinished-projects.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Why I Blog, and Why You Should Too]]></title>
            <link>https://jakmaz.com/blog/blogging</link>
            <guid isPermaLink="false">https://jakmaz.com/blog/blogging</guid>
            <pubDate>Thu, 25 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[How blogging helps me think and grow, and why it can do the same for you]]></description>
            <content:encoded><![CDATA[<p>I've written a couple of blog posts here already, and with each one, I notice more and more advantages of having a blog. In this post, I'll share some of them, and hopefully convince you to start your own.</p>
<h2>You learn more about the topics</h2>
<p>A lot of the time, I write a blog post not to teach others, but to learn about the topic myself. When I want to understand something better, writing a post forces me to slow down, break things apart, and go deeper into the details.</p>
<p>If I just read about the topic I might skim it and move on. But when I know I'm going to explain it to others, I have to really understand what is it about. And another bonus: once you write about something, <strong>you remember it better</strong>, it sticks. Trying to explain an idea clearly to someone else, forces you to organize your thoughts, and that makes learning way more effective.</p>
<h2>You learn how to express yourself</h2>
<p>I’m not a native English speaker, so for me this one is huge. Writing blog posts has been an amazing way to practice putting my thoughts together in a way that’s clear and (hopefully) fun to read. Even if you are a native speaker, though, writing helps you sharpen that skill.</p>
<p><strong>It's one thing to know something in your head, but it's another to explain it so someone else can understand it without confusion.</strong> That’s a skill a lot of people (especially software engineers) overlook, but being able to communicate your ideas well can set you apart.</p>
<h2>You grow your presence and reputation</h2>
<p>A great side effect is that putting your writing out there builds a presence. I like the idea of being part of a community, and blogging is one of the easiest ways to add to it. Your words stick around, and they can reach way more people than you’d expect.</p>
<p>In my opinion, creating new stuff <strong>feels really rewarding</strong>. There’s the excitement of knowing someone might stumble across your post through a Google search or recommendation. It’s a great feeling to know you’ve helped someone or taught them something new.</p>
<p><strong>It can also even help professionally.</strong> Think about it: if you’re a recruiter choosing between two candidates with the same technical skills, and one of them has a blog full of engaging posts, wouldn’t you lean toward the one who clearly loves what they do? It shows passion, consistency, and a willingness to share knowledge.</p>
<p>That said, don’t fall into the “content trap”. It’s tempting to think more is better, but blasting out AI-generated slop won’t do you any favors. <strong>Authenticity matters</strong>. It’s better to publish fewer posts that genuinely reflect your thoughts and experiences.</p>
<h2>So, how to start?</h2>
<p>If you’re a developer, I’d definitely recommend building your own blog. Otherwise there are countless tools that make it ridiculously easy to get started: WordPress, Ghost, Medium, Substack, heck, even LinkedIn posts can count as blogging.</p>
<p><strong>Keep a running list of blog ideas</strong>, topics you want to learn about or knowledge you want to share. Whenever an idea comes, just add it to the note. And whenever you find some free time… just write!</p>
<p>It doesn’t need to be perfect, polished, or even particularly good at first. You’ll learn, you’ll practice, and you’ll slowly get better without even noticing.</p>
<p>And a year from now, you might scroll back to your very first post, cringe a little, and then smile at how far you’ve come. That’s the journey, and blogging lets you see it in real time.</p>]]></content:encoded>
            <author>contact@jakmaz.com (Jakub Mazur)</author>
            <category>personal</category>
            <category>productivity</category>
            <enclosure url="https://jakmaz.com/og/blogging.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Why I love Neovim]]></title>
            <link>https://jakmaz.com/blog/neovim-love</link>
            <guid isPermaLink="false">https://jakmaz.com/blog/neovim-love</guid>
            <pubDate>Wed, 27 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Why I choose ancient vim magic over modern editors]]></description>
            <content:encoded><![CDATA[<p>About a year into my Neovim journey, and I'm still reaching for it every morning. Not because I'm stubborn (well, maybe a little), but because it genuinely makes me enjoy coding more. Sure, there's VS Code with its slick UI and Cursor with its AI magic, but here's the thing - all of those features can be replicated in Neovim, and then customized exactly how you want them.</p>
<p>Most people see two massive hurdles: learning vim motions and setting everything up from scratch. Fair enough - they're real barriers. But once you get past them, they become the very reasons you'll never want to leave.</p>
<p><img src="/images/blog/pasted-image-20251123141529.png" alt="pasted-image-20251123141529.png"></p>
<h2>Moving like you think</h2>
<p>Remember when you first tried vim and thought "who the hell designed this?" HJKL for navigation, no mouse, everything feels backwards. I definitely had that moment. Spent weeks feeling like I was coding with oven mitts on.</p>
<p>But then something clicked. The editing commands started making sense - <code>d</code> for delete, <code>c</code> for change, <code>y</code> for yank. Your fingers start remembering patterns. <code>ci"</code> to change inside quotes, <code>dap</code> to delete around a paragraph. It stops being about memorizing commands and becomes about expressing what you want to do.</p>
<p>The speed thing everyone talks about? Honestly, that's not the point. It's more about staying in flow. When you can edit text as fast as you think about it, you don't get pulled out of your train of thought by clunky interactions.</p>
<h2>Building your own lightsaber</h2>
<p>Here's where Neovim gets interesting - it ships as basically a text editor with potential. No fancy autocomplete, no git integration, no file tree. Just raw editing power waiting for you to shape it.</p>
<p>This terrifies beginners and delights tinkerers. I fall firmly in the second camp.</p>
<p>With tools like lazy.nvim and the Lua ecosystem, setting up Neovim has become surprisingly pleasant. You're not fighting arcane config files anymore - you're building something that fits exactly how you work.</p>
<p>Want that sleek VS Code file explorer? There's nvim-tree. Need AI autocompletion like Cursor? Supermaven or Copilot work beautifully. Fancy UI elements? You've got telescope, lualine, and a whole ecosystem of plugins that can make Neovim look however you want.</p>
<p>My setup knows that when I hit <code>&#x3C;space>ff</code>, I want to find files. <code>&#x3C;space>e</code> toggles my file explorer. <code>gd</code> jumps to definitions. These aren't arbitrary mappings - they're extensions of how I think about navigating code.</p>
<p>And honestly? I'm constantly tweaking it. New plugin catches my eye? I'm trying it out. Found a better way to organize my keymaps? I'm refactoring the whole thing. Some people set up their config once and forget about it, but for me, tinkering with Neovim is half the fun. It's like having a hobby that directly improves your work.</p>
<h2>The terminal is home</h2>
<p>There's something satisfying about doing everything in the terminal. While others are alt-tabbing between their editor, terminal, and browser, I'm living in tmux sessions where everything flows together.</p>
<p>My Neovim sits alongside running servers, git operations, and build processes. No context switching, no separate applications fighting for screen real estate. Just one cohesive workspace where the keyboard rules everything.</p>
<p>This setup has made me more comfortable with command-line tools in general. When your editor is terminal-native, you naturally start reaching for grep, sed, and all the other Unix tools that make development more powerful.</p>
<p>This workflow feels lightweight and efficient to me. No Electron apps eating RAM, no vendor lock-in, just tools that do exactly what they say they do.</p>
<p>Neovim still surprises me. Not with flashy features, but with how well it gets out of my way and lets me focus on what matters - writing good code.</p>]]></content:encoded>
            <author>contact@jakmaz.com (Jakub Mazur)</author>
            <category>neovim</category>
            <enclosure url="https://jakmaz.com/og/neovim-love.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Adding Views and Likes to Blog Posts in Next.js Without Breaking Static Generation]]></title>
            <link>https://jakmaz.com/blog/static-engagement</link>
            <guid isPermaLink="false">https://jakmaz.com/blog/static-engagement</guid>
            <pubDate>Thu, 07 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[A practical guide to implementing engagement features while keeping your blog fast and statically generated]]></description>
            <content:encoded><![CDATA[<p>I love static sites. They're fast, SEO-friendly, and perfect for blogs. But then I wanted to add view counts and like buttons to my posts, and suddenly things got complicated.</p>
<h2>The Problem</h2>
<p>Adding likes and views to a static blog creates this weird tension. Static sites are pre-rendered at build time and cached globally, which makes them lightning fast. But engagement features are dynamic, user-specific, and need real-time updates.</p>
<p>The naive approach? Make everything dynamic. But there's a better way.</p>
<h2>Why Not Suspense?</h2>
<p>Suspense looks tempting because it seems to give you the best of both worlds:</p>
<pre><code class="hljs language-tsx"><span class="hljs-comment">// This seems like a good idea...</span>
&#x3C;<span class="hljs-title class_">Suspense</span> fallback={<span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span>></span>Loading views...<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span></span>}>
  <span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">ViewCount</span> <span class="hljs-attr">slug</span>=<span class="hljs-string">{post.slug}</span> /></span></span>
&#x3C;/<span class="hljs-title class_">Suspense</span>>
</code></pre>
<p>At first glance, this looks perfect. The page loads fast, shows a loading state, then hydrates the engagement data. It feels like you're getting instant page loads with progressive enhancement.</p>
<p>The problem is that Suspense breaks static generation. When Next.js sees Suspense boundaries, it assumes the page needs server-side rendering because it has dynamic content. This means your pages can't be pre-rendered at build time anymore.</p>
<p>Instead of having pre-built pages that can be prefetched and cached in your browser, every page navigation now needs to hit a server. Even if that server is fast, it's still slower than having the page already loaded in your browser. Plus, you lose the amazing prefetching benefits that make static sites so good.</p>
<h2>The Magic of Static Sites + Prefetching</h2>
<p>Here's what makes static sites feel so fast: Next.js automatically prefetches all the links on your page when they come into view. So when someone's reading your blog post and they see a link to another post in your navigation or footer, Next.js quietly downloads that page in the background.</p>
<p>When they actually click the link, the page appears instantly because it's already loaded. This is what makes well-built Next.js sites feel like native mobile apps, instant navigation between pages.</p>
<p>If you break static generation with Suspense, you lose this prefetching magic. Now every page navigation needs to hit a server, which kills that snappy feeling.</p>
<h2>The Better Approach</h2>
<p>Instead, I render a static shell and fetch engagement data on the client side:</p>
<pre><code class="hljs language-tsx"><span class="hljs-comment">// This keeps your page static</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">BlogPost</span>(<span class="hljs-params">{ post }: { post: BlogPost }</span>) {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">article</span>></span>
      <span class="hljs-tag">&#x3C;<span class="hljs-name">h1</span>></span>{post.title}<span class="hljs-tag">&#x3C;/<span class="hljs-name">h1</span>></span>
      <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex gap-4"</span>></span>
        <span class="hljs-tag">&#x3C;<span class="hljs-name">EngagementStats</span> <span class="hljs-attr">slug</span>=<span class="hljs-string">{post.slug}</span> /></span>
      <span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
      <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"markdown"</span>></span>{post.content}<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
    <span class="hljs-tag">&#x3C;/<span class="hljs-name">article</span>></span></span>
  );
}
</code></pre>
<p>The <code>EngagementStats</code> component fetches both views and likes after the page loads. This way, the page itself is completely static and gets all the prefetching benefits, but the engagement features are still dynamic.</p>
<h2>Choosing the Right Database</h2>
<p>For storing likes and views, I use Redis (specifically Upstash Redis). You might wonder why not just use a regular database like PostgreSQL.</p>
<p>Redis is perfect for this type of simple data because it's an in-memory database. That means it stores everything in RAM instead of on disk, which makes it incredibly fast. When someone likes a post, I can increment the counter and check if they've already liked it in just a few milliseconds.</p>
<p>For engagement features where you're doing lots of simple reads and writes (getting view counts, incrementing likes, checking if someone already liked something), Redis is way overkill in the best way. It's like using a sports car for grocery shopping, but hey, groceries get done really fast.</p>
<h2>Building the View Counter</h2>
<p>First, I created API routes for views and likes:</p>
<pre><code class="hljs language-tsx"><span class="hljs-comment">// app/api/views/[slug]/route.ts</span>
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">NextRequest</span>, <span class="hljs-title class_">NextResponse</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { getViewCount, recordViewCount } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/actions/viewCount"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">GET</span>(<span class="hljs-params">
  <span class="hljs-attr">request</span>: <span class="hljs-title class_">NextRequest</span>,
  { params }: { params: <span class="hljs-built_in">Promise</span>&#x3C;{ slug: <span class="hljs-built_in">string</span> }> },
</span>) {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { slug } = <span class="hljs-keyword">await</span> params;

    <span class="hljs-comment">// Get current views and record this view in parallel</span>
    <span class="hljs-keyword">const</span> [currentViews] = <span class="hljs-keyword">await</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">all</span>([
      <span class="hljs-title function_">getViewCount</span>(slug),
      <span class="hljs-title function_">recordViewCount</span>(slug), <span class="hljs-comment">// Auto-record the view on GET</span>
    ]);

    <span class="hljs-keyword">return</span> <span class="hljs-title class_">NextResponse</span>.<span class="hljs-title function_">json</span>({
      <span class="hljs-attr">views</span>: currentViews.<span class="hljs-property">views</span> + <span class="hljs-number">1</span>, <span class="hljs-comment">// Return incremented count</span>
    });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"Error fetching/recording views:"</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-title class_">NextResponse</span>.<span class="hljs-title function_">json</span>(
      { <span class="hljs-attr">error</span>: <span class="hljs-string">"Failed to fetch views"</span> },
      { <span class="hljs-attr">status</span>: <span class="hljs-number">500</span> },
    );
  }
}
</code></pre>
<p>Then the client component that handles both views and likes:</p>
<pre><code class="hljs language-tsx"><span class="hljs-comment">// components/blog/engagement-stats.tsx</span>
<span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> useSWR <span class="hljs-keyword">from</span> <span class="hljs-string">"swr"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">ShowViews</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"./show-views"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">LikeButton</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"./like-button"</span>;

<span class="hljs-keyword">interface</span> <span class="hljs-title class_">EngagementStatsProps</span> {
  <span class="hljs-attr">slug</span>: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">EngagementStats</span>(<span class="hljs-params">{ slug }: <span class="hljs-title class_">EngagementStatsProps</span></span>) {
  <span class="hljs-keyword">const</span> {
    <span class="hljs-attr">data</span>: likes,
    <span class="hljs-attr">error</span>: likesError,
    <span class="hljs-attr">isLoading</span>: likesLoading,
  } = <span class="hljs-title function_">useSWR</span>(<span class="hljs-string">`/api/likes/<span class="hljs-subst">${slug}</span>`</span>);
  <span class="hljs-keyword">const</span> {
    <span class="hljs-attr">data</span>: views,
    <span class="hljs-attr">error</span>: viewsError,
    <span class="hljs-attr">isLoading</span>: viewsLoading,
  } = <span class="hljs-title function_">useSWR</span>(<span class="hljs-string">`/api/views/<span class="hljs-subst">${slug}</span>`</span>);

  <span class="hljs-keyword">const</span> isLoading = likesLoading || viewsLoading;

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span>
      <span class="hljs-attr">className</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">flex</span> <span class="hljs-attr">items-center</span> <span class="hljs-attr">gap-2</span> <span class="hljs-attr">transition-all</span> <span class="hljs-attr">duration-500</span> ${
        <span class="hljs-attr">isLoading</span> ? "<span class="hljs-attr">translate-y-1</span> <span class="hljs-attr">opacity-0</span>" <span class="hljs-attr">:</span> "<span class="hljs-attr">translate-y-0</span> <span class="hljs-attr">opacity-100</span>"
      }`}
    ></span>
      <span class="hljs-tag">&#x3C;<span class="hljs-name">ShowViews</span> <span class="hljs-attr">views</span>=<span class="hljs-string">{views?.views</span> ?? <span class="hljs-attr">0</span>} <span class="hljs-attr">hasError</span>=<span class="hljs-string">{!!viewsError}</span> /></span>
      <span class="hljs-tag">&#x3C;<span class="hljs-name">LikeButton</span>
        <span class="hljs-attr">slug</span>=<span class="hljs-string">{slug}</span>
        <span class="hljs-attr">count</span>=<span class="hljs-string">{likes?.count</span> ?? <span class="hljs-attr">0</span>}
        <span class="hljs-attr">liked</span>=<span class="hljs-string">{likes?.hasLiked</span> ?? <span class="hljs-attr">false</span>}
        <span class="hljs-attr">isLoading</span>=<span class="hljs-string">{likesLoading}</span>
        <span class="hljs-attr">hasError</span>=<span class="hljs-string">{!!likesError}</span>
      /></span>
    <span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span></span>
  );
}
</code></pre>
<h2>Building the Like Button</h2>
<p>For likes, I wanted to prevent people from spamming the button, so I added IP hashing and server actions:</p>
<pre><code class="hljs language-tsx"><span class="hljs-comment">// lib/actions/likeCount.ts</span>
<span class="hljs-string">"use server"</span>;

<span class="hljs-keyword">import</span> redis <span class="hljs-keyword">from</span> <span class="hljs-string">"../redis"</span>;
<span class="hljs-keyword">import</span> { getHashedIP } <span class="hljs-keyword">from</span> <span class="hljs-string">"./hashIP"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">getLikeCount</span>(<span class="hljs-params"><span class="hljs-attr">slug</span>: <span class="hljs-built_in">string</span></span>) {
  <span class="hljs-keyword">const</span> hashedIp = <span class="hljs-keyword">await</span> <span class="hljs-title function_">getHashedIP</span>();
  <span class="hljs-keyword">const</span> likeKey = [<span class="hljs-string">"likes"</span>, <span class="hljs-string">"blogs"</span>, slug].<span class="hljs-title function_">join</span>(<span class="hljs-string">":"</span>);
  <span class="hljs-keyword">const</span> userLikeKey = [<span class="hljs-string">"ip"</span>, hashedIp, <span class="hljs-string">"liked"</span>, slug].<span class="hljs-title function_">join</span>(<span class="hljs-string">":"</span>);

  <span class="hljs-keyword">const</span> [likeCount, hasLiked] = <span class="hljs-keyword">await</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">all</span>([
    redis.<span class="hljs-property">get</span>&#x3C;<span class="hljs-built_in">number</span>>(likeKey),
    redis.<span class="hljs-title function_">get</span>(userLikeKey),
  ]);

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">count</span>: likeCount ?? <span class="hljs-number">0</span>,
    <span class="hljs-attr">hasLiked</span>: !!hasLiked,
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">toggleLike</span>(<span class="hljs-params"><span class="hljs-attr">slug</span>: <span class="hljs-built_in">string</span></span>) {
  <span class="hljs-keyword">const</span> hashedIp = <span class="hljs-keyword">await</span> <span class="hljs-title function_">getHashedIP</span>();
  <span class="hljs-keyword">const</span> likeKey = [<span class="hljs-string">"likes"</span>, <span class="hljs-string">"blogs"</span>, slug].<span class="hljs-title function_">join</span>(<span class="hljs-string">":"</span>);
  <span class="hljs-keyword">const</span> userLikeKey = [<span class="hljs-string">"ip"</span>, hashedIp, <span class="hljs-string">"liked"</span>, slug].<span class="hljs-title function_">join</span>(<span class="hljs-string">":"</span>);

  <span class="hljs-keyword">const</span> hasLiked = <span class="hljs-keyword">await</span> redis.<span class="hljs-title function_">get</span>(userLikeKey);
  <span class="hljs-keyword">const</span> pipeline = redis.<span class="hljs-title function_">pipeline</span>();

  <span class="hljs-keyword">if</span> (hasLiked) {
    <span class="hljs-comment">// Unlike</span>
    pipeline.<span class="hljs-title function_">decr</span>(likeKey);
    pipeline.<span class="hljs-title function_">del</span>(userLikeKey);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Like</span>
    pipeline.<span class="hljs-title function_">incr</span>(likeKey);
    pipeline.<span class="hljs-title function_">set</span>(userLikeKey, <span class="hljs-string">"1"</span>);
  }

  <span class="hljs-keyword">await</span> pipeline.<span class="hljs-title function_">exec</span>();
  <span class="hljs-keyword">const</span> newCount = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">max</span>(<span class="hljs-number">0</span>, (<span class="hljs-keyword">await</span> redis.<span class="hljs-property">get</span>&#x3C;<span class="hljs-built_in">number</span>>(likeKey)) ?? <span class="hljs-number">0</span>);

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>,
    newCount,
    <span class="hljs-attr">liked</span>: !hasLiked,
  };
}
</code></pre>
<p>The like system also needs API endpoints to handle both fetching current like state and toggling likes. Here's the API route that connects to our server actions:</p>
<pre><code class="hljs language-tsx"><span class="hljs-comment">// app/api/likes/[slug]/route.ts</span>
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">NextRequest</span>, <span class="hljs-title class_">NextResponse</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { getLikeCount, toggleLike } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/actions/likeCount"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">GET</span>(<span class="hljs-params">
  <span class="hljs-attr">request</span>: <span class="hljs-title class_">NextRequest</span>,
  { params }: { params: <span class="hljs-built_in">Promise</span>&#x3C;{ slug: <span class="hljs-built_in">string</span> }> },
</span>) {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { slug } = <span class="hljs-keyword">await</span> params;
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> <span class="hljs-title function_">getLikeCount</span>(slug);

    <span class="hljs-keyword">return</span> <span class="hljs-title class_">NextResponse</span>.<span class="hljs-title function_">json</span>({
      <span class="hljs-attr">count</span>: result.<span class="hljs-property">count</span>,
      <span class="hljs-attr">hasLiked</span>: result.<span class="hljs-property">hasLiked</span>,
    });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"Error fetching likes:"</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-title class_">NextResponse</span>.<span class="hljs-title function_">json</span>(
      { <span class="hljs-attr">error</span>: <span class="hljs-string">"Failed to fetch likes"</span> },
      { <span class="hljs-attr">status</span>: <span class="hljs-number">500</span> },
    );
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">POST</span>(<span class="hljs-params">
  <span class="hljs-attr">request</span>: <span class="hljs-title class_">NextRequest</span>,
  { params }: { params: <span class="hljs-built_in">Promise</span>&#x3C;{ slug: <span class="hljs-built_in">string</span> }> },
</span>) {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { slug } = <span class="hljs-keyword">await</span> params;
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> <span class="hljs-title function_">toggleLike</span>(slug);

    <span class="hljs-keyword">if</span> (result.<span class="hljs-property">success</span>) {
      <span class="hljs-keyword">return</span> <span class="hljs-title class_">NextResponse</span>.<span class="hljs-title function_">json</span>({
        <span class="hljs-attr">count</span>: result.<span class="hljs-property">newCount</span>,
        <span class="hljs-attr">hasLiked</span>: result.<span class="hljs-property">liked</span>,
      });
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">return</span> <span class="hljs-title class_">NextResponse</span>.<span class="hljs-title function_">json</span>(
        { <span class="hljs-attr">error</span>: <span class="hljs-string">"Failed to toggle like"</span> },
        { <span class="hljs-attr">status</span>: <span class="hljs-number">500</span> },
      );
    }
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"Error toggling like:"</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-title class_">NextResponse</span>.<span class="hljs-title function_">json</span>(
      { <span class="hljs-attr">error</span>: <span class="hljs-string">"Failed to toggle like"</span> },
      { <span class="hljs-attr">status</span>: <span class="hljs-number">500</span> },
    );
  }
}
</code></pre>
<p>And the like button component:</p>
<pre><code class="hljs language-tsx"><span class="hljs-comment">// components/blog/like-button.tsx</span>
<span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> { mutate } <span class="hljs-keyword">from</span> <span class="hljs-string">"swr"</span>;
<span class="hljs-keyword">import</span> { useTransition } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">Heart</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"lucide-react"</span>;

<span class="hljs-keyword">interface</span> <span class="hljs-title class_">LikeButtonProps</span> {
  <span class="hljs-attr">slug</span>: <span class="hljs-built_in">string</span>;
  <span class="hljs-attr">count</span>: <span class="hljs-built_in">number</span>;
  <span class="hljs-attr">liked</span>: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-attr">size</span>?: <span class="hljs-string">"sm"</span> | <span class="hljs-string">"md"</span> | <span class="hljs-string">"lg"</span>;
  <span class="hljs-attr">isLoading</span>?: <span class="hljs-built_in">boolean</span>;
  <span class="hljs-attr">hasError</span>?: <span class="hljs-built_in">boolean</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">LikeButton</span>(<span class="hljs-params">{
  slug,
  count,
  liked,
  size = <span class="hljs-string">"md"</span>,
  isLoading = <span class="hljs-literal">false</span>,
  hasError = <span class="hljs-literal">false</span>,
}: <span class="hljs-title class_">LikeButtonProps</span></span>) {
  <span class="hljs-keyword">const</span> [isPending, startTransition] = <span class="hljs-title function_">useTransition</span>();

  <span class="hljs-keyword">const</span> sizeConfig = {
    <span class="hljs-attr">sm</span>: { <span class="hljs-attr">icon</span>: <span class="hljs-number">14</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">"text-xs"</span>, <span class="hljs-attr">gap</span>: <span class="hljs-string">"gap-1"</span> },
    <span class="hljs-attr">md</span>: { <span class="hljs-attr">icon</span>: <span class="hljs-number">16</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">"text-sm"</span>, <span class="hljs-attr">gap</span>: <span class="hljs-string">"gap-1"</span> },
    <span class="hljs-attr">lg</span>: { <span class="hljs-attr">icon</span>: <span class="hljs-number">20</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">"text-base"</span>, <span class="hljs-attr">gap</span>: <span class="hljs-string">"gap-2"</span> },
  };

  <span class="hljs-keyword">const</span> config = sizeConfig[size];

  <span class="hljs-keyword">const</span> <span class="hljs-title function_">handleLike</span> = (<span class="hljs-params"></span>) => {
    <span class="hljs-keyword">const</span> newLiked = !liked;
    <span class="hljs-keyword">const</span> newCount = liked ? count - <span class="hljs-number">1</span> : count + <span class="hljs-number">1</span>;

    <span class="hljs-comment">// Optimistic update</span>
    <span class="hljs-title function_">mutate</span>(
      <span class="hljs-string">`/api/likes/<span class="hljs-subst">${slug}</span>`</span>,
      { <span class="hljs-attr">count</span>: newCount, <span class="hljs-attr">hasLiked</span>: newLiked },
      <span class="hljs-literal">false</span>,
    );

    <span class="hljs-title function_">startTransition</span>(<span class="hljs-title function_">async</span> () => {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-string">`/api/likes/<span class="hljs-subst">${slug}</span>`</span>, {
          <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
        });

        <span class="hljs-keyword">if</span> (response.<span class="hljs-property">ok</span>) {
          <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> response.<span class="hljs-title function_">json</span>();
          <span class="hljs-title function_">mutate</span>(<span class="hljs-string">`/api/likes/<span class="hljs-subst">${slug}</span>`</span>, result, <span class="hljs-literal">false</span>);
        } <span class="hljs-keyword">else</span> {
          <span class="hljs-comment">// Revert on error</span>
          <span class="hljs-title function_">mutate</span>(<span class="hljs-string">`/api/likes/<span class="hljs-subst">${slug}</span>`</span>, { count, <span class="hljs-attr">hasLiked</span>: liked }, <span class="hljs-literal">false</span>);
        }
      } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"Failed to toggle like:"</span>, error);
        <span class="hljs-title function_">mutate</span>(<span class="hljs-string">`/api/likes/<span class="hljs-subst">${slug}</span>`</span>, { count, <span class="hljs-attr">hasLiked</span>: liked }, <span class="hljs-literal">false</span>);
      }
    });
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">button</span>
      <span class="hljs-attr">onClick</span>=<span class="hljs-string">{handleLike}</span>
      <span class="hljs-attr">disabled</span>=<span class="hljs-string">{isPending</span> || <span class="hljs-attr">isLoading</span>}
      <span class="hljs-attr">className</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">group</span> <span class="hljs-attr">flex</span> <span class="hljs-attr">items-center</span> ${<span class="hljs-attr">config.gap</span>} <span class="hljs-attr">transition-colors</span> <span class="hljs-attr">disabled:opacity-50</span>`}
    ></span>
      <span class="hljs-tag">&#x3C;<span class="hljs-name">Heart</span>
        <span class="hljs-attr">size</span>=<span class="hljs-string">{config.icon}</span>
        <span class="hljs-attr">className</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">transition-colors</span> ${
          <span class="hljs-attr">liked</span>
            ? "<span class="hljs-attr">fill-black</span> <span class="hljs-attr">stroke-black</span>"
            <span class="hljs-attr">:</span> "<span class="hljs-attr">stroke-neutral-600</span> <span class="hljs-attr">group-hover:stroke-black</span>"
        }`}
      /></span>
      <span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>
        <span class="hljs-attr">className</span>=<span class="hljs-string">{</span>`${<span class="hljs-attr">config.text</span>} <span class="hljs-attr">transition-colors</span> ${
          <span class="hljs-attr">liked</span> ? "<span class="hljs-attr">text-black</span>" <span class="hljs-attr">:</span> "<span class="hljs-attr">text-neutral-600</span> <span class="hljs-attr">group-hover:text-black</span>"
        }`}
      ></span>
        {hasError ? "0" : count}
      <span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
    <span class="hljs-tag">&#x3C;/<span class="hljs-name">button</span>></span></span>
  );
}
</code></pre>
<h2>Why This Works So Well</h2>
<p>This approach keeps pages static while adding interactivity. The initial page load is instant because it's served from a CDN, and the engagement data loads in the background. Pages work without JavaScript (progressive enhancement), and there are no hydration mismatches.</p>
<p>Most importantly, you keep all the prefetching magic that makes static sites feel so snappy.</p>
<h2>Making It Even Better with SWR</h2>
<p>For an even smoother experience, I use SWR throughout the engagement system. SWR (stale-while-revalidate) is a data fetching library that handles caching, revalidation, and optimistic updates.</p>
<p>The basic idea is that SWR shows you cached data immediately (stale), then fetches fresh data in the background (revalidate). It also gives you tools for optimistic updates, where you update the UI immediately and then sync with the server.</p>
<p>My <code>EngagementStats</code> component uses SWR to fetch both views and likes data:</p>
<pre><code class="hljs language-tsx"><span class="hljs-keyword">const</span> {
  <span class="hljs-attr">data</span>: likes,
  <span class="hljs-attr">error</span>: likesError,
  <span class="hljs-attr">isLoading</span>: likesLoading,
} = <span class="hljs-title function_">useSWR</span>(<span class="hljs-string">`/api/likes/<span class="hljs-subst">${slug}</span>`</span>);
<span class="hljs-keyword">const</span> {
  <span class="hljs-attr">data</span>: views,
  <span class="hljs-attr">error</span>: viewsError,
  <span class="hljs-attr">isLoading</span>: viewsLoading,
} = <span class="hljs-title function_">useSWR</span>(<span class="hljs-string">`/api/views/<span class="hljs-subst">${slug}</span>`</span>);
</code></pre>
<p>With SWR, clicking like/unlike feels instant because the UI updates immediately with <code>mutate()</code>, even before the server responds. If the server request fails, I manually revert the optimistic update to keep the UI in sync.</p>
<h2>Wrapping Up</h2>
<p>You don't have to sacrifice static generation to add dynamic features. Keep your shell static, load engagement data client-side, and make sure everything works without JavaScript first. Your users get fast page loads, instant navigation between pages, and engaging interactions without any compromises.</p>]]></content:encoded>
            <author>contact@jakmaz.com (Jakub Mazur)</author>
            <category>tutorial</category>
            <category>nextjs</category>
            <category>web</category>
            <enclosure url="https://jakmaz.com/og/static-engagement.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[How to add Auto Generated Preview Images for Your Next.js Blog]]></title>
            <link>https://jakmaz.com/blog/og-images</link>
            <guid isPermaLink="false">https://jakmaz.com/blog/og-images</guid>
            <pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[A step-by-step guide to adding automaticaly generating Open Graph images to your Next.js blog]]></description>
            <content:encoded><![CDATA[<p>If you want your blog posts to look great when shared on social media, you need Open Graph (OG) images. Next.js 13+ makes it possible to generate these images dynamically for each post, but there are some important caveats—especially around file system access and static vs. dynamic generation.</p>
<p>This tutorial will walk you through the process, highlight the pitfalls I hit, and show you the final working solution.</p>
<h2>Why Dynamic OG Images?</h2>
<p>Static OG images are fine, but dynamic ones let you:</p>
<ul>
<li>Show the post title, tags, and excerpt on the image</li>
<li>Automatically update images when you change your content</li>
<li>Avoid manually designing a new image for every post</li>
</ul>
<h2>Step 1: The Naive Approach (and Why It Fails)</h2>
<p>My first attempt was to use a dynamic route handler for OG images:</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// app/blog/[slug]/opengraph-image.tsx</span>
<span class="hljs-keyword">import</span> { getBlogPostBySlug } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/postsLoaders"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">ImageResponse</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/og"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> alt = <span class="hljs-string">"My Blog"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> size = { <span class="hljs-attr">width</span>: <span class="hljs-number">1200</span>, <span class="hljs-attr">height</span>: <span class="hljs-number">630</span> };
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> contentType = <span class="hljs-string">"image/png"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Image</span>(<span class="hljs-params">{ params }: { params: { slug: <span class="hljs-built_in">string</span> } }</span>) {
  <span class="hljs-keyword">const</span> post = <span class="hljs-title function_">getBlogPostBySlug</span>(params.<span class="hljs-property">slug</span>);
  <span class="hljs-comment">// ...generate image...</span>
}
</code></pre>
<p>My <code>getBlogPostBySlug</code> function used <code>fs.readFileSync</code> to read markdown files from disk. This worked perfectly in development.</p>
<p><strong>But in production, all OG images broke.</strong></p>
<h2>Step 2: Understanding the Problem</h2>
<p>Here’s what I learned:</p>
<ul>
<li><strong>In development:</strong><br>
Your local server has access to the file system, so <code>fs</code> works.</li>
<li><strong>In production (Vercel, Netlify, etc.):</strong><br>
Dynamic route handlers (like <code>opengraph-image.tsx</code>) run in a serverless/edge environment, which <strong>does not have access to your project’s file system</strong> (except for <code>/public</code>).<br>
Any attempt to use <code>fs</code> to read files will fail.</li>
</ul>
<p><strong>Key lesson:</strong></p>
<blockquote>
<p>You cannot use <code>fs</code> to read files in dynamic route handlers in production.</p>
</blockquote>
<h2>Step 3: The Docs Confusion</h2>
<p>The Next.js docs show examples using <code>fetch</code> or even <code>fs</code> to load assets. The key detail is:</p>
<ul>
<li>If you use <code>generateStaticParams</code> to enumerate all possible slugs at build time, Next.js will generate the OG images as static assets, and you <em>can</em> use <code>fs</code> (because it runs at build time).</li>
<li>If you don’t, the handler runs at request time in production, and <code>fs</code> will fail.</li>
</ul>
<h2>Step 4: The Solution — Static Generation with <code>generateStaticParams</code></h2>
<p>To make it work, I added a <code>generateStaticParams</code> export to my OG image route:</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// app/blog/[slug]/opengraph-image.tsx</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">generateStaticParams</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> { getAllBlogPosts } = <span class="hljs-keyword">await</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">"@/lib/postsLoaders"</span>);
  <span class="hljs-keyword">const</span> posts = <span class="hljs-title function_">getAllBlogPosts</span>();
  <span class="hljs-keyword">return</span> posts.<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">post</span>) =></span> ({ <span class="hljs-attr">slug</span>: post.<span class="hljs-property">slug</span> }));
}
</code></pre>
<p>Now, Next.js knows all possible slugs at build time and generates the OG images as static files. This means:</p>
<ul>
<li>You can use <code>fs</code> in your handler (it runs at build time).</li>
<li>Your OG images are fast and reliable in production.</li>
</ul>
<h2>Step 5: The Final Working Code</h2>
<p>Here’s the complete code for my OG image route:</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// app/blog/[slug]/opengraph-image.tsx</span>
<span class="hljs-keyword">import</span> { getBlogPostBySlug } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/postsLoaders"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">ImageResponse</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/og"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> alt = <span class="hljs-string">"jakmaz.com"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> size = {
  <span class="hljs-attr">width</span>: <span class="hljs-number">1200</span>,
  <span class="hljs-attr">height</span>: <span class="hljs-number">630</span>,
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> contentType = <span class="hljs-string">"image/png"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Image</span>(<span class="hljs-params">{ params }: { params: { slug: <span class="hljs-built_in">string</span> } }</span>) {
  <span class="hljs-keyword">const</span> post = <span class="hljs-title function_">getBlogPostBySlug</span>(params.<span class="hljs-property">slug</span>);

  <span class="hljs-keyword">if</span> (!post) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(<span class="hljs-string">"Not found"</span>, { <span class="hljs-attr">status</span>: <span class="hljs-number">404</span> });
  }

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">ImageResponse</span>(
    <span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span>
      <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
        <span class="hljs-attr">height:</span> "<span class="hljs-attr">100</span>%",
        <span class="hljs-attr">width:</span> "<span class="hljs-attr">100</span>%",
        <span class="hljs-attr">display:</span> "<span class="hljs-attr">flex</span>",
        <span class="hljs-attr">flexDirection:</span> "<span class="hljs-attr">column</span>",
        <span class="hljs-attr">justifyContent:</span> "<span class="hljs-attr">space-between</span>",
        <span class="hljs-attr">backgroundColor:</span> "#<span class="hljs-attr">FFFFFF</span>",
        <span class="hljs-attr">color:</span> "#<span class="hljs-attr">111827</span>",
        <span class="hljs-attr">padding:</span> "<span class="hljs-attr">64px</span>",
        <span class="hljs-attr">fontFamily:</span> "<span class="hljs-attr">sans-serif</span>",
        <span class="hljs-attr">boxSizing:</span> "<span class="hljs-attr">border-box</span>",
        <span class="hljs-attr">position:</span> "<span class="hljs-attr">relative</span>",
      }}
    ></span>
      {/* Left accent bar */}
      <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span>
        <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
          <span class="hljs-attr">position:</span> "<span class="hljs-attr">absolute</span>",
          <span class="hljs-attr">top:</span> <span class="hljs-attr">0</span>,
          <span class="hljs-attr">left:</span> <span class="hljs-attr">0</span>,
          <span class="hljs-attr">width:</span> "<span class="hljs-attr">12px</span>",
          <span class="hljs-attr">height:</span> "<span class="hljs-attr">100</span>%",
          <span class="hljs-attr">backgroundColor:</span> "#<span class="hljs-attr">000000</span>",
        }}
      /></span>

      {/* Title + Tags + Excerpt */}
      <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> "<span class="hljs-attr">flex</span>", <span class="hljs-attr">flexDirection:</span> "<span class="hljs-attr">column</span>", <span class="hljs-attr">gap:</span> "<span class="hljs-attr">32px</span>" }}></span>
        {/* Tags */}
        <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">display:</span> "<span class="hljs-attr">flex</span>", <span class="hljs-attr">gap:</span> "<span class="hljs-attr">12px</span>", <span class="hljs-attr">flexWrap:</span> "<span class="hljs-attr">wrap</span>" }}></span>
          {post.tags.map((tag, index) => (
            <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span>
              <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span>
              <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
                <span class="hljs-attr">backgroundColor:</span> "#<span class="hljs-attr">000000</span>",
                <span class="hljs-attr">color:</span> "#<span class="hljs-attr">FFFFFF</span>",
                <span class="hljs-attr">padding:</span> "<span class="hljs-attr">8px</span> <span class="hljs-attr">16px</span>",
                <span class="hljs-attr">borderRadius:</span> "<span class="hljs-attr">8px</span>",
                <span class="hljs-attr">fontSize:</span> "<span class="hljs-attr">18px</span>",
                <span class="hljs-attr">fontWeight:</span> <span class="hljs-attr">600</span>,
              }}
            ></span>
              {tag}
            <span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
          ))}
        <span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>

        <span class="hljs-tag">&#x3C;<span class="hljs-name">h1</span>
          <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
            <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">72</span>,
            <span class="hljs-attr">fontWeight:</span> <span class="hljs-attr">800</span>,
            <span class="hljs-attr">lineHeight:</span> <span class="hljs-attr">1.1</span>,
            <span class="hljs-attr">margin:</span> <span class="hljs-attr">0</span>,
          }}
        ></span>
          {post.title}
        <span class="hljs-tag">&#x3C;/<span class="hljs-name">h1</span>></span>
        <span class="hljs-tag">&#x3C;<span class="hljs-name">p</span>
          <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
            <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">32</span>,
            <span class="hljs-attr">color:</span> "#<span class="hljs-attr">6B7280</span>",
            <span class="hljs-attr">margin:</span> <span class="hljs-attr">0</span>,
            <span class="hljs-attr">maxWidth:</span> "<span class="hljs-attr">80</span>%",
            <span class="hljs-attr">lineHeight:</span> <span class="hljs-attr">1.4</span>,
          }}
        ></span>
          {post.excerpt}
        <span class="hljs-tag">&#x3C;/<span class="hljs-name">p</span>></span>
      <span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>

      {/* Footer author name */}
      <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span>
        <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
          <span class="hljs-attr">display:</span> "<span class="hljs-attr">flex</span>",
          <span class="hljs-attr">justifyContent:</span> "<span class="hljs-attr">space-between</span>",
          <span class="hljs-attr">width:</span> "<span class="hljs-attr">100</span>%",
          <span class="hljs-attr">fontSize:</span> <span class="hljs-attr">28</span>,
          <span class="hljs-attr">color:</span> "#<span class="hljs-attr">6B7280</span>",
          <span class="hljs-attr">fontWeight:</span> <span class="hljs-attr">600</span>,
        }}
      ></span>
        <span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>jakmaz.com<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
        <span class="hljs-tag">&#x3C;<span class="hljs-name">span</span>></span>{new Date(post.date).toLocaleDateString()}<span class="hljs-tag">&#x3C;/<span class="hljs-name">span</span>></span>
      <span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
    <span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span></span>,
    {
      ...size,
    },
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">generateStaticParams</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> { getAllBlogPosts } = <span class="hljs-keyword">await</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">"@/lib/postsLoaders"</span>);
  <span class="hljs-keyword">const</span> posts = <span class="hljs-title function_">getAllBlogPosts</span>();
  <span class="hljs-keyword">return</span> posts.<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">post</span>) =></span> ({ <span class="hljs-attr">slug</span>: post.<span class="hljs-property">slug</span> }));
}
</code></pre>
<h2>Step 6: Alternative — Dynamic API Route (No <code>fs</code>!)</h2>
<p>If you want to generate OG images dynamically (e.g., always up-to-date, or for slugs not known at build time), you can use an API route:</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// app/api/og/route.tsx</span>
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">ImageResponse</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/og"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> runtime = <span class="hljs-string">"edge"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">GET</span>(<span class="hljs-params"><span class="hljs-attr">request</span>: <span class="hljs-title class_">Request</span></span>) {
  <span class="hljs-keyword">const</span> { searchParams } = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(request.<span class="hljs-property">url</span>);
  <span class="hljs-keyword">const</span> title = searchParams.<span class="hljs-title function_">get</span>(<span class="hljs-string">"title"</span>) || <span class="hljs-string">"My Blog"</span>;
  <span class="hljs-comment">// ...generate image using title...</span>
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">ImageResponse</span>(<span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">div</span>></span>{title}<span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span></span>);
}
</code></pre>
<p><strong>But:</strong><br>
You cannot use <code>fs</code> in this API route in production. Pass all needed data via query params, or use a database or pre-bundled JSON.</p>
<h2>Key Takeaways</h2>
<ul>
<li><strong>Don’t use <code>fs</code> in dynamic route handlers unless you’re sure they run at build time.</strong></li>
<li><strong>Use <code>generateStaticParams</code> to statically generate OG images for all known slugs.</strong></li>
<li><strong>For dynamic images, use an API route and pass all data via query params or use a runtime-available data source.</strong></li>
</ul>]]></content:encoded>
            <author>contact@jakmaz.com (Jakub Mazur)</author>
            <category>tutorial</category>
            <category>nextjs</category>
            <category>web</category>
            <enclosure url="https://jakmaz.com/og/og-images.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Minimalistic Modern LSP Setup in Neovim with lazy.nvim]]></title>
            <link>https://jakmaz.com/blog/nvim-lsp</link>
            <guid isPermaLink="false">https://jakmaz.com/blog/nvim-lsp</guid>
            <pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how to automatically install language servers and connect them to nvim]]></description>
            <content:encoded><![CDATA[<p>If you're customizing Neovim and want a more powerful coding experience - real-time diagnostics, go-to-definition, smart completion, code
actions - you'll want LSP.</p>
<p>This post walks you through a <strong>minimal LSP setup</strong> using <a href="https://github.com/folke/lazy.nvim">lazy.nvim</a> for plugin management.</p>
<h2>What is LSP?</h2>
<p>LSP stands for <strong>Language Server Protocol</strong> - it defines a standard way for editors and language-specific servers to communicate.</p>
<p>By connecting Neovim to language servers, you get:</p>
<ul>
<li>Syntax checking (errors, warnings)</li>
<li>Auto-completion</li>
<li>Go-to-definition, references</li>
<li>Hover documentation</li>
<li>Code actions (like quick-fixes, rename, refactor)</li>
<li>Formatting</li>
</ul>
<h2>Plugins Used</h2>
<p>We’ll use the following Neovim plugins:</p>
<ul>
<li>
<p><a href="https://github.com/mason-org/mason.nvim"><code>mason.nvim</code></a><br>
Easily install and manage LSP servers, formatters, linters, and debuggers from inside Neovim.</p>
</li>
<li>
<p><a href="https://github.com/mason-org/mason-lspconfig.nvim"><code>mason-lspconfig.nvim</code></a><br>
A bridge between Mason and Neovim’s built-in LSP client. It ensures the correct servers are installed and makes them easy to configure.</p>
</li>
<li>
<p><a href="https://github.com/neovim/nvim-lspconfig"><code>nvim-lspconfig</code></a><br>
Official plugin for configuring and launching language servers. It handles the actual connection between Neovim and each server.</p>
</li>
</ul>
<h2>Step 1: Create <code>lsp-config.lua</code></h2>
<p>To keep things organized, start by creating a new file for LSP-related plugins:</p>
<pre><code class="hljs language-bash">~/.config/nvim/lua/plugins/lsp-config.lua
</code></pre>
<p>If you're using a plugin manager like <code>lazy.nvim</code> or <code>packer.nvim</code>, this is where you’ll define and configure your LSP setup.</p>
<h2>Step 2: Install <code>mason.nvim</code> - A Package Manager for LSPs</h2>
<p>First, we need a way to <strong>install and manage language servers</strong> without dealing with npm, pip, or system package managers. That’s exactly
what <a href="https://github.com/mason-org/mason.nvim"><code>mason.nvim</code></a> does.</p>
<h3>What is Mason?</h3>
<p><code>mason.nvim</code> is a plugin that downloads and manages:</p>
<ul>
<li>LSP servers</li>
<li>Formatters (like <code>prettier</code>, <code>black</code>, <code>clang-format</code>)</li>
<li>Linters (like <code>eslint</code>, <code>flake8</code>)</li>
<li>Debuggers</li>
</ul>
<p>All directly from Neovim - cross-platform and without leaving your config.</p>
<h3>Add this to your <code>lsp-config.lua</code>:</h3>
<pre><code class="hljs language-lua"><span class="hljs-keyword">return</span> {
  {
    <span class="hljs-string">'mason-org/mason.nvim'</span>,
    opts = {},
  },
}
</code></pre>
<h3>Now What?</h3>
<p>Restart Neovim and run:</p>
<pre><code>:Mason
</code></pre>
<p>This opens an UI where you can:</p>
<ul>
<li>Browse all available LSP servers, linters, and formatters</li>
<li>Install or uninstall them with a keypress</li>
<li>See what's already installed</li>
</ul>
<blockquote>
<p>Neovim doesn't yet know which language servers to connect to, or how to talk to them. Let’s fix that next.</p>
</blockquote>
<h2>Step 3: Install <code>mason-lspconfig.nvim</code> - Connecting Mason to Neovim</h2>
<p>Now that <code>mason.nvim</code> is installed, you might be wondering:</p>
<blockquote>
<p>“How does Neovim know which LSP servers to actually set up and connect to my code?”</p>
</blockquote>
<p>That’s where <a href="https://github.com/mason-org/mason-lspconfig.nvim"><code>mason-lspconfig.nvim</code></a> comes in.</p>
<h3>Why We Use <code>mason-lspconfig.nvim</code></h3>
<p>This plugin acts as a <strong>bridge</strong> between Mason and Neovim's built-in LSP client. It:</p>
<ul>
<li>Automatically installs LSP servers listed in <code>ensure_installed</code></li>
<li>Helps match Mason’s internal server names to the config names expected by Neovim</li>
<li>Makes setup smoother and less error-prone</li>
</ul>
<h3>Add this to your <code>lsp-config.lua</code>:</h3>
<pre><code class="hljs language-lua">{
  <span class="hljs-string">"mason-org/mason-lspconfig.nvim"</span>,
  opts = {
    ensure_installed = {
      <span class="hljs-string">"lua_ls"</span>,
      <span class="hljs-string">"pyright"</span>,
      <span class="hljs-string">"ts_ls"</span>,
      <span class="hljs-string">"rust_analyzer"</span>,
      <span class="hljs-string">"clangd"</span>
    },
  },
},
</code></pre>
<p>This tells Mason to automatically install these popular LSP servers on startup:</p>
<ul>
<li><code>lua_ls</code>: for Lua (great for configuring Neovim)</li>
<li><code>pyright</code>: Python</li>
<li><code>ts_ls</code>: JavaScript &#x26; TypeScript</li>
<li><code>rust_analyzer</code>: Rust</li>
<li><code>clangd</code>: C and C++</li>
</ul>
<p>You can find the full list of supported servers <a href="https://github.com/mason-org/mason-lspconfig.nvim#available-lsp-servers">here</a>.</p>
<p><strong>After this step</strong>:<br>
Neovim now knows:</p>
<ul>
<li>Which LSP servers to install</li>
<li>Where to find them (via Mason)</li>
<li>That these servers are available for connection</li>
</ul>
<p>But we still haven’t actually <em>hooked up</em> Neovim to the servers. That’s what we’ll handle next using <code>nvim-lspconfig</code>.</p>
<h2>Step 4: Connect to LSP Servers with <code>nvim-lspconfig</code></h2>
<p>So far, we've told Mason which language servers to install and manage. But installing the servers is only half of the setup - we still need
to <strong>tell Neovim how to connect to each server</strong> and actually start using them.</p>
<p>That's exactly what <a href="https://github.com/neovim/nvim-lspconfig"><code>nvim-lspconfig</code></a> is for.</p>
<h3>What is <code>nvim-lspconfig</code>?</h3>
<p><code>nvim-lspconfig</code> is the official plugin maintained by the Neovim team that provides easy configurations for connecting to LSP servers. Each
language server needs to be started with some settings, and this plugin handles the boilerplate for you.</p>
<p>In short, it’s the final link in the chain:</p>
<ul>
<li>Mason installs the servers</li>
<li><code>mason-lspconfig</code> tells us which ones we want</li>
<li><code>nvim-lspconfig</code> <strong>starts them and connects them to Neovim</strong></li>
</ul>
<p>Without this, the servers would be installed on your system, but Neovim wouldn’t use them.</p>
<h3>Add this to your <code>lsp-config.lua</code>:</h3>
<pre><code class="hljs language-lua">{
  <span class="hljs-string">"neovim/nvim-lspconfig"</span>,
  <span class="hljs-built_in">config</span> = <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span></span>
    <span class="hljs-keyword">local</span> lspconfig = <span class="hljs-built_in">require</span>(<span class="hljs-string">"lspconfig"</span>)

    lspconfig.lua_ls.setup({})
    lspconfig.pyright.setup({})
    lspconfig.ts_ls.setup({})
    lspconfig.rust_analyzer.setup({})
    lspconfig.clangd.setup({})
  <span class="hljs-keyword">end</span>
},
</code></pre>
<p>This code:</p>
<ul>
<li>Loads the <code>lspconfig</code> module</li>
<li>Calls <code>.setup({})</code> for each installed server</li>
<li>Initializes each language server so it attaches to your files when you open them</li>
</ul>
<p>You can customize the <code>{}</code> part for each server to pass in specific settings, root directories, or formatting options - but keeping it empty
works as a good starting point.</p>
<h3>How to Check if It's Working</h3>
<p>Once you've saved your config and restarted Neovim, open a file in one of the supported languages (like <code>.lua</code>, <code>.py</code>, or <code>.rs</code>), and run:</p>
<pre><code>:LspInfo
</code></pre>
<p>This will show you:</p>
<ul>
<li>Which servers are currently installed</li>
<li>Which ones are attached to your current buffer</li>
<li>Whether your LSP setup is working properly</li>
</ul>
<p>You should also see things like:</p>
<ul>
<li>Function names and types on hover</li>
<li>Inline diagnostics under problematic code</li>
<li>Completion suggestions from the language server</li>
</ul>
<p>At this point, you're connected - LSP is up and running.</p>
<h2>Result</h2>
<p>Our resulting config is</p>
<pre><code class="hljs language-lua"><span class="hljs-keyword">return</span> {
  <span class="hljs-comment">-- Mason: installs and manages external tools like LSP servers</span>
  {
    <span class="hljs-string">'mason-org/mason.nvim'</span>,
    opts = {},
  },

  <span class="hljs-comment">-- Mason-LSPConfig: tells Mason which servers to install and links them to lspconfig</span>
  {
    <span class="hljs-string">'mason-org/mason-lspconfig.nvim'</span>,
    opts = {
      ensure_installed = {
        <span class="hljs-string">'lua_ls'</span>, <span class="hljs-comment">-- Lua (great for editing Neovim config)</span>
        <span class="hljs-string">'pyright'</span>, <span class="hljs-comment">-- Python</span>
        <span class="hljs-string">'ts_ls'</span>, <span class="hljs-comment">-- TypeScript / JavaScript</span>
        <span class="hljs-string">'rust_analyzer'</span>, <span class="hljs-comment">-- Rust</span>
        <span class="hljs-string">'clangd'</span>, <span class="hljs-comment">-- C / C++</span>
      },
    },
  },

  <span class="hljs-comment">-- nvim-lspconfig: connects Neovim to installed LSP servers</span>
  {
    <span class="hljs-string">"neovim/nvim-lspconfig"</span>,
    <span class="hljs-built_in">config</span> = <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span></span>
      <span class="hljs-keyword">local</span> lspconfig = <span class="hljs-built_in">require</span>(<span class="hljs-string">"lspconfig"</span>)

      lspconfig.lua_ls.setup({})
      lspconfig.pyright.setup({})
      lspconfig.ts_ls.setup({})
      lspconfig.rust_analyzer.setup({})
      lspconfig.clangd.setup({})
    <span class="hljs-keyword">end</span>
  },
}
</code></pre>
<p>With this setup:</p>
<ul>
<li>LSP servers are installed automatically.</li>
<li>Neovim is connected to the servers and offers full IDE functionality.</li>
<li>Keymaps work out-of-the-box when a server is active.</li>
<li>Code actions look great thanks to Telescope.</li>
</ul>
<p>It’s <strong>minimal</strong>, <strong>modular</strong>, and <strong>fully powered by lazy.nvim</strong>.</p>
<hr>
<p><strong>Interested in my full Neovim setup and dotfiles?</strong>
Check them out here: <a href="https://github.com/jakmaz/dotfiles">jakmaz/dotfiles</a></p>]]></content:encoded>
            <author>contact@jakmaz.com (Jakub Mazur)</author>
            <category>tutorial</category>
            <category>neovim</category>
            <enclosure url="https://jakmaz.com/og/nvim-lsp.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[How to Set Up an Email Newsletter in Next.js for Free]]></title>
            <link>https://jakmaz.com/blog/email-newsletter</link>
            <guid isPermaLink="false">https://jakmaz.com/blog/email-newsletter</guid>
            <pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how to integrate an email newsletter into your Next.js website or portfolio.]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Edit: I removed newsletter on my website in favor of RSS feed but this post remains relevant!</p>
</blockquote>
<p>Adding an email newsletter to your website is a great way to engage visitors and build an audience. I wanted a solution to keep users updated with every new post while keeping
things simple and cost-effective.</p>
<h2>Choosing a Newsletter Service</h2>
<p>To find the right service, I set a few requirements:</p>
<ol>
<li><strong>Free tier</strong> – No upfront cost for small mailing lists.</li>
<li><strong>Custom domain support</strong> – The ability to send emails from my own domain.</li>
<li><strong>API access</strong> – So I could programmatically add subscribers.</li>
</ol>
<p>After testing several options, I found that <a href="https://www.sender.net/">Sender.net</a> met all my requirements. It offers a free plan with:</p>
<ul>
<li>Up to 2,500 subscribers</li>
<li>15,000 emails per month</li>
</ul>
<p>This was more than enough for my needs, so I proceeded with the next steps.</p>
<h2>Step 1: Creating a Simple React Form</h2>
<p>Let's start with a basic email subscription form in <strong>Next.js</strong>. This form will collect the user's email and submit it when they click the button.</p>
<pre><code class="hljs language-tsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Subscribe</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">form</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col gap-2"</span>></span>
      <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex gap-2"</span>></span>
        <span class="hljs-tag">&#x3C;<span class="hljs-name">input</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Email Address"</span> /></span>
        <span class="hljs-tag">&#x3C;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>></span>Subscribe<span class="hljs-tag">&#x3C;/<span class="hljs-name">button</span>></span>
      <span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
    <span class="hljs-tag">&#x3C;/<span class="hljs-name">form</span>></span></span>
  );
}
</code></pre>
<h3>What This Code Does:</h3>
<ul>
<li>It creates a simple <strong>input field</strong> for the email address.</li>
<li>It includes a <strong>"Subscribe" button</strong> for submission.</li>
<li>Right now, it <strong>doesn't send data anywhere</strong>—we'll handle that in the next steps.</li>
</ul>
<h2>Step 2: Adding an API Action to Handle Submissions</h2>
<p>To connect our form to <strong>Sender.net</strong>, we need a backend function that will:</p>
<ul>
<li><strong>Send a POST request</strong> to <a href="https://api.sender.net/">Sender.net’s API</a>.</li>
<li><strong>Pass the user's email</strong> along with our API token for authentication.</li>
</ul>
<p>Instead of creating a separate API route, we’ll use <strong><a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions">Next.js Server Actions</a></strong>, which allow
calling server-side functions directly from a form.</p>
<h3>Securing the API Key</h3>
<p>Before we write our API request, we need to <strong>store the Sender.net API key securely</strong>.<br>
Create a <code>.env.local</code> file in your Next.js project root and add:</p>
<pre><code>SENDER_TOKEN=your_sender_net_api_key
</code></pre>
<p>Make sure to restart your Next.js server after adding this to <strong>load the environment variable</strong>. If you're new to environment variables in Next.js, check out
<a href="https://nextjs.org/docs/basic-features/environment-variables">the official .env documentation</a>.</p>
<h3>Adding backend logic</h3>
<p>Create <code>actions.ts</code> and add the following:</p>
<pre><code class="hljs language-ts"><span class="hljs-string">"use server"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">addNewsletterSubscriber</span>(<span class="hljs-params"><span class="hljs-attr">_</span>: <span class="hljs-built_in">unknown</span>, <span class="hljs-attr">formData</span>: <span class="hljs-title class_">FormData</span></span>) {
  <span class="hljs-comment">// Extract the email submitted via the form</span>
  <span class="hljs-keyword">const</span> email = formData.<span class="hljs-title function_">get</span>(<span class="hljs-string">"email"</span>);

  <span class="hljs-comment">// Define the API URL for Sender.net</span>
  <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(<span class="hljs-string">"https://api.sender.net/v2/subscribers"</span>);

  <span class="hljs-comment">// Set request headers, including authentication with our API token</span>
  <span class="hljs-keyword">const</span> headers = {
    <span class="hljs-comment">// Secure API key from environment variables</span>
    <span class="hljs-title class_">Authorization</span>: <span class="hljs-string">"Bearer "</span> + process.<span class="hljs-property">env</span>.<span class="hljs-property">SENDER_TOKEN</span>,
    <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
    <span class="hljs-title class_">Accept</span>: <span class="hljs-string">"application/json"</span>,
  };

  <span class="hljs-comment">// Data payload to send in the request</span>
  <span class="hljs-keyword">const</span> data = {
    <span class="hljs-attr">email</span>: email,
    <span class="hljs-attr">trigger_automation</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// Prevents triggering automation flows in Sender.net</span>
  };

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Send the request to Sender.net</span>
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(url, {
      <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
      headers,
      <span class="hljs-attr">body</span>: <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(data),
    });

    <span class="hljs-comment">// Parse the response</span>
    <span class="hljs-keyword">const</span> responseData = <span class="hljs-keyword">await</span> response.<span class="hljs-title function_">json</span>();

    <span class="hljs-comment">// Handle success or failure</span>
    <span class="hljs-keyword">if</span> (response.<span class="hljs-property">ok</span> &#x26;&#x26; responseData.<span class="hljs-property">success</span>) {
      <span class="hljs-keyword">return</span> { <span class="hljs-attr">message</span>: <span class="hljs-string">"Subscription successful!"</span> };
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"Subscription failed:"</span>, responseData);
      <span class="hljs-keyword">return</span> { <span class="hljs-attr">message</span>: <span class="hljs-string">"Subscription failed. Please try again."</span> };
    }
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"An error occurred:"</span>, error);
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">message</span>: <span class="hljs-string">"An error occurred. Please try again later."</span> };
  }
}
</code></pre>
<h3>What This Code Does:</h3>
<ul>
<li>Extracts the submitted email from the form.</li>
<li>Sends a <strong>POST request</strong> to Sender.net with the email.</li>
<li>Uses an <strong>API token</strong> stored in <code>.env.local</code> (keeps it secure).</li>
<li>Handles <strong>success and error cases</strong> gracefully.</li>
</ul>
<h2>Step 3: Connecting the Form to the API</h2>
<p>Now, let's modify our frontend form to <strong>use the API action</strong>. This will make the form <strong>interactive</strong>—when the user submits their email, it will actually be stored in Sender.net.</p>
<p>Update the <code>Subscribe</code> component:</p>
<pre><code class="hljs language-tsx"><span class="hljs-string">"use client"</span>;

<span class="hljs-keyword">import</span> { addNewsletterSubscriber } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/actions"</span>;
<span class="hljs-keyword">import</span> { useActionState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Subscribe</span>(<span class="hljs-params"></span>) {
  <span class="hljs-comment">// useActionState connects our form to the server action</span>
  <span class="hljs-keyword">const</span> [response, formAction, isPending] = <span class="hljs-title function_">useActionState</span>(
    addNewsletterSubscriber,
    <span class="hljs-literal">undefined</span>,
  );

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&#x3C;<span class="hljs-name">form</span> <span class="hljs-attr">action</span>=<span class="hljs-string">{formAction}</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col gap-2"</span>></span>
      <span class="hljs-tag">&#x3C;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex gap-2"</span>></span>
        <span class="hljs-tag">&#x3C;<span class="hljs-name">input</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Email Address"</span> <span class="hljs-attr">required</span> /></span>
        <span class="hljs-tag">&#x3C;<span class="hljs-name">button</span> <span class="hljs-attr">disabled</span>=<span class="hljs-string">{isPending}</span>></span>Subscribe<span class="hljs-tag">&#x3C;/<span class="hljs-name">button</span>></span>
      <span class="hljs-tag">&#x3C;/<span class="hljs-name">div</span>></span>
      {/* Display feedback message after submission */}
      <span class="hljs-tag">&#x3C;<span class="hljs-name">p</span>></span>{response?.message}<span class="hljs-tag">&#x3C;/<span class="hljs-name">p</span>></span>
    <span class="hljs-tag">&#x3C;/<span class="hljs-name">form</span>></span></span>
  );
}
</code></pre>
<h3>What This Code Does:</h3>
<ul>
<li>Uses <code>useActionState()</code> to <strong>connect</strong> the form to our server action.</li>
<li>Calls <code>addNewsletterSubscriber</code> <strong>when the form is submitted</strong>.</li>
<li>Displays a message (<code>Subscription successful!</code> or <code>Subscription failed.</code>).</li>
<li>Disables the button while the request is pending.</li>
</ul>
<h2>Step 4: Testing and Deploying</h2>
<p>Now that everything is set up:</p>
<ol>
<li><strong>Run your Next.js app</strong> and enter an email into the form.</li>
<li><strong>Check your Sender.net account</strong> to see if the email was added.</li>
<li><strong>Deploy</strong> to production (e.g., Vercel, Netlify) with your <code>.env</code> properly configured.</li>
</ol>
<h2>Summary</h2>
<p>By following this guide, you’ve successfully:</p>
<ul>
<li><strong>Built a React form</strong> for newsletter signups.</li>
<li><strong>Connected it to Sender.net’s API</strong> using <a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions">Next.js Server Actions</a>.</li>
<li><strong>Ensured security</strong> by storing the API key safely.</li>
</ul>
<p>Now, your website visitors can subscribe to your newsletter <strong>seamlessly</strong>! 🎉</p>]]></content:encoded>
            <author>contact@jakmaz.com (Jakub Mazur)</author>
            <category>tutorial</category>
            <category>web</category>
            <category>nextjs</category>
            <enclosure url="https://jakmaz.com/og/email-newsletter.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[The Beginning of My Coding Journey]]></title>
            <link>https://jakmaz.com/blog/coding-journey</link>
            <guid isPermaLink="false">https://jakmaz.com/blog/coding-journey</guid>
            <pubDate>Mon, 16 Sep 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[A quick story of how I discovered coding and the passion that drives me today.]]></description>
            <content:encoded><![CDATA[<p>When I was younger, Minecraft was the most popular game around, and I was no exception to the craze. My dad had an iPad from work, and I
spent hours playing the Pocket Edition version of the game. Although it was fun, it lacked many features from the PC version, which I really
wanted to try. When I eventually got a PC, I immediately became hooked on creating things—especially automated systems. While other players
were building houses, I was busy trying to automate every possible task using Redstone. Whether it was machines or farms, I loved figuring
out how to make things work on their own.</p>
<h2>Innotech Labs and My First Coding Experience</h2>
<p>In 2016, free coding workshops called Innotech Labs started in my town, led by Konrad Strzelecki. I signed up and really enjoyed the
experience. The labs were held every weekend morning, and I found myself waiting the whole week for them. Each session, Konrad brought a big
pack of cookies, and we worked on a variety of cool projects. We played with drones, built Lego Mindstorms robots, coded in Scratch, and
even started with JavaScript—my first programming language.</p>
<p>The labs continued for a couple of years before they eventually stopped. After they ended, I asked for materials to continue learning on my
own, but eventually, I also took a break as I started high school and focused on other things.</p>
<h2>Rediscovering Coding</h2>
<p>A few years later, I came across a YouTube video that sparked my interest in coding again. The video showed how to send a bee script over
WhatsApp using Python, which was exciting for me because it involved automation—something I always enjoyed. I followed the tutorial,
downloaded Python, and successfully sent the script to a friend. The fun of making it work led me to explore more with automation.</p>
<p>I searched for more tutorials and found a video about creating a Piano Tiles bot using pyautogui, a Python library for automation. That
project opened up a new world for me. I spent a lot of time coding Python scripts for automating tasks, mostly involving screen scanning and
desktop apps. I even started exploring APIs and sending requests, which made me realize just how much coding could do.</p>
<h2>Finding My Path</h2>
<p>In high school, I studied to become an automation technician, but I soon realized that I wasn’t really interested in electronics and
physical systems. I knew I wanted to focus on software, so I decided to pursue Computer Science at Maastricht University.</p>
<p>Once I started my university studies, I began learning the fundamentals of programming and computer science in a structured way. While I had
played around with coding before, this was where I started learning real concepts. I explored topics like Java, static typing, computer
networks, data structures, and much more. Though I haven’t learned everything yet, I’m enjoying the process and feel like I’m building a
strong foundation.</p>
<p>Beyond just the coursework, I’ve started working on side projects that allow me to create useful tools and solutions. It's rewarding to see
something I’ve built help others, and that drives me to continue learning and improving my skills.</p>]]></content:encoded>
            <author>contact@jakmaz.com (Jakub Mazur)</author>
            <category>personal</category>
            <enclosure url="https://jakmaz.com/og/coding-journey.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>