<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Max Shen</title>
    <description>The latest articles on DEV Community by Max Shen (@m4xshen).</description>
    <link>https://dev.to/m4xshen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1016994%2F0bb1cf96-bebb-4497-94e0-9d819e643d23.png</url>
      <title>DEV Community: Max Shen</title>
      <link>https://dev.to/m4xshen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/m4xshen"/>
    <language>en</language>
    <item>
      <title>How I Fixed GitHub’s 14 Days Repo Traffic Graph</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Thu, 10 Jul 2025 13:17:30 +0000</pubDate>
      <link>https://dev.to/m4xshen/how-i-fixed-githubs-14-days-repo-traffic-graph-59ji</link>
      <guid>https://dev.to/m4xshen/how-i-fixed-githubs-14-days-repo-traffic-graph-59ji</guid>
      <description>&lt;h2&gt;
  
  
  Start of the journey
&lt;/h2&gt;

&lt;p&gt;If you are an open-source maintainer sharing projects on GitHub, you are probably familiar with Github’s repository traffic graph that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fioumy0m4e2zxl68djf7i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fioumy0m4e2zxl68djf7i.png" alt=" " width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first glance, this feature looks useful, but its limitation is clear: it only shows the past 14 days of your repo’s traffic data, making it hard to track long-term trends.&lt;/p&gt;

&lt;p&gt;While searching for solutions, I realized that many developers face similar challenges. This issue is widely discussed, particularly in a GitHub thread: &lt;a href="https://github.com/isaacs/github/issues/399" rel="noopener noreferrer"&gt;Track traffic to GitHub repo longer than 14 days #399&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exploring alternatives
&lt;/h2&gt;

&lt;p&gt;Within the discussion, I came across a GitHub action that fetches traffic data and stores it in a CSV file, also generating a PDF report:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh1ojfcdk8bay4cixszf6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh1ojfcdk8bay4cixszf6.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now I can view my traffic data more than 14 days, which is a significant improvement.&lt;/p&gt;

&lt;p&gt;However, its chart clarity and user interface is pretty bad, and it adds the complexity of setup and accessibility.&lt;/p&gt;

&lt;p&gt;This is because, as a GitHub action, I have to manually setup the workflow file. Also, the output being in PDF format made it less accessible and harder to interact with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building my own solution
&lt;/h2&gt;

&lt;p&gt;After reflecting on these experiences, I listed out what my ideal tool would look like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Data access extending beyond 14 days&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Intuitive and beautiful chart design&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simple setup&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I keep searching tools like this but none of them meets all the requirements. So I decided to build my own!&lt;/p&gt;

&lt;p&gt;After few months of working, I finally finished my own tool called &lt;a href="https://github.com/repohistory/repohistory" rel="noopener noreferrer"&gt;Repohistory&lt;/a&gt;. Here are some screenshots:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1n6bvfcv6etn93950dcf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1n6bvfcv6etn93950dcf.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F84glw9zp9dr2sygbnwhy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F84glw9zp9dr2sygbnwhy.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s the approach I took to fulfill all the requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Showing data more than 14 days
&lt;/h3&gt;

&lt;p&gt;It fetches traffic data with GitHub API and stores them in Supabase. This runs every day as a cron job to keep the data up to date.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intuitive &amp;amp; beautiful chart design
&lt;/h3&gt;

&lt;p&gt;I redesign the whole chart in a modern way and built them with shadcn/ui and Recharts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Easy to setup
&lt;/h3&gt;

&lt;p&gt;I integrates GitHub Apps into &lt;a href="https://github.com/repohistory/repohistory" rel="noopener noreferrer"&gt;Repohistory&lt;/a&gt;, so that user only need to sign in and select the repos they want to track, then it is good to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  More Features
&lt;/h2&gt;

&lt;p&gt;In additional to tracking repo traffic data, I also add some features to make Repohistory even more useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Star history chart
&lt;/h3&gt;

&lt;p&gt;The first one is the repo star history chart. It allows us to see the growth of this repo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuyiwm4e7kgna23suotoz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuyiwm4e7kgna23suotoz.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also add a button to generate sharable image which you can use inside your repo README.md:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffdfvpyedikq1s8cuxm3z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffdfvpyedikq1s8cuxm3z.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Release download chart
&lt;/h3&gt;

&lt;p&gt;The other feature is a chart to show your releases overtime with the total downlaod count on each. Here’s an example from neovim/neovim:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4nbrj3rn7twxr2bhh5r9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4nbrj3rn7twxr2bhh5r9.png" alt=" " width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Going forward
&lt;/h2&gt;

&lt;p&gt;Thank you for sticking with me on this ongoing journey of developing a tool to fix GitHub’s repo traffic graph! It’s been a challenging yet enjoyable experience.&lt;/p&gt;

&lt;p&gt;However the journey doesn’t end here. I’m continually working on developing and adding some exciting new features to Repohistory. If you’re interested in this evolving project, feel free to check it out and stay updated on &lt;a href="https://github.com/repohistory/repohistory" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>github</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>You might be overusing Vim visual mode 🤯</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sat, 22 Mar 2025 12:57:39 +0000</pubDate>
      <link>https://dev.to/m4xshen/you-might-be-overusing-vim-visual-mode-434h</link>
      <guid>https://dev.to/m4xshen/you-might-be-overusing-vim-visual-mode-434h</guid>
      <description>&lt;p&gt;When I started learning Vim few years ago, I read through a lot of blog posts and watched a lot of videos. I notice that most of the tutorials teach visual mode in a way that makes it seem like an important part of Vim.&lt;/p&gt;

&lt;p&gt;After using Vim for a while, I think this is a mistake. Visual mode is not as important as most people think. In fact, you might be overusing it.&lt;/p&gt;

&lt;p&gt;Let me explain why with some examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: Copying whole file
&lt;/h2&gt;

&lt;p&gt;Let's say you want to copy the whole file to system clipboard to ask ChatGPT for help. You might do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ggVG"+y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break it down:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;gg&lt;/code&gt; - Go to the beginning of the file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;V&lt;/code&gt; - Enter visual line mode.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;G&lt;/code&gt; - Go to the end of the file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"+y&lt;/code&gt; - Yank the selected text to system clipboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, you can actually achieve the same result with fewer keystrokes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gg"+yG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the breakdown:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;gg&lt;/code&gt; - Go to the beginning of the file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"+yG&lt;/code&gt; - Yank from the current position to the end of the file to system clipboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Example 2: Deleting current and previous line
&lt;/h2&gt;

&lt;p&gt;Let's say you want to delete current and previous line. You might do something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vkd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break it down:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;V&lt;/code&gt; - Enter visual line mode.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;k&lt;/code&gt; - Move up one line.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;d&lt;/code&gt; - Delete the selected lines.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, you can actually do it with fewer keystrokes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Different mindset
&lt;/h2&gt;

&lt;p&gt;In the above examples, the first editing approach is: &lt;code&gt;v/V&lt;/code&gt; + &lt;code&gt;motion&lt;/code&gt; + &lt;code&gt;operator&lt;/code&gt;. You select text, move the cursor to a specific location, and then do the operation.&lt;/p&gt;

&lt;p&gt;The second approach is: &lt;code&gt;operator&lt;/code&gt; + &lt;code&gt;motion&lt;/code&gt;. This means apply an operation from current cursor position to a specific location. In this way you don't need to enter visual mode at all.&lt;/p&gt;

&lt;p&gt;I think the reason why so many people stick to first approach is because when we use general text editors, we are used to selecting text with the mouse and then do the operation. So, when we switch to Vim, we try to do the same thing with visual mode.&lt;/p&gt;

&lt;p&gt;However, Vim is not a general text editor. It has a lot of powerful normal mode &lt;code&gt;motion&lt;/code&gt; that can help you achieve the same result without selecting text first.&lt;/p&gt;

&lt;p&gt;This is a different mindset, but once you get used to it, you will find that you can do things much faster in Vim!&lt;/p&gt;

&lt;h2&gt;
  
  
  You still need visual mode
&lt;/h2&gt;

&lt;p&gt;While I think you might be overusing visual mode, I'm not saying you should never use it.&lt;/p&gt;

&lt;p&gt;If you can't find a &lt;code&gt;motion&lt;/code&gt;  to achieve what you want, visual mode is a good fallback. Here are some situations where you actually need visual mode:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select text that is not selectable with simple &lt;code&gt;motion&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Select lines that are out of the current view.&lt;/li&gt;
&lt;li&gt;Visually confirm the text you are going to operate on.&lt;/li&gt;
&lt;li&gt;Use blockwise visual mode to operate on multiple lines at once.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bonus tip
&lt;/h2&gt;

&lt;p&gt;If you are too used to visual mode and find it hard to switch to the new mindset. You can try installing &lt;a href="https://github.com/m4xshen/hardtime.nvim" rel="noopener noreferrer"&gt;hardtime.nvim&lt;/a&gt;. It will notify you when there's a better way to do what you are trying to do with visual mode.&lt;/p&gt;

