<?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: Arbab Tahir</title>
    <description>The latest articles on DEV Community by Arbab Tahir (@marbabtahir).</description>
    <link>https://dev.to/marbabtahir</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F744126%2F05761e2f-148e-4e14-b297-63d14dedee74.png</url>
      <title>DEV Community: Arbab Tahir</title>
      <link>https://dev.to/marbabtahir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marbabtahir"/>
    <language>en</language>
    <item>
      <title>How We Think About Content Structure While Building JustBlogged</title>
      <dc:creator>Arbab Tahir</dc:creator>
      <pubDate>Fri, 07 Aug 2026 05:39:33 +0000</pubDate>
      <link>https://dev.to/marbabtahir/how-we-think-about-content-structure-while-building-justblogged-45hi</link>
      <guid>https://dev.to/marbabtahir/how-we-think-about-content-structure-while-building-justblogged-45hi</guid>
      <description>&lt;h1&gt;
  
  
  How We Think About Content Structure While Building JustBlogged
&lt;/h1&gt;

&lt;p&gt;Most posts about building a writing platform turn into a rundown of the tech stack. This one isn't that. Instead, I want to talk about something developers usually skip over: content UX. The invisible structure that decides whether a piece of writing actually gets read, or just gets published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Should Care About Content UX
&lt;/h2&gt;

&lt;p&gt;We're used to thinking about UX in terms of buttons, load times, and responsive layouts. But content UX. How information is structured, paced, and formatted. Is just as much an engineering problem as it is a writing one.&lt;/p&gt;

&lt;p&gt;If you've ever built a blog, a docs site, or any tool where users create content, you've already made content UX decisions whether you realized it or not. Every default font size, every character limit, every "are you sure you want to publish?" prompt shapes how people write, not just how they read.&lt;/p&gt;

&lt;p&gt;For developers, this matters because bad content UX creates bad content and bad content is a support burden, a retention problem, and eventually a churn problem. A writing tool that makes structure hard will produce walls of text. A tool that makes structure easy will produce content people actually finish reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Writing Tools Influence Publishing Workflows
&lt;/h2&gt;

&lt;p&gt;The tool shapes the output more than most people give it credit for. A few examples of how this plays out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Friction changes behavior.&lt;/strong&gt; If adding a heading takes three clicks, most people won't bother even if they know their post needs one. Reduce the friction, and structure becomes the default instead of the exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constraints create discipline.&lt;/strong&gt; A title field with a soft character limit nudges writers toward punchier, clearer headlines instead of vague run-on phrases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defaults are opinions.&lt;/strong&gt; Whatever formatting option is easiest to reach becomes the one people use most. If your editor defaults to plain paragraphs, that's what most drafts will look like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback loops matter.&lt;/strong&gt; Tools that show writers a live preview, a reading-time estimate, or a readability signal change decisions in real time, not after the fact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is about forcing a rigid format on writers. It's about making the &lt;em&gt;good&lt;/em&gt; choice the &lt;em&gt;easy&lt;/em&gt; choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Implementation Decisions That Improve the Writing Experience
&lt;/h2&gt;

&lt;p&gt;While building JustBlogged, a lot of the improvements that mattered most weren't big features. They were small, almost boring decisions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Auto-suggesting headings as a draft grows.&lt;/strong&gt; Once a draft crosses a certain length without a heading, we nudge the writer to break it up. Small prompt, big impact on scannability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeping the title field visually separate and slightly constrained.&lt;/strong&gt; This alone pushes people toward writing sharper, more specific titles instead of full sentences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline formatting instead of a separate toolbar.&lt;/strong&gt; Reducing the number of clicks between "having a thought" and "formatting it correctly" keeps writers in flow instead of breaking their focus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surfacing structure, not just content, in drafts.&lt;/strong&gt; Showing a lightweight outline of a post's headings while writing helps authors catch when a piece is unbalanced: too much intro, not enough substance, no real conclusion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating whitespace as a feature, not an afterthought.&lt;/strong&gt; Default spacing between paragraphs and sections was tuned specifically so posts don't feel dense on first glance, which is often the real reason readers bounce.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Individually, none of these decisions is impressive. Together, they add up to a meaningfully different writing experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned From Building a Platform for Creators
&lt;/h2&gt;