&lt;p&gt;For example, if you try to use &lt;code&gt;V5jd&lt;/code&gt;, it will show you a warning message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use d5j instead of V5jd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this way you can slowly get used to the new mindset!&lt;/p&gt;

</description>
      <category>vim</category>
      <category>neovim</category>
      <category>linux</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Half year reflections on building my first SaaS</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sat, 27 Jul 2024 12:04:55 +0000</pubDate>
      <link>https://dev.to/flowmo/half-year-reflections-on-building-my-first-saas-3ci5</link>
      <guid>https://dev.to/flowmo/half-year-reflections-on-building-my-first-saas-3ci5</guid>
      <description>&lt;p&gt;It's been around a half year since I started building &lt;a href="https://flowmo.io/blog/my-dream-productivity-app?utm_source=devto" rel="noopener noreferrer"&gt;my dream productivity app&lt;/a&gt;, Flowmodor. I think it's a good time to reflect on the journey and share some insights.&lt;/p&gt;

&lt;p&gt;Start with some interesting stats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MRR: $52 (from 11 paying users)&lt;/li&gt;
&lt;li&gt;Total registered users: 3700&lt;/li&gt;
&lt;li&gt;Average monthly active users: 300&lt;/li&gt;
&lt;li&gt;Average monthly landing page visitors: 1000&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The overall trend is a slow but steady growth. Here are some reflections on the journey so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO and traffic
&lt;/h2&gt;

&lt;p&gt;On average, I get 90 new registered users per week, most of them come from organic search:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxj1z9bomw41zq1s8nq7s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxj1z9bomw41zq1s8nq7s.png" alt="traffic top sources chart" width="800" height="668"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I think that's because I have been refining the landing page SEO for &lt;code&gt;flowmodoro&lt;/code&gt; keyword. It's not a popular term yet so I successfully ranked the top for it.&lt;/p&gt;

&lt;p&gt;This is actually a good strategy for a new product. Instead of targeting popular keywords, choosing a niche keyword can help you rank faster and get more targeted users.&lt;/p&gt;

&lt;p&gt;In addition to the landing page, I also wrote some blog posts about the Flowtime Technique and Pomodoro Technique. However, the ranking of these posts are not that good. Therefore didn't bring much traffic:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0ishzgvynsrnc3944sf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd0ishzgvynsrnc3944sf.png" alt="entry pages chart" width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Paying users
&lt;/h2&gt;

&lt;p&gt;I switched from PayPal to Paddle for payment processing. It's a good decision because I can now accept more payment methods and the checkout process is smoother (same for DX). It also provides a better dashboard to track the revenue and users.&lt;/p&gt;

&lt;p&gt;Among the 11 paying users, 3 of them are on the yearly plan, which is actually suggested by one of the users. I didn't expect that people would pay for a yearly plan, but it turns out that some users prefer it.&lt;/p&gt;

&lt;p&gt;I also added a 14-day free trial for every new registered user. Letting users try the full features for free is a good way to convert them into paying users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product development
&lt;/h2&gt;

&lt;p&gt;Although I care about the traffic and revenue, I think the most important thing is still the product itself. It's the real value that I'm providing to users.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://flowmo.io/blog/my-dream-productivity-app#the-plan?utm_source=devto" rel="noopener noreferrer"&gt;3 MVP features&lt;/a&gt; are all implemented and even more polished. For example the task list now supports Todoist integration, and the focus report now has a yearly heatmap view.&lt;/p&gt;

&lt;p&gt;The completion of the MVP features also means that I'll soon have a public launch. I'm planning to launch it on Product Hunt next week. I hope it can bring more exposure and users. You can &lt;a href="https://www.producthunt.com/products/flowmodor" rel="noopener noreferrer"&gt;sign up here&lt;/a&gt; to get notified when it's live!&lt;/p&gt;

&lt;p&gt;Another important thing is that I've built a &lt;a href="https://app.flowmo.io/feedback" rel="noopener noreferrer"&gt;feedback board&lt;/a&gt; for users to submit feature requests and vote for them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjyo1cf9g0w3bta23h4i8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjyo1cf9g0w3bta23h4i8.png" alt="feedback board" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a great way to understand what users really need and prioritize the development. I've already implemented 3 features from the board and I'll start working on the most voted feature: a mobile app, after the public launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building Flowmodor is a challenging but enjoyable journey. This is my first time building a SaaS product and I've learned a lot from it. I'm grateful for all the Flowmodor users providing feedback and support. I'll keep improving the app and looking forward to the &lt;a href="https://www.producthunt.com/products/flowmodor" rel="noopener noreferrer"&gt;PH launch&lt;/a&gt; next week 💜&lt;/p&gt;



</description>
      <category>saas</category>
      <category>startup</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I grew my open-source project to 1k stars 🌟</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sun, 16 Jun 2024 11:09:17 +0000</pubDate>
      <link>https://dev.to/m4xshen/how-i-grew-my-open-source-project-to-1k-stars-3mk2</link>
      <guid>https://dev.to/m4xshen/how-i-grew-my-open-source-project-to-1k-stars-3mk2</guid>
      <description>&lt;p&gt;Recently, my open-source Neovim plugin &lt;a href="https://github.com/m4xshen/hardtime.nvim" rel="noopener noreferrer"&gt;hardtime.nvim&lt;/a&gt; reached 1k stars on GitHub!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnsad1d7umwi8jmcgasd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnsad1d7umwi8jmcgasd.png" alt="hardtime.nvim reaches 1k stars on GitHub" width="800" height="137"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I think it's a good time to share what I have learned about promoting an open-source project. I break down the process into three stages: preparation, promotion, and maintenance. Let's dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparation
&lt;/h2&gt;

&lt;p&gt;Before promoting your project, make sure it's ready for the public. Here are some things you should prepare:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. README
&lt;/h3&gt;

&lt;p&gt;README is the first thing people see when they visit your project. Make sure it's clear and concise. At a minimum, it should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-sentence description of the project&lt;/li&gt;
&lt;li&gt;Demo GIF or screenshot&lt;/li&gt;
&lt;li&gt;Features of this project&lt;/li&gt;
&lt;li&gt;Installation guide&lt;/li&gt;
&lt;li&gt;Usage guide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first two are the most important. They make people quickly understand what your project is about and how it looks like so that they are more likely to continue reading the rest of the README. Here's an example from my project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsj04z087v4sfyzhvnvmt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsj04z087v4sfyzhvnvmt.png" alt="README example" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the rest of the README, it might be difficult to write a good one from scratch. Don't worry! You can look at other popular projects in the same domain to get inspiration.&lt;/p&gt;

&lt;p&gt;For example, I am inspired a lot by the README of &lt;a href="https://github.com/folke/lazy.nvim" rel="noopener noreferrer"&gt;lazy.nvim&lt;/a&gt;. Learning from other's success is a good way to improve your project.&lt;/p&gt;

&lt;p&gt;It is also important to separate the content into sections using markdown headers. GitHub will automatically generate a table of content, which makes it easier for people to navigate:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggef61qtd5qlqg20xzbg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggef61qtd5qlqg20xzbg.png" alt="GitHub Repo Table of Content" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Contribution guide
&lt;/h3&gt;

&lt;p&gt;If you want people to contribute to your project, you need to provide a contribution guide. It should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to set up the development environment&lt;/li&gt;
&lt;li&gt;The coding style of this project&lt;/li&gt;
&lt;li&gt;The format of the commit message&lt;/li&gt;
&lt;li&gt;What issues are good for beginners&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1t46qjnji3a0zgyiejs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1t46qjnji3a0zgyiejs.png" alt="CONTRUBUTIN example" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Issue template
&lt;/h3&gt;

&lt;p&gt;When people start using your project, they might encounter bugs or have feature requests. To make it easier for them to report issues, you should provide an issue template.&lt;/p&gt;

&lt;p&gt;You can create an issue template by following &lt;a href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository" rel="noopener noreferrer"&gt;this GitHub docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This ensures users provide enough information for you to reproduce the issue and fix it, which makes your later maintenance work easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promotion
&lt;/h2&gt;

&lt;p&gt;After preparing your project, it's time to share it! Here are some strategies you can use:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Share with the community
&lt;/h3&gt;

&lt;p&gt;You can share your project on the platform where your target community is active. For example, the Neovim community is pretty active on Reddit, so I shared my project on the &lt;a href="https://www.reddit.com/r/neovim/comments/14jferq/hardtimenvim_a_neovim_plugin_helping_you/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button" rel="noopener noreferrer"&gt;r/neovim&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0y9sm2qv4iprtx6j17r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0y9sm2qv4iprtx6j17r.png" alt="Reddit post introducing hardtime.nvim" width="800" height="748"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it comes to sharing on social media, it is crucial to provide a visual element. People are more likely to click on a post with a GIF or image than a plain text post.&lt;/p&gt;

&lt;p&gt;For the detail explanation of your project, you can post it in the comment section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5brm970g9z4jk04z66p8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5brm970g9z4jk04z66p8.png" alt="Reddit comment introducing hardtime.nvim" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also remember to engage with the community. If someone asks a question or gives feedback no matter it's positive or negative, make sure to respond to them. This shows that you care about the users and are willing to improve the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Write articles
&lt;/h3&gt;

&lt;p&gt;When I say write articles, I don't mean you have to write a post about your project. Often, people are not interested in reading a post that is just a promotion of a project. Instead, you can write articles related to the problem your project solves and mention your project in the article.&lt;/p&gt;

&lt;p&gt;For example, I wrote an article about &lt;a href="https://m4xshen.dev/posts/vim-command-workflow" rel="noopener noreferrer"&gt;practical Vim command workflow&lt;/a&gt;. In the article, I share my experience of using Vim operators and motions and how they help me become more productive. At the end of the article, I mention hardtime.nvim as a tool to help establish good command workflow.&lt;/p&gt;

&lt;p&gt;In this way you provide value to the readers and promote your project at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Hacktoberfest
&lt;/h3&gt;

&lt;p&gt;Apart from actively promoting your project, you can also leverage events like &lt;a href="https://hacktoberfest.com" rel="noopener noreferrer"&gt;Hacktoberfest&lt;/a&gt; to attract contributors. Hacktoberfest is an annual event that encourages people to contribute to open source throughout October.&lt;/p&gt;

&lt;p&gt;By tagging your project with &lt;code&gt;hacktoberfest&lt;/code&gt; and providing issues that are good for beginners, you can attract more contributors to your project. Here is an example of the pull requests I received during Hacktoberfest:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvemnjpm5rhuum6pyuvs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvemnjpm5rhuum6pyuvs.png" alt="Hacktoberfest pull requests" width="800" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance
&lt;/h2&gt;

&lt;p&gt;Building an open-source project is not a one-time thing. You need to maintain it to keep it alive. User feedback, bug reports, and feature requests will keep coming in, and you need to handle them properly.&lt;/p&gt;

&lt;p&gt;Using issue labels can help you organize the issues. For example, you can use &lt;code&gt;bug&lt;/code&gt;, &lt;code&gt;enhancement&lt;/code&gt;, &lt;code&gt;good first issue&lt;/code&gt;, etc., to categorize the issues. This makes it easier for you to prioritize and work on them.&lt;/p&gt;

&lt;p&gt;After a while, your project might develop more features or even release a major version. You can then get back to the promotion stage and share the updates with the community.&lt;/p&gt;

&lt;p&gt;For example, after my first post about hardtime.nvim, I worked on the users' feedback and added some new features. I then wrote a new post few months later to share the updates:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcx2c1zza0m9a3k7x44hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcx2c1zza0m9a3k7x44hx.png" alt="Reddit post introducing update of hardtime.nvim" width="800" height="748"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just make sure the update are valuable to the users. You don't want to spam the community with minor updates that don't bring much value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That's pretty much it! I hope you find these tips helpful for promoting your open-source project. Remember, building an open-source project is not just about writing code. A good project is useless if no one knows about it. So don't be shy, share your project with the world!&lt;/p&gt;






&lt;p&gt;If you like this kind of content and want to see more of it in the future, consider following my X to get notified when I post new content!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/m4xshen" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;Follow on X&lt;/a&gt;
&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>github</category>
      <category>sideprojects</category>
      <category>marketing</category>
    </item>
    <item>
      <title>3 Vim commands for blazingly fast navigation between brackets ⚡</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sat, 24 Feb 2024 09:34:03 +0000</pubDate>
      <link>https://dev.to/m4xshen/3-vim-commands-for-blazingly-fast-navigation-between-brackets-55kc</link>
      <guid>https://dev.to/m4xshen/3-vim-commands-for-blazingly-fast-navigation-between-brackets-55kc</guid>
      <description>&lt;p&gt;There are often lots of brackets within a programming file. Therefore the efficiency of navigating between them becomes crucial for the overall productivity. I'll introduce you to three types of commands that allows you to navigate between brackets blazingly fast!&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;%&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Let's begin with an example first (the arrow pointing to your cursor position):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;( example )