&lt;p&gt;A few things became clear the more we worked on this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Writers don't want more options. They want fewer decisions.&lt;/strong&gt; Every extra formatting choice is a moment where a writer has to stop writing and start deciding. The best tools make good structural decisions on the writer's behalf by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure is a reader-facing feature, even though it's authored by the writer.&lt;/strong&gt; Every heading, every paragraph break, every list is really a UX decision made for someone who hasn't shown up yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small nudges outperform big rules.&lt;/strong&gt; We got much better results from gentle suggestions ("this section is getting long") than from hard constraints or rigid templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The best content tools disappear.&lt;/strong&gt; The goal isn't for writers to notice the tool. It's for them to notice that writing feels easier, without necessarily knowing why.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there's one takeaway for developers building anything content-related: the code you write doesn't just render content, it shapes what content gets created in the first place. Treat that responsibility with the same care you'd give any other core UX decision.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's a small tooling decision you've made. In a CMS, an editor, or even a README template that ended up having an outsized effect on the quality of what people wrote? Curious to hear how others have approached this.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>writing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building an SEO-Friendly Slug Generator for JustBlogged</title>
      <dc:creator>Arbab Tahir</dc:creator>
      <pubDate>Fri, 31 Jul 2026 05:45:50 +0000</pubDate>
      <link>https://dev.to/marbabtahir/building-an-seo-friendly-slug-generator-for-justblogged-4n2k</link>
      <guid>https://dev.to/marbabtahir/building-an-seo-friendly-slug-generator-for-justblogged-4n2k</guid>
      <description>&lt;h2&gt;
  
  
  When we started building &lt;strong&gt;JustBlogged&lt;/strong&gt;, we noticed a small but recurring problem.
&lt;/h2&gt;

&lt;p&gt;Many creators publish great content but pay very little attention to their URLs. Some platforms generate long or unclear slugs automatically, while others require users to edit them manually.&lt;/p&gt;

&lt;p&gt;Since JustBlogged aims to simplify publishing, we decided to build a free &lt;strong&gt;Slug Generator&lt;/strong&gt; that creates clean, readable, and SEO-friendly URLs in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Slugs Matter
&lt;/h2&gt;

&lt;p&gt;A URL is often the first thing people see before opening your content.&lt;/p&gt;

&lt;p&gt;Compare these two examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/post?id=48392
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/how-to-start-a-tech-blog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second immediately tells both users and search engines what the page is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Approach
&lt;/h2&gt;

&lt;p&gt;We wanted the generator to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convert text to lowercase.&lt;/li&gt;
&lt;li&gt;Replace spaces with hyphens.&lt;/li&gt;
&lt;li&gt;Remove unnecessary punctuation.&lt;/li&gt;
&lt;li&gt;Produce clean, readable URLs.&lt;/li&gt;
&lt;li&gt;Be fast enough to update instantly while typing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified version of the logic looks 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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;slugify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^\w\s&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, there are additional edge cases to consider, such as Unicode characters, accented letters, emojis, duplicate slugs, and reserved routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned
&lt;/h2&gt;

&lt;p&gt;Building this feature reminded us that publishing isn't just about writing.&lt;/p&gt;

&lt;p&gt;Small details like URL structure, readability, and workflow all contribute to a better publishing experience.&lt;/p&gt;

&lt;p&gt;Sometimes the smallest features save the most time because they're used every single day.&lt;/p&gt;

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

&lt;p&gt;If you're a blogger, developer, or content creator, you can try our free &lt;strong&gt;Slug Generator&lt;/strong&gt; here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://justblogged.com/tools/slug-generator" rel="noopener noreferrer"&gt;https://justblogged.com/tools/slug-generator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're continuously improving our creator tools, so we'd love to hear your feedback and ideas for what we should build next.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>seo</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Real-Time Word Counter in JavaScript</title>
      <dc:creator>Arbab Tahir</dc:creator>
      <pubDate>Wed, 29 Jul 2026 11:04:07 +0000</pubDate>
      <link>https://dev.to/marbabtahir/building-a-real-time-word-counter-in-javascript-56gc</link>
      <guid>https://dev.to/marbabtahir/building-a-real-time-word-counter-in-javascript-56gc</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why Build One?&lt;/strong&gt;&lt;br&gt;
Whether you are building a modern CMS, an in-browser markdown editor, or a custom blogging platform, real-time text analysis is a fundamental utility. While a basic word count might seem simple at first glance, accurately handling edge cases such as non-standard whitespace, multi-line line breaks, symbols, and reading time estimates requires a firm understanding of string manipulation and performance optimization in JavaScript.&lt;/p&gt;

&lt;p&gt;In this guide, we will build a performant, dependency-free text metrics engine from scratch.&lt;/p&gt;

&lt;p&gt;Counting Words Accurately&lt;br&gt;
The naive approach to word counting usually looks 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="c1"&gt;// ❌ Naive approach - fragile!&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countWordsNaive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This breaks instantly if the user enters multiple spaces, newlines, or leading/trailing whitespace. A robust implementation cleans the input and uses regular expressions to isolate words.&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="cm"&gt;/**
 * Accurately counts words in a string.
 * @param {string} text 
 * @returns {number}
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getWordCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;trimmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Match sequences of non-whitespace characters&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;By matching non-whitespace sequences (&lt;code&gt;\S+&lt;/code&gt;), we automatically handle tabs, multiple spaces, and line breaks without extra sanitization steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Character Count&lt;/strong&gt;&lt;br&gt;
Character counts usually come in two variations: &lt;strong&gt;total characters&lt;/strong&gt; (including spaces) and visible characters (excluding spaces).&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="cm"&gt;/**
 * Calculates character metrics.
 * @param {string} text 
 * @returns {{ total: number, noSpaces: number }}
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCharacterCounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;noSpaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Sentence Detection&lt;/strong&gt;&lt;br&gt;
Detecting sentences accurately via Regex can be tricky due to abbreviations (e.g., Dr., e.g.), but for standard prose analysis, splitting on sentence-ending punctuation (&lt;code&gt;.&lt;/code&gt;, &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;) followed by whitespace or string end provides a solid balance of accuracy and speed.&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="cm"&gt;/**
 * Estimates sentence count in a text block.
 * @param {string} text 
 * @returns {number}
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSentenceCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;trimmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Split by '.', '!', or '?' followed by space or end of string&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sentences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.!?&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:\s&lt;/span&gt;&lt;span class="sr"&gt;+|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Paragraph Detection&lt;/strong&gt;&lt;br&gt;
Paragraphs are defined by structural line breaks. We must filter out empty lines caused by consecutive presses of the &lt;code&gt;Enter&lt;/code&gt; key.&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="cm"&gt;/**
 * Counts non-empty paragraphs.
 * @param {string} text 
 * @returns {number}
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getParagraphCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Split by one or more newlines and remove whitespace-only lines&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paragraph&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;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;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Estimating Reading Time&lt;/strong&gt;&lt;br&gt;
The average adult reads approximately &lt;strong&gt;200 to 250 words per minute (WPM)&lt;/strong&gt;. We can calculate estimated reading time in minutes and format it cleanly for UI consumption.&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="cm"&gt;/**
 * Calculates estimated reading time in minutes.
 * @param {number} wordCount 
 * @param {number} wpm 
 * @returns {number}
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateReadingTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wordCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wpm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;225&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wordCount&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="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wordCount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;wpm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Performance Considerations&lt;/strong&gt;&lt;br&gt;
Binding text parsing directly to an &lt;code&gt;input&lt;/code&gt; event on a large &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; can cause UI lag if re-computations happen on every single keystroke. To maintain a butter-smooth 60fps, use debouncing or &lt;code&gt;requestAnimationFrame&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is a lightweight implementation using &lt;code&gt;requestAnimationFrame&lt;/code&gt; for high-frequency DOM input:&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;class&lt;/span&gt; &lt;span class="nc"&gt;RealTimeTextAnalyzer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onInput&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;onInput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestAnimationFrame&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticking&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticking&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="nf"&gt;analyze&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getWordCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getCharacterCounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getSentenceCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;paragraphs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getParagraphCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;readingTimeMin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;calculateReadingTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stats&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;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
With less than 100 lines of plain JavaScript, you can build a robust, high-performance text metrics pipeline. This logic can easily be wrapped into a React hook, Vue composable, or Web Component depending on your stack.&lt;/p&gt;

&lt;p&gt;JustBlogged Offers a Free Word Counter For Bloggers 👇🏻&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://justblogged.com/tools/word-counter" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;justblogged.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>frontend</category>
    </item>
    <item>
      <title>How Early User Feedback Changed the Way We're Building Our SaaS</title>
      <dc:creator>Arbab Tahir</dc:creator>
      <pubDate>Thu, 23 Jul 2026 11:44:10 +0000</pubDate>
      <link>https://dev.to/marbabtahir/how-early-user-feedback-changed-the-way-were-building-our-saas-50o9</link>
      <guid>https://dev.to/marbabtahir/how-early-user-feedback-changed-the-way-were-building-our-saas-50o9</guid>
      <description>&lt;h2&gt;
  
  
  How Early User Feedback Changed the Way We're Building Our SaaS
&lt;/h2&gt;

&lt;p&gt;As developers, it's easy to think the next feature is what users need.&lt;/p&gt;

&lt;p&gt;While building JustBlogged, we discovered that wasn't the most important question.&lt;/p&gt;

&lt;p&gt;The better question became:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What would actually make someone leave the product they already use?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That shifted our conversations from collecting feature requests to understanding real user problems.&lt;/p&gt;

&lt;p&gt;We've started focusing on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reducing friction between writing and publishing.&lt;/li&gt;
&lt;li&gt;Simplifying the overall experience instead of adding complexity.&lt;/li&gt;
&lt;li&gt;Validating problems before building solutions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One interesting lesson is that every feature has a hidden cost. Even if users never click it, they still need to recognize it, understand it, and decide whether it matters.&lt;/p&gt;

&lt;p&gt;Keeping a product simple often requires saying "no" more than saying "yes."&lt;/p&gt;

&lt;p&gt;We're still learning, but building in public has accelerated that learning more than any internal brainstorming session ever could.&lt;/p&gt;

&lt;p&gt;I'm curious! What's the most valuable piece of user feedback you've received while building a product?&lt;/p&gt;

</description>
      <category>saas</category>
      <category>buildinpublic</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>I Built an Open-Source Discord Bot for Developers (Code Analysis, Docs, GitHub Insights)</title>
      <dc:creator>Arbab Tahir</dc:creator>
      <pubDate>Tue, 03 Feb 2026 20:01:02 +0000</pubDate>
      <link>https://dev.to/marbabtahir/i-built-an-open-source-discord-bot-for-developers-code-analysis-docs-github-insights-388k</link>
      <guid>https://dev.to/marbabtahir/i-built-an-open-source-discord-bot-for-developers-code-analysis-docs-github-insights-388k</guid>
      <description>&lt;p&gt;I’ve been working on a Discord bot that developer communities can self-host: it analyzes code, generates docs, explains errors, and even inspects GitHub repos, all via slash commands and powered by OpenAI. I’ve open-sourced it and wanted to share what it does and how it’s built.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;The bot exposes six slash commands. No need for the bot to read every message; everything is interaction-based.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/analyze-code&lt;/code&gt;&lt;/strong&gt; — Paste a code snippet. You get the detected language, potential issues, refactoring ideas, and quality/security notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/generate-docs&lt;/code&gt;&lt;/strong&gt; — Paste code and get generated documentation: what it does, parameters, return values, and usage examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/error-explain&lt;/code&gt;&lt;/strong&gt; — Paste an error or stack trace and get a plain-language explanation plus suggested fixes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/explain&lt;/code&gt;&lt;/strong&gt; — Ask for an explanation of a concept (e.g. “async await”) at Beginner, Intermediate, or Senior level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/github-analyze&lt;/code&gt;&lt;/strong&gt; — Pass a public GitHub repo URL. You get a tech stack summary, project structure overview, and improvement suggestions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/project-health&lt;/code&gt;&lt;/strong&gt; — Same repo URL. You get recent commit activity, contributor overview, and a short “health” summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;User activity (e.g. which commands are used) is tracked in MongoDB for a simple skill/usage view; the data stays with whoever hosts the bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js + TypeScript&lt;/strong&gt; — Typed, maintainable code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;discord.js&lt;/strong&gt; — Slash commands and interactions; no privileged intents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI API (gpt-3.5-turbo)&lt;/strong&gt; — All the “understanding” and text generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB (Mongoose)&lt;/strong&gt; — User and usage data for the scoring idea.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo includes setup guides: Discord app configuration, env vars, optional GitHub token for the repo commands, and how to run it locally or in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this structure
&lt;/h2&gt;

&lt;p&gt;I wanted something that’s easy to extend: one place for command definitions, one for handlers, and separate services for GitHub, OpenAI, and user logic. New commands are just a new definition + handler + optional service. There’s an &lt;a href="https://github.com/marbabtahir/discord-developer-assistant/blob/main/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; doc in the repo for contributors.&lt;/p&gt;

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

&lt;p&gt;Everything is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/marbabtahir/discord-developer-assistant" rel="noopener noreferrer"&gt;Discord Developer Assistant&lt;/a&gt;&lt;/strong&gt;. It’s MIT licensed. The README and &lt;code&gt;/docs&lt;/code&gt; folder cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discord Developer Portal setup (which intents and permissions to enable)&lt;/li&gt;
&lt;li&gt;Optional GitHub token for &lt;code&gt;/github-analyze&lt;/code&gt; and &lt;code&gt;/project-health&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;MongoDB (local or Atlas)&lt;/li&gt;
&lt;li&gt;Build and run commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run a dev community on Discord or just want to tinker with a bot that uses OpenAI and the GitHub API, feel free to clone it, open issues, or send PRs. I’d love to hear what you’d add next.&lt;/p&gt;

</description>
      <category>discord</category>
      <category>typescript</category>
      <category>openai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