↑
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;%&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;( example )
          ↑
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;%&lt;/code&gt; again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;( example )
↑
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This demonstrates that &lt;code&gt;%&lt;/code&gt; jumps between the matched brackets. However the actual behavior is more than that. &lt;code&gt;%&lt;/code&gt; find the next item in this line &lt;strong&gt;after or under the cursor&lt;/strong&gt; and jump to its match.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;%&lt;/code&gt; not only works on &lt;code&gt;()&lt;/code&gt;, it also works with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pairs: &lt;code&gt;()&lt;/code&gt; or &lt;code&gt;[]&lt;/code&gt; or &lt;code&gt;{}&lt;/code&gt; (this can be changed with the &lt;code&gt;'matchpairs'&lt;/code&gt; option)&lt;/li&gt;
&lt;li&gt;C-style comment: &lt;code&gt;/* */&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;HTML tag: &lt;code&gt;&amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's crucial to note the emphasis on &lt;strong&gt;after or under the cursor&lt;/strong&gt;, which means that you can use it even when bracket is not under the cursor.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;some text (( example )) some text
↑
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's say you want to go to the last &lt;code&gt;)&lt;/code&gt;. You might initially think to use &lt;code&gt;f(%&lt;/code&gt; in the following steps:&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;f(&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;some text (( example )) some text
          ↑
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;%&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;some text (( example )) some text
                      ↑
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However the &lt;code&gt;f(&lt;/code&gt; is unnecessary. You can achieve the same navigation with just &lt;code&gt;%&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is because of &lt;code&gt;%&lt;/code&gt; works like this under the hood:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds the first pair after or under the cursor which is &lt;code&gt;(&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Jump to its match which is &lt;code&gt;)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It might not be that intuitive to use &lt;code&gt;%&lt;/code&gt; when bracket not under the cursor, but after you get more familiar with it you can achieve some magic movement with it!&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;[(&lt;/code&gt; and &lt;code&gt;[{&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[(&lt;/code&gt; jumps backward to the first unmatched &lt;code&gt;(&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[{&lt;/code&gt; jumps backward to the first unmatched &lt;code&gt;{&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{

  example

  text
  ↑
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;[{&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
↑
  example

  text

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to &lt;code&gt;[(&lt;/code&gt; and &lt;code&gt;[{&lt;/code&gt;, there's also &lt;code&gt;])&lt;/code&gt; and &lt;code&gt;]}&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;])&lt;/code&gt; jumps forward to unmatched &lt;code&gt;)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;]}&lt;/code&gt; jumps forward to unmatched &lt;code&gt;}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's see a practical example. Imagine you're navigating inside a large function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;example&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="err"&gt;↑&lt;/span&gt; 
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// some other operation, which takes many lines&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's say you want to jump to the closing &lt;code&gt;}&lt;/code&gt; of that function. There are several ways to do this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Use relative jump such as &lt;code&gt;12j&lt;/code&gt; to jump to the &lt;code&gt;}&lt;/code&gt; directly:&lt;br&gt;
This approach works for smaller functions. However in the example this is impossible because the function is too big and the last line of the function is outside the screen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;code&gt;/}&lt;/code&gt; to search the &lt;code&gt;}&lt;/code&gt;:&lt;br&gt;
This might work if there's no other &lt;code&gt;}&lt;/code&gt; inside the function. However in the example there's other &lt;code&gt;}&lt;/code&gt;, and you need to press &lt;code&gt;n&lt;/code&gt; multiple times to get to the last &lt;code&gt;}&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use &lt;code&gt;]}&lt;/code&gt;:&lt;br&gt;
The most efficient method is to use &lt;code&gt;]}&lt;/code&gt;, which directly takes you to the closing &lt;code&gt;}&lt;/code&gt; of the function without the need for counting lines or repeated searching.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;][&lt;/code&gt; and &lt;code&gt;[]&lt;/code&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;][&lt;/code&gt;: jump forward to the next &lt;code&gt;}&lt;/code&gt; &lt;strong&gt;in the first column&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[]&lt;/code&gt;: jump backward to the next &lt;code&gt;}&lt;/code&gt; &lt;strong&gt;in the first column&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
↑ example
}

{
  {
    example
  }
}

{
  example
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;][&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  example
}
↑
{
  {
    example
  }
}

{
  example
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;][&lt;/code&gt; again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  example
}

{
  {
    example
  }
}
↑
{
  example
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;][&lt;/code&gt; again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  example
}

{
  {
    example
  }
}

{
  example
}
↑
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when in files containing multiple functions. In most languages, the end of a function is a &lt;code&gt;}&lt;/code&gt; at the start of a line (first column). You can then use &lt;code&gt;][&lt;/code&gt; to jump to the end of next function and &lt;code&gt;[]&lt;/code&gt; to jump to the end of previous function.&lt;/p&gt;

&lt;p&gt;There's also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;]]&lt;/code&gt;: jump forward to the next &lt;code&gt;{&lt;/code&gt; in the first column.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[[&lt;/code&gt;: jump backward to the next &lt;code&gt;{&lt;/code&gt; in the first column.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Though in practice, the &lt;code&gt;{&lt;/code&gt; character appearing in the first column is less common. I personally rarely find an occasion to use them. If you can think of scenarios where &lt;code&gt;]]&lt;/code&gt; and &lt;code&gt;[[&lt;/code&gt; would be particularly useful, please share them in the comments below!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In conclusion, mastering Vim commands such as &lt;code&gt;%&lt;/code&gt;, &lt;code&gt;[(&lt;/code&gt;, &lt;code&gt;[{&lt;/code&gt;, &lt;code&gt;][&lt;/code&gt;, and &lt;code&gt;[]&lt;/code&gt; boosts navigation efficiency in programming files. Each command offers unique advantages under different situation, enabling us to move through code with precision and ease!&lt;/p&gt;

</description>
      <category>vim</category>
      <category>neovim</category>
      <category>productivity</category>
      <category>linux</category>
    </item>
    <item>
      <title>Understand OAuth 2.0 code grant flow 🔐</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sat, 17 Feb 2024 12:57:02 +0000</pubDate>
      <link>https://dev.to/m4xshen/understand-oauth-20-code-grant-flow-3lnj</link>
      <guid>https://dev.to/m4xshen/understand-oauth-20-code-grant-flow-3lnj</guid>
      <description>&lt;p&gt;If you're building a web application that uses a third-party API, you're likely using the OAuth 2.0 code grant to obtain user permissions. However, its process might not be entirely clear to you. In this post, I'll demystify the process and guide you through the entire OAuth 2.0 code grant flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up your application
&lt;/h2&gt;

&lt;p&gt;Before you start working with OAuth 2.0, you first need to sign up your application on the service provider's website. Take Google's APIs as an example; you must register on the Google API Console.&lt;/p&gt;

&lt;p&gt;You'll be asked for some simple details about your application, like its name, website, and logo. You'll also need to provide a &lt;code&gt;redirect_uri&lt;/code&gt;, which I'll explain in more detail later.&lt;/p&gt;

&lt;p&gt;After this, you'll get a &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt;. These are important because you'll use them in the OAuth 2.0 process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete OAuth 2.0 code grant flow
&lt;/h2&gt;

&lt;p&gt;Once you've set up your application, you're ready to start using OAuth 2.0. Here's what the complete process looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request authorization and get authorization code.&lt;/li&gt;
&lt;li&gt;Exchange authorization code for access token.&lt;/li&gt;
&lt;li&gt;Use access token to get user resource through API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's dive into each step, using Google OAuth 2.0 as example.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Request authorization and get authorization code
&lt;/h3&gt;

&lt;p&gt;The first step of the web flow is to request authorization from the user. This is accomplished by creating an authorization request link (a GET request) for the user to click on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep9moo2x7jj0at3202gw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep9moo2x7jj0at3202gw.png" alt="authorization request link" width="800" height="384"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET https://example-service.com/login/oauth/authorize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This endpoint usually takes the following query parameters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;client_id&lt;/code&gt;: This is your app's unique identifier, which you receive after registering your app with the service.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;response_type&lt;/code&gt;: This should be set to &lt;code&gt;code&lt;/code&gt;, indicating that you're asking for an authorization code in response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;state&lt;/code&gt;: This acts as a security measure and should be a random value for each request. When the user is redirected back to your app, you should verify that the state value matches what you originally sent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redirect_uri&lt;/code&gt; (optional): The URI where you want the user to be redirected after the authorization is complete. This must match the redirect URI that you have previously registered with the service.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scope&lt;/code&gt; (optional): Here, you can ask for different levels of access by including one or more scope values (separated by spaces). The specific values depend on the service you're using.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Following the link leads the user to a sign in page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvl8m617sy77cfb0srle0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvl8m617sy77cfb0srle0.png" alt="signin page" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the user signs in, the service presents a page detailing your request, including the name of your application, what access you're asking for, etc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftu34bndz9tx7wy12d72u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftu34bndz9tx7wy12d72u.png" alt="signin confirm page" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the user agrees to proceed by clicking &lt;strong&gt;Continue&lt;/strong&gt;, the service redirects them back to &lt;code&gt;redirect_uri&lt;/code&gt;, including &lt;code&gt;code&lt;/code&gt; (authorization code) and the same &lt;code&gt;state&lt;/code&gt; parameter you specified, in the query string.&lt;/p&gt;

&lt;p&gt;Here's a flowchart that outlines this first step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5us7tyqnp2qid863m0s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5us7tyqnp2qid863m0s.png" alt="step 1 flowchart" width="800" height="767"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Exchange authorization code for access token
&lt;/h3&gt;

&lt;p&gt;The second phase involves exchanging the &lt;code&gt;code&lt;/code&gt; you've received for an &lt;code&gt;access_token&lt;/code&gt;. This is done by making a POST request to the service's authorization server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://example-service.com/login/oauth/access_token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This endpoint usually takes the following query parameters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;grant_type&lt;/code&gt;: This should be set to &lt;code&gt;authorization_code&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;code&lt;/code&gt;: The authorization code that you received.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;client_id&lt;/code&gt;: The unique identifier for your app, provided when you registered with the service.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;client_secret&lt;/code&gt;: A secret key you received upon registering your app, meant to authenticate your request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redirect_uri&lt;/code&gt; (optional): If you included a redirect URI in the initial authorization request, it must also be included here and match exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The service's response to this POST request will include the &lt;code&gt;access_token&lt;/code&gt; you'll use in the final step.&lt;/p&gt;

&lt;p&gt;Here's a flowchart summarizing this step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawp48mcxako1htj5zeag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fawp48mcxako1htj5zeag.png" alt="step 2 flowchart" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use access token to get user resource through API
&lt;/h3&gt;

&lt;p&gt;Once you have the &lt;code&gt;access_token&lt;/code&gt;, you might save it for later to make API calls. Include it in your request headers like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This informs the resource server that you are authorized to access the user's information.&lt;/p&gt;

&lt;p&gt;Here's a flowchart illustrating this step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiu2rxjcw28kcnydquxql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiu2rxjcw28kcnydquxql.png" alt="step 3 diagram" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that you understand the basic OAuth 2.0 code grant flow, let's review everything with a diagram showing the full process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F583cc97g6e1iwd52uayc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F583cc97g6e1iwd52uayc.png" alt="complete diagram" width="800" height="941"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  PKCE
&lt;/h2&gt;

&lt;p&gt;The basic authorization flow works well, but it has a vulnerability: if an attacker manages to intercept the &lt;code&gt;code&lt;/code&gt;, they can exchange it for an &lt;code&gt;access_token&lt;/code&gt;. This poses a serious security risk, potentially allowing the attacker to access protected resources as the user.&lt;/p&gt;

&lt;p&gt;This is where PKCE (Proof Key for Code Exchange) comes into play. PKCE is an extension to the basic authorization flow to prevent this kind of attack.&lt;/p&gt;

&lt;p&gt;To implement PKCE, the application must first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;creates a &lt;code&gt;code_verifier&lt;/code&gt;. This is a cryptographically random string.&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;code_verifier&lt;/code&gt; to generate the &lt;code&gt;code_challenge&lt;/code&gt; by performing a SHA256 hash. If the application cannot perform a SHA256 hash, it should use the plain &lt;code&gt;code_verifier&lt;/code&gt; as the &lt;code&gt;code_challenge&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next, you'll include additional parameters in the previous steps to integrate PKCE:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Request authorization and get authorization code
&lt;/h3&gt;

&lt;p&gt;When requesting authorization, include these additional parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;code_challenge=XXXXXXXXX&lt;/code&gt;: The code challenge generated as previously described&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;code_challenge_method=S256&lt;/code&gt;: This can be either &lt;code&gt;plain&lt;/code&gt; or &lt;code&gt;S256&lt;/code&gt;, indicating whether the challenge is the plain &lt;code&gt;code_verifier&lt;/code&gt; string or a SHA256 hash of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The authorization server will associate &lt;code&gt;code_challenge&lt;/code&gt; and &lt;code&gt;code_challenge_method&lt;/code&gt; with the &lt;code&gt;code&lt;/code&gt; it generates.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Exchange authorization code for access token
&lt;/h3&gt;

&lt;p&gt;When exchanging the &lt;code&gt;code&lt;/code&gt; for an &lt;code&gt;access_token&lt;/code&gt;, add the following parameter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;code_verifier&lt;/code&gt;: The code verifier for the PKCE request, that the app originally generated before the authorization request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the &lt;code&gt;code_challenge&lt;/code&gt; and &lt;code&gt;code_challenge_method&lt;/code&gt; were associated with the &lt;code&gt;code&lt;/code&gt; initially, the server should already know which method to use to verify the &lt;code&gt;code_verifier&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the &lt;code&gt;plain&lt;/code&gt; method, the authorization server simply verifies that the provided &lt;code&gt;code_verifier&lt;/code&gt; matches the previously stored &lt;code&gt;code_challenge&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For the &lt;code&gt;S256&lt;/code&gt; method, the authorization server will hash the provided &lt;code&gt;code_verifier&lt;/code&gt; using SHA256 and then compare it to the stored &lt;code&gt;code_challenge&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the verification is successful and the values match, the server issues an &lt;code&gt;access_token&lt;/code&gt;. If they don't match, the request is denied.&lt;/p&gt;

&lt;p&gt;This mechanism ensures that even if an attacker intercepts the &lt;code&gt;code&lt;/code&gt;, they cannot exchange it for an &lt;code&gt;access_token&lt;/code&gt; without the &lt;code&gt;code_verifier&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In summary, the OAuth 2.0 code grant flow is a crucial process for you to understand and implement when building web applications that require access to user data from third-party services. By following the outlined steps and incorporating PKCE, you can securely authenticate users and access necessary resources while protecting against common security vulnerabilities. &lt;/p&gt;






</description>
      <category>webdev</category>
      <category>oauth</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>Tailwind CSS under the hood</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sat, 10 Feb 2024 14:00:07 +0000</pubDate>
      <link>https://dev.to/m4xshen/tailwind-css-under-the-hood-m1o</link>
      <guid>https://dev.to/m4xshen/tailwind-css-under-the-hood-m1o</guid>
      <description>&lt;p&gt;Tailwind CSS defines itself on its official site as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A utility-first CSS framework packed with classes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this post I'll explain to you what does utility-first mean and how Tailwind CSS works under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Utility-first
&lt;/h2&gt;

&lt;p&gt;Utility classes are easily understood, single purpose classes. For example, Tailwind provides utility classes such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.mx-auto&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;margin-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.text-sm&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.875rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So when you want to horizontally center a paragraph with small font size, you can just use &lt;code&gt;mx-auto text-sm&lt;/code&gt; as the class name of the element.&lt;/p&gt;

&lt;p&gt;In this way, we can build complex components by combining different utility classes together.&lt;/p&gt;

&lt;h2&gt;
  
  
  3 main benefits of utility-first
&lt;/h2&gt;

&lt;p&gt;Now we understand what is utility-first and let’s see the benefits of it:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Design with constraints
&lt;/h3&gt;

&lt;p&gt;Using inline styles, every value is an arbitrary number. With utilities, you’re choosing styles from a predefined design system such as the default spacing scale:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgab2y3r6vmmbkl1d16k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgab2y3r6vmmbkl1d16k.png" alt="default spacing scale" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Take setting width for an example, you just use utility class such as &lt;code&gt;w-48&lt;/code&gt; or &lt;code&gt;w-96&lt;/code&gt;, instead of specify the exact pixel you want. This makes it much easier to build visually consistent UIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. CSS stops growing
&lt;/h3&gt;

&lt;p&gt;Using a traditional approach, your CSS files get bigger every time you add a new UI component. With utilities, everything is reusable. This means the size of your CSS file stops growing after a certain point because the same utility classes are used again and again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9032akgqdcq696wqfkud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9032akgqdcq696wqfkud.png" alt="traditional css vs utility css" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No need to invent class names
&lt;/h3&gt;

&lt;p&gt;With traditional CSS, you might be adding class names like &lt;code&gt;sidebar-inner-wrapper&lt;/code&gt; just to be able to style a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. With utilities, you just composing them to build a complex style you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Tailwind CSS works under the hood
&lt;/h2&gt;

&lt;p&gt;Tailwind CSS works by scanning all of files for class names at build time, then generating all of the corresponding CSS for those styles.&lt;/p&gt;

&lt;p&gt;The paths to all of your content files is specified by &lt;code&gt;content&lt;/code&gt; section of your &lt;code&gt;tailwind.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** @type {import('tailwindcss').Config} */&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./pages/**/*.{html,js}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/**/*.{html,js}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tailwind use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions" rel="noopener noreferrer"&gt;regular expression&lt;/a&gt; to detect class names. Therefore when you want to use utility class conditionally like the following syntax doesn’t work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`text-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;green&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-600`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Lorem Ipsum&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is because the regular expression doesn’t match any class name.&lt;/p&gt;

&lt;p&gt;Instead, you should use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-red-600&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-green-600&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Lorem Ipsum&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the second approach, Tailwind can match the string &lt;code&gt;'text-red-600'&lt;/code&gt; and &lt;code&gt;'text-green-600'&lt;/code&gt; and then generates the corresponding CSS.&lt;/p&gt;

&lt;p&gt;This also explains why you can't use props to build class names. For example, this won't work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`bg-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-600 hover:bg-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-500 ...`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, map props to complete class names that are statically detectable at build-time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;colorVariants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-blue-600 hover:bg-blue-500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;red&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bg-red-600 hover:bg-red-500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;colorVariants&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; ...`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;To sum up, we've learned that Tailwind CSS makes it easy to design neat and consistent websites. Knowing how it works also helps us understand why some things work well and others don't, guiding us to use Tailwind more effectively!&lt;/p&gt;



</description>
      <category>webdev</category>
      <category>css</category>
      <category>tailwindcss</category>
      <category>react</category>
    </item>
    <item>
      <title>5 things to do during Pomodoro break without electronic devices</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sun, 28 Jan 2024 13:04:08 +0000</pubDate>
      <link>https://dev.to/flowmo/5-things-to-do-during-pomodoro-break-without-electronic-devices-303i</link>
      <guid>https://dev.to/flowmo/5-things-to-do-during-pomodoro-break-without-electronic-devices-303i</guid>
      <description>&lt;p&gt;When practicing productivity techniques like the Pomodoro or &lt;a href="https://flowmo.io/blog/flowtime-technique?utm_source=devto" rel="noopener noreferrer"&gt;Flowmodoro&lt;/a&gt; methods, it's easy to dismiss the importance of breaks.&lt;/p&gt;

&lt;p&gt;During these breaks, it's tempting to immediately reach for your phone and spend the entire time scrolling, rationalizing it as a reward for your prior concentration.&lt;/p&gt;

&lt;p&gt;However, this habit not only makes it challenging to regain focus for the next session but also denies your eyes the rest they need. Therefore, I'll introduce five activities you can engage in during your breaks, all without the use of electronic devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stretch your body 🤸
&lt;/h2&gt;

&lt;p&gt;Stretching during breaks is not just a way to relieve physical tension but also to clear your mind. The nature of programming often demands extended durations of seated concentration, which can lead to physical rigidity. Simple stretching exercises can relieve muscle tension and also provide a mental reset, preparing you for the next phase of intense focus.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Go for a walk 🚶
&lt;/h2&gt;

&lt;p&gt;A short walk is an excellent way to disconnect from your work. It's not just about the physical movement but also about changing your environment. Walking away from your workspace allows you to return with a fresh perspective. Whether it's a stroll around your office, a quick venture into your backyard, or just pacing in your room, the act of walking helps in resetting your mind, making you more creative and productive when you return.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Look off into the distance 🔭
&lt;/h2&gt;

&lt;p&gt;Taking a quick break to look away from your computer screen is simple but really helpful. When you've been staring at code for a while, it's good to give your eyes a rest. Try to find a spot where you can look out a window. Focus on something far away like the sky, trees, or distant buildings. This isn't just about resting your eyes; it's also a quiet moment for your mind. As you look away, take a few deep breaths. This easy break can help you feel more relaxed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Organize your work desk 🧹
&lt;/h2&gt;

&lt;p&gt;A cluttered desk can lead to a cluttered mind. Use your break to organize your workspace. Put away unnecessary items, arrange your documents, and clear any trash. A clean and organized environment can significantly boost your productivity and reduce stress levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Fill your water bottle &amp;amp; visit the restroom 💧
&lt;/h2&gt;

&lt;p&gt;Hydration is key to maintaining high energy levels and focus. Use your break to refill your water bottle. It's a simple act, but it ensures that you stay hydrated, especially important if you're so engrossed in your work that you forget to drink water. Additionally, a regular visit to the restroom is also important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;In short, taking breaks is key to doing well at work or study. Using breaks wisely, by doing things like stretching, walking, looking into the distance, tidying up your desk, or getting water, isn't just a rest from work. It's a way to make sure you come back to your tasks with more focus and energy. &lt;/p&gt;

&lt;p&gt;These simple actions during breaks can help you feel better and work better. So, remember to step back, take a good break, and see the difference it makes in your day!&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>learning</category>
      <category>pomodoro</category>
    </item>
    <item>
      <title>Why pomodoro doesn't work? Try this alternative 🍅</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sun, 14 Jan 2024 12:00:57 +0000</pubDate>
      <link>https://dev.to/flowmo/why-pomodoro-doesnt-work-try-this-alternative-2no9</link>
      <guid>https://dev.to/flowmo/why-pomodoro-doesnt-work-try-this-alternative-2no9</guid>
      <description>&lt;h2&gt;
  
  
  What's Pomodoro
&lt;/h2&gt;

&lt;p&gt;The Pomodoro Technique, developed by Francesco Cirillo, is a time management method that uses a timer to break work into intervals, traditionally 25 minutes in length, separated by 5-minutes short breaks.&lt;/p&gt;

&lt;p&gt;These intervals are known as "pomodoros", named after the tomato-shaped kitchen timer that Cirillo used as a university student.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Pomodoro might not work for developers
&lt;/h2&gt;

&lt;p&gt;While the Pomodoro Technique is popular, it might not be the best fit for everyone, especially developers. Here’s why:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Interruption of Flow State&lt;/strong&gt;: The rigid timing can disrupt the deep "flow state" crucial for coding. When you're deeply engrossed in a complex problem, stopping because a timer goes off can break your train of thought.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Variable Task Length&lt;/strong&gt;: Coding tasks vary in complexity and often don't neatly fit into 25-minute intervals. Some tasks might require prolonged uninterrupted focus, while others are shorter and more straightforward.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context Switching&lt;/strong&gt;: Frequent breaks mandated by the Pomodoro Technique can lead to excessive context switching. This is counterproductive for tasks that require sustained concentration and a deep understanding of the problem at hand.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The better alternative - Flowmodoro
&lt;/h2&gt;

&lt;p&gt;The Flowtime Technique a.k.a. Flowmodoro was created by &lt;a href="https://medium.com/@UrgentPigeon/the-flowtime-technique-7685101bd191" rel="noopener noreferrer"&gt;Zoë Read-Bivens&lt;/a&gt; as a solution to Pomodoro's main problem.&lt;/p&gt;

&lt;p&gt;Unlike the Pomodoro Technique, Flowmodoro counts up instead of counting down. It allows you to focus until you naturally feel the need for a break. Then, when you decide to rest, you simply stop the timer, divide the focus time by 5, and set a countdown timer for your break.&lt;/p&gt;

&lt;p&gt;This method respects your flow state and adapts to the variable nature of coding tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to implement Flowmodoro
&lt;/h2&gt;

&lt;p&gt;Implementing Flowmodoro is simple and can start with tools as basic as a stopwatch and timer app. Here's a basic guide:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick One Task&lt;/strong&gt;: Begin by selecting a single task to focus on. This ensures that your attention isn't divided across multiple tasks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start the work&lt;/strong&gt;: Once your task is chosen, start the stopwatch. This marks the beginning of your focused work period. Dive into your task without any distractions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stop the work&lt;/strong&gt;: Keep working until you naturally feel the need for a break. This could be when you feel your concentration waning or you've reached a logical stopping point in your task. Then, stop the stopwatch. This time recorded is your focused work duration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Take a break&lt;/strong&gt;: Calculate your break time as one-fifth of your focused work duration. For instance, if you worked for 50 minutes, take a 10-minute break. Set a countdown timer for this break period. This ratio ensures that you get adequate rest without losing the momentum of your work.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can repeat this cycle again and again. &lt;/p&gt;

&lt;h2&gt;
  
  
  Automate the process
&lt;/h2&gt;

&lt;p&gt;I have been using Flowmodoro and it really help me improve my productivity while coding. However, I noticed one minor drawback: the repetitive process of setting up the timer manually each time.&lt;/p&gt;

&lt;p&gt;To address this, I'm currently working on a solution that seamlessly integrates with this workflow. That's where &lt;a href="https://flowmo.io?utm_source=devto" rel="noopener noreferrer"&gt;Flowmo&lt;/a&gt; comes in – a web app I am creating to automate and refine the Flowmodoro process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.flowmo.io" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;Get Started&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;As developers, our work requires flexibility and adaptation. Flowmodoro is designed with this in mind. Let's embrace our peak flow states with Flowmodoro!&lt;/p&gt;



</description>
      <category>productivity</category>
      <category>pomodoro</category>
      <category>career</category>
      <category>flowmodoro</category>
    </item>
    <item>
      <title>Build a Neovim plugin in Lua 🌙</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Tue, 10 Oct 2023 11:08:35 +0000</pubDate>
      <link>https://dev.to/m4xshen/building-a-neovim-plugin-in-lua-54j9</link>
      <guid>https://dev.to/m4xshen/building-a-neovim-plugin-in-lua-54j9</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In this article, you'll learn how to develop a Neovim plugin in Lua, understand the structure of Neovim plugin, Lua module, and create a simple plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let’s set it up 🔥
&lt;/h2&gt;

&lt;p&gt;Before developing the plugin, we need to set up the project. There are two different approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;setup with GitHub repository&lt;/li&gt;
&lt;li&gt;setup locally&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Setup with GitHub repository
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a new repository on GitHub (e.g.: example.nvim)&lt;/li&gt;
&lt;li&gt;Install it with a plugin manager
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- lazy.nvim&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="s2"&gt;"m4xshen/example.nvim"&lt;/span&gt; &lt;span class="c1"&gt;-- replace this with your {user_name}/{repo_name}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;cd&lt;/code&gt; to the directory where the plugin is installed
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.local/share/nvim/lazy/example.nvim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setup locally
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a new directory on your local machine
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; ~/example.nvim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Install it with a plugin manager
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- lazy.nvim&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~/example.nvim"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;cd&lt;/code&gt; to the directory
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/example.nvim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Plugin structure 🏗️
&lt;/h2&gt;

&lt;p&gt;Now the plugin is setup and installed. Let's learn about its folder structure.&lt;/p&gt;

&lt;p&gt;At the root of your plugin, create a structure like this (replace the &lt;code&gt;example&lt;/code&gt; with your plugin name):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.nvim/
├── lua
│   └── example
│       └── init.lua
└── plugin
    └── example.lua
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;plugin/&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The files inside &lt;code&gt;plugin/&lt;/code&gt; folder will be executed when Neovim starts. Try to add the following line to &lt;code&gt;example.lua&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"plugin/example.lua is executed!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open a new Neovim instance after that, you'll see the string printed because Neovim runs the Lua code of the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;lua/&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Most of the time you don't want the plugin to execute everything at startup. You want it to run some functions when events happened, command called, etc. and organize the code in a structured way. That's where Lua module comes into play.&lt;/p&gt;

&lt;p&gt;A Lua module is a regular Lua table that is used to contain functions and data. The table is declared local not to pollute the global scope. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;-- M stands for module, a naming convention&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;M&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to load Lua module on demand, you can place them inside the &lt;code&gt;lua/&lt;/code&gt; directory in your &lt;code&gt;'runtimepath'&lt;/code&gt; and load them with &lt;code&gt;require&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rules of &lt;code&gt;require&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Here are the rules when &lt;code&gt;require&lt;/code&gt; finding files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any &lt;code&gt;.&lt;/code&gt; in the module name is treated as a directory separator when searching.&lt;/li&gt;
&lt;li&gt;When the file with module name is not searched, it then searches for &lt;code&gt;init.lua&lt;/code&gt; inside the folder with module name.&lt;/li&gt;
&lt;li&gt;You don't need to type the &lt;code&gt;.lua&lt;/code&gt; extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: For a module &lt;code&gt;foo.bar&lt;/code&gt;, each directory inside &lt;code&gt;'runtimepath'&lt;/code&gt; is searched for &lt;code&gt;lua/foo/bar.lua&lt;/code&gt;, then &lt;code&gt;lua/foo/bar/init.lua&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So if you put the code above into &lt;code&gt;lua/example/init.lua&lt;/code&gt;, you can run &lt;code&gt;:lua require("example").setup()&lt;/code&gt; to print hello. That is because the plugin manager added the folder of the plugins into &lt;code&gt;'runtimepath'&lt;/code&gt; for you. Therefore the &lt;code&gt;require&lt;/code&gt; can find this file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 💡
&lt;/h2&gt;

&lt;p&gt;After understanding the structure of Neovim plugin, let's finish the example.nvim! This plugin does 2 simple things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It maps the &lt;code&gt;&amp;lt;Leader&amp;gt;h&lt;/code&gt; to print &lt;code&gt;hello&lt;/code&gt; to the user.&lt;/li&gt;
&lt;li&gt;If user specify their name when setting up plugins, it prints &lt;code&gt;hello, {user_name}&lt;/code&gt; instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the &lt;code&gt;lua/example/init.lua&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;M&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

   &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;Leader&amp;gt;h"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
         &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hello, "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
         &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to call &lt;code&gt;setup&lt;/code&gt; after installing plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"example"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you are using lazy.nvim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="s2"&gt;"m4xshen/example.nvim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that we use &lt;code&gt;vim.keymap.set&lt;/code&gt; which is only available after Neovim 0.7.0. We need to add a version checker and it should be executed automatically at startup, so it should be put inside &lt;code&gt;plugin/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Here's the &lt;code&gt;plugin/example.lua&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"nvim-0.7.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
   &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nvim_err_writeln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Example.nvim requires at least nvim-0.7.0."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that this simple plugin is finished. Now try pressing &lt;code&gt;&amp;lt;Leader&amp;gt;h&lt;/code&gt; and the greeting message should be printed! You can also set your name inside &lt;code&gt;setup&lt;/code&gt; function so that it prints greeting message with name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"example"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
   &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Max"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you are using lazy.nvim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="s2"&gt;"m4xshen/example.nvim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Max"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the &lt;a href="https://github.com/m4xshen/example.nvim" rel="noopener noreferrer"&gt;complete source code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Words 🚀
&lt;/h2&gt;

&lt;p&gt;Getting started is the hardest part when developing a Neovim plugin first time. I hope this tutorial helps you out. If you have any questions, feel free to ask in the comment section.&lt;/p&gt;




&lt;p&gt;That’s a wrap. Thanks for reading. If you like this kind of stuff, consider following for more.&lt;/p&gt;

&lt;p&gt;You can also see what I am working on my &lt;a href="https://m4xshen.dev" rel="noopener noreferrer"&gt;Personal Website&lt;/a&gt; and &lt;a href="https://github.com/m4xshen" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>neovim</category>
      <category>vim</category>
      <category>editor</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Setup Next.js with Airbnb ESLint, Prettier, TypeScript and Tailwind CSS</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sun, 10 Sep 2023 13:40:03 +0000</pubDate>
      <link>https://dev.to/m4xshen/setup-nextjs-with-airbnb-eslint-prettier-typescript-and-tailwind-css-273d</link>
      <guid>https://dev.to/m4xshen/setup-nextjs-with-airbnb-eslint-prettier-typescript-and-tailwind-css-273d</guid>
      <description>&lt;p&gt;In this tutorial you'll learn how to setup a Next.js project with Airbnb ESLint, Prettier, Typescript and Tailwind CSS in a correct way so that you don't need to turn off a lot of rules inside &lt;code&gt;.eslintrc.json&lt;/code&gt; anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create Project
&lt;/h2&gt;

&lt;p&gt;I recommend starting a new Next.js app using &lt;code&gt;create-next-app&lt;/code&gt;, which sets up everything automatically for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx create-next-app@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to select &lt;code&gt;Yes&lt;/code&gt; for these 3 prompts to have a basic setup for TypeScript, ESLint and Tailwind CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prettier
&lt;/h2&gt;

&lt;p&gt;Prettier is an code formatter. You can install it with your package manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add --dev --exact prettier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install --save-dev --save-exact prettier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, create a config file named &lt;code&gt;.prettierrc.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Check out all possible config &lt;a href="https://prettier.io/docs/en/options" rel="noopener noreferrer"&gt;options&lt;/a&gt;. If you don't need to set any options, just leave a &lt;code&gt;{}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"singleQuote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point you can run the Prettier and it should works!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn prettier . --write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx prettier . --write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also setup Prettier for your editor so that you can run Prettier efficiently with keymap. See &lt;a href="https://prettier.io/docs/en/editors" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tailwind CSS
&lt;/h2&gt;

&lt;p&gt;There's an official Prettier plugin for Tailwind CSS that scans your files for class attributes containing Tailwind CSS classes, and then sorts those classes automatically following the &lt;a href="https://tailwindcss.com/blog/automatic-class-sorting-with-prettier#how-classes-are-sorted" rel="noopener noreferrer"&gt;recommended class order&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Install the plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add --dev prettier prettier-plugin-tailwindcss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install --save-dev prettier prettier-plugin-tailwindcss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following line into the prettier config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"plugins": ["prettier-plugin-tailwindcss"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the example config becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"singleQuote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"prettier-plugin-tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run Prettier now, the Tailwind CSS classes name will be sorted!&lt;/p&gt;

&lt;h2&gt;
  
  
  ESLint
&lt;/h2&gt;

&lt;p&gt;ESLint is a tool for identifying and reporting on patterns found in ECMAScript code, with the goal of making code more consistent and avoiding bugs.&lt;/p&gt;

&lt;p&gt;You can add community plugins, configurations, and parsers to extend the functionality of ESLint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Airbnb's rules for ECMAScript 6+ and React
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx install-peerdeps --dev eslint-config-airbnb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;airbnb&lt;/code&gt; and &lt;code&gt;airbnb/hooks&lt;/code&gt; to &lt;code&gt;.eslintrc.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"airbnb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"airbnb/hooks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"next/core-web-vitals"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;next/core-web-vitals&lt;/code&gt; is the default rule set by &lt;code&gt;create-next-app&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;If you run lint now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn run lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see error like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JSX not allowed in files with extension '.tsx'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which means that we don't have the TypeScript support in our setting now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Airbnb's rules for TypeScript
&lt;/h3&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add eslint-config-airbnb-typescript \
            @typescript-eslint/eslint-plugin@^6.0.0 \
            @typescript-eslint/parser@^6.0.0 \
            --dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install eslint-config-airbnb-typescript \
            @typescript-eslint/eslint-plugin@^6.0.0 \
            @typescript-eslint/parser@^6.0.0 \
            --save-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;airbnb-typescript&lt;/code&gt; and &lt;code&gt;parserOptions&lt;/code&gt; to &lt;code&gt;.eslintrc.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"airbnb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"airbnb-typescript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"airbnb/hooks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"next/core-web-vitals"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parserOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integrate Prettier with ESLint
&lt;/h3&gt;

&lt;p&gt;Finally, we need to turn off all rules that are unnecessary or might conflict with Prettier, which can be done with the following package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add --dev eslint-config-prettier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install --save-dev eslint-config-prettier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to &lt;code&gt;.eslintrc.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"airbnb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"airbnb-typescript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"airbnb/hooks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"next/core-web-vitals"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"prettier"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parserOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.json"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each rule will extend or overwrite the previous ones, and in this situation we want to turn off the rules that might conflict with Prettier, so we should put it at the end of the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Words
&lt;/h2&gt;

&lt;p&gt;After following this tutorial, you should setup an awesome environment for developing Next.js project. All of the installation and setup commands are taken from the following official docs. You can check them out for more detail explanation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/getting-started/installation" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prettier.io/docs/en/install" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/blog/automatic-class-sorting-with-prettier" rel="noopener noreferrer"&gt;Tailwind CSS Plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/eslint-config-airbnb" rel="noopener noreferrer"&gt;eslint-config-airbnb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/eslint-config-airbnb-typescript" rel="noopener noreferrer"&gt;eslint-config-airbnb-typescript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prettier.io/docs/en/integrating-with-linters.html" rel="noopener noreferrer"&gt;Integrating Prettier with Linters&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;That’s a wrap. Thanks for reading. If you like this kind of stuff, consider following for more.&lt;/p&gt;

&lt;p&gt;You can also see what I am working on my &lt;a href="https://m4xshen.dev" rel="noopener noreferrer"&gt;Personal Website&lt;/a&gt; and &lt;a href="https://github.com/m4xshen" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>frontend</category>
      <category>nextjs</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build your modern Neovim config in Lua</title>
      <dc:creator>Max Shen</dc:creator>
      <pubDate>Sun, 26 Feb 2023 11:26:45 +0000</pubDate>
      <link>https://dev.to/m4xshen/build-your-modern-neovim-config-in-lua-1i3l</link>
      <guid>https://dev.to/m4xshen/build-your-modern-neovim-config-in-lua-1i3l</guid>
      <description>&lt;p&gt;In this tutorial you'll learn how to build and structure your modern Neovim config in Lua. I'll go through options, mappings, autocmds and plugins. You can read along with &lt;a href="https://github.com/m4xshen/dotfiles/tree/main/nvim/nvim" rel="noopener noreferrer"&gt;my configuration&lt;/a&gt;. To learn more about what each option does, use &lt;code&gt;:h 'option name'&lt;/code&gt; in Neovim.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Neovim loads config
&lt;/h2&gt;

&lt;p&gt;Neovim supports using &lt;code&gt;init.lua&lt;/code&gt; as the configuration file. This should be placed in your config directory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux, BSD or macOS: &lt;code&gt;~/.config/nvim&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Windows: &lt;code&gt;~/AppData/Local/nvim/&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although you can put all the settings inside &lt;code&gt;init.lua&lt;/code&gt;, you probably don't want to because the file will become large and difficult to manage.&lt;/p&gt;

&lt;p&gt;To avoid this, separate files to multiple modules and then load them in the &lt;code&gt;init.lua&lt;/code&gt; using &lt;code&gt;require&lt;/code&gt;. You can place those modules in the &lt;code&gt;lua/&lt;/code&gt; directory in your &lt;code&gt;runtimepath&lt;/code&gt; (a list of directories to be searched on startup).&lt;/p&gt;

&lt;p&gt;My Neovim config structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.config/nvim
|-- init.lua
|-- lua/
|  |-- config/
|  |  |-- options.lua
|  |  |-- mappings.lua
|  |  |-- autocmds.lua
|  |  |-- lazy.lua
|  |-- plugins/
|     |-- autoclose.lua
|     |-- lsp.lua
|       :
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line of &lt;code&gt;init.lua&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"config.options"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Neovim reads this line on startup, it goes through the &lt;code&gt;runtimepath&lt;/code&gt;, search for &lt;code&gt;lua/&lt;/code&gt; and load &lt;code&gt;/config/options.lua&lt;/code&gt;. The default &lt;code&gt;runtimepath&lt;/code&gt; includes includes &lt;code&gt;~/.config/nvim&lt;/code&gt;. This is why we put the &lt;code&gt;lua/&lt;/code&gt; inside it.&lt;/p&gt;

&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.&lt;/code&gt; in the module name is treated as a directory separator when searching.&lt;/li&gt;
&lt;li&gt;You don't need to type the &lt;code&gt;.lua&lt;/code&gt; extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now we understand how Neovim finds our files. We can start configuring it! Note that all the directories specified below start from the Neovim config directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Options
&lt;/h2&gt;

&lt;p&gt;You can set options via Lua in two ways: &lt;code&gt;vim.opt&lt;/code&gt; and &lt;code&gt;vim.o&lt;/code&gt; series. I recommend using &lt;code&gt;vim.opt&lt;/code&gt; series because it is more Lua-style, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;code&gt;:append()&lt;/code&gt;, &lt;code&gt;:prepend()&lt;/code&gt; and &lt;code&gt;:remove()&lt;/code&gt; to manipulate options&lt;/li&gt;
&lt;li&gt;set its value to Lua table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(see the differences between them with &lt;code&gt;:h lua-guide-options&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;You can set options with &lt;code&gt;vim.opt.option-name = value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Part of my &lt;a href="https://github.com/m4xshen/dotfiles/blob/main/nvim/nvim/lua/config/options.lua" rel="noopener noreferrer"&gt;lua/config/option.lua&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- enable line number and relative line number&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relativenumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;-- use global statusline&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;laststatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="c1"&gt;-- disable mouse&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mouse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to &lt;code&gt;require("config.options")&lt;/code&gt; in &lt;code&gt;init.lua&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keymap
&lt;/h2&gt;

&lt;p&gt;Define your leader key: (I use space. Change this to whatever you like.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mapleader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;" "&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a new mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lhs&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;{mode} (string or table) mode short-name

&lt;ul&gt;
&lt;li&gt;"" for Normal, Visual, Select, Operator-pending mode&lt;/li&gt;
&lt;li&gt;"n" for Normal mode&lt;/li&gt;
&lt;li&gt;"v" for Visual and Select mode&lt;/li&gt;
&lt;li&gt;"s" for Select mode&lt;/li&gt;
&lt;li&gt;"x" for Visual mode&lt;/li&gt;
&lt;li&gt;"o" for Operator-pending mode&lt;/li&gt;
&lt;li&gt;"i" for Insert mode&lt;/li&gt;
&lt;li&gt;"t" for Terminal mode&lt;/li&gt;
&lt;li&gt;"!" for Insert Insert and Command-line mode&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;{lhs}: (string) left-hand side of the mapping, the keys we want to map&lt;/li&gt;

&lt;li&gt;{rhs}: (string or function) right-hand side of the mapping, the keys or function we want to execute after pressing {lhs}&lt;/li&gt;

&lt;li&gt;{opts}: (table) optional parameters

&lt;ul&gt;
&lt;li&gt;silent: define a mapping that will not be echoed on the command line&lt;/li&gt;
&lt;li&gt;noremap: disable recursive mapping&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;See all available options with &lt;code&gt;:h map-arguments&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Part of my &lt;a href="https://github.com/m4xshen/dotfiles/blob/main/nvim/nvim/lua/config/mappings.lua" rel="noopener noreferrer"&gt;lua/config/mappings.lua&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- map leader+w to save current file in normal mode&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;Leader&amp;gt;w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;":write&amp;lt;CR&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;noremap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;silent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;-- map leader+y to copy to system clipboard in normal and visual mode&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keymap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s2"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"v"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;Leader&amp;gt;y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'"+y'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;noremap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;silent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to &lt;code&gt;require("config.mappings")&lt;/code&gt; in &lt;code&gt;init.lua&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto commands
&lt;/h2&gt;

&lt;p&gt;Create an autocommand event handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;nvim_create_autocmd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;{event}: (string or array) events that will trigger the handler&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BufEnter: after entering a buffer&lt;/li&gt;
&lt;li&gt;CmdlineLeave: before leaving the command-line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See all available events with &lt;code&gt;:h autocmd-events&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;{opts}: options&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pattern (string or array): pattern to match&lt;/li&gt;
&lt;li&gt;callback (function or string): Lua function called when the event is triggered&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See all available options with &lt;code&gt;:h nvim_create_autocmd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Part of my &lt;a href="https://github.com/m4xshen/dotfiles/blob/main/nvim/nvim/lua/config/autocmds.lua" rel="noopener noreferrer"&gt;lua/config/autocmds.lua&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- set tab to 3 space when entering a buffer with .lua file&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nvim_create_autocmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"BufEnter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"*.lua"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shiftwidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
      &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tabstop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
      &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;softtabstop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to &lt;code&gt;require("config.autocmds")&lt;/code&gt; in &lt;code&gt;init.lua&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins
&lt;/h2&gt;

&lt;p&gt;At this point, you should already have a basic Neovim setup. However you can install plugins to make it even better!&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://github.com/folke/lazy.nvim" rel="noopener noreferrer"&gt;lazy.nvim&lt;/a&gt; to manage my plugins. We need to install it first. Remember to &lt;code&gt;require("config.lazy")&lt;/code&gt; in &lt;code&gt;init.lua&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- `lua/config/lazy.lua`&lt;/span&gt;

&lt;span class="c1"&gt;-- install lazy.nvim&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;lazypath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;"/lazy/lazy.nvim"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fs_stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lazypath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
   &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="s2"&gt;"git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"clone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"--filter=blob:none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"https://github.com/folke/lazy.nvim.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"--branch=stable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- latest stable release&lt;/span&gt;
      &lt;span class="n"&gt;lazypath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rtp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lazypath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- load plugins&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"lazy"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command on the last line loads all the &lt;code&gt;.lua&lt;/code&gt; file under &lt;code&gt;lua/plugins/&lt;/code&gt; and the returned table will be merged and passed to &lt;code&gt;setup()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- lua/plugins/autoclose.lua&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"m4xshen/autoclose.nvim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;disabled_filetypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"markdown"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;disable_when_touch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"windwp/nvim-ts-autotag"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the returned table, the first line is the plugin's short url and the rest are arguments(optional) to set plugins up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;config&lt;/code&gt;: Function that is executed when the plugin loads. The default implementation will run &lt;code&gt;require("plugin").setup(opts)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;opts&lt;/code&gt;: Passing options to the &lt;code&gt;config&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;init&lt;/code&gt;: Functions that is executed during startup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out all available option on lazy.nvim's &lt;a href="https://github.com/folke/lazy.nvim#-plugin-spec" rel="noopener noreferrer"&gt;README.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Follow this pattern to install and set other plugins up. You can also see my &lt;a href="https://github.com/m4xshen/dotfiles/tree/main/nvim/nvim/lua/plugins" rel="noopener noreferrer"&gt;plugins config files&lt;/a&gt; to get some ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Words
&lt;/h2&gt;

&lt;p&gt;Now you have a modern Neovim configuration file written in Lua! If you want to explore more, you can install plugins like &lt;a href="https://github.com/nvim-treesitter/nvim-treesitter" rel="noopener noreferrer"&gt;nvim-treesitter&lt;/a&gt; and &lt;a href="https://github.com/neovim/nvim-lspconfig" rel="noopener noreferrer"&gt;nvim-lspconfig&lt;/a&gt;. These plugins can give you better experience when coding.&lt;/p&gt;

&lt;p&gt;Discover more plugins on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/rockerBOO/awesome-neovim" rel="noopener noreferrer"&gt;Awesome Neovim&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://this-week-in-neovim.org/" rel="noopener noreferrer"&gt;This Week in Neovim&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;That’s a wrap. Thanks for reading. If you like this kind of stuff, consider following for more.&lt;/p&gt;

&lt;p&gt;You can also see what I am working on my &lt;a href="https://m4xshen.dev" rel="noopener noreferrer"&gt;Personal Website&lt;/a&gt; and &lt;a href="https://github.com/m4xshen" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>gratitude</category>
      <category>career</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
