<?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: Kazu</title>
    <description>The latest articles on DEV Community by Kazu (@_402ccbd6e5cb02871506).</description>
    <link>https://dev.to/_402ccbd6e5cb02871506</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%2F3422958%2F561728f2-3289-4079-9cba-cc1c855c8b68.png</url>
      <title>DEV Community: Kazu</title>
      <link>https://dev.to/_402ccbd6e5cb02871506</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_402ccbd6e5cb02871506"/>
    <language>en</language>
    <item>
      <title>I Built a Markdown Linter in Go — Here's How It Stacks Up Against markdownlint and remark-lint</title>
      <dc:creator>Kazu</dc:creator>
      <pubDate>Tue, 10 Mar 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/_402ccbd6e5cb02871506/i-built-a-markdown-linter-in-go-heres-how-it-stacks-up-against-markdownlint-and-remark-lint-4b6i</link>
      <guid>https://dev.to/_402ccbd6e5cb02871506/i-built-a-markdown-linter-in-go-heres-how-it-stacks-up-against-markdownlint-and-remark-lint-4b6i</guid>
      <description>&lt;h2&gt;
  
  
  Documentation breaks quietly. Trust erodes loudly.
&lt;/h2&gt;

&lt;p&gt;If you've worked on an engineering team with shared documentation, you've probably seen this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An H4 heading appearing directly under an H2 with no H3 in between&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;[see details](#overview)&lt;/code&gt; link silently 404ing because someone renamed the section&lt;/li&gt;
&lt;li&gt;A fenced code block missing its closing backticks — making everything below it render as code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike code bugs, documentation quality degrades without throwing errors. By the time someone notices, someone else is already confused.&lt;/p&gt;

&lt;p&gt;That frustration led me to build &lt;strong&gt;&lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;gomarklint&lt;/a&gt;&lt;/strong&gt; — a fast, opinionated Markdown linter written in Go, designed for CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not just use an existing tool?
&lt;/h2&gt;

&lt;p&gt;That was my first question too.&lt;/p&gt;

&lt;p&gt;The existing tools are excellent, but they each have tradeoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;markdownlint&lt;/strong&gt; (JavaScript): Requires Node.js. Adding it to a Go project's CI means pulling in a JS runtime just for linting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;remark-lint&lt;/strong&gt; (JavaScript): Extremely powerful and pluggable, but configuration complexity can be a barrier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mdformat&lt;/strong&gt; (Python): A formatter, not a linter — it silently rewrites files rather than reporting violations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something I could drop into any CI pipeline with a single binary, no runtime dependencies. In Go, that's natural.&lt;/p&gt;




&lt;h2&gt;
  
  
  I did a deep dive into the Markdown linter ecosystem
&lt;/h2&gt;

&lt;p&gt;Before planning gomarklint's next phase, I did a structured comparison of the major tools. Here's what the landscape looks like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;Rules&lt;/th&gt;
&lt;th&gt;Nature&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;markdownlint (DavidAnson)&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;~5,900&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60 built-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structural linter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remark-lint&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;~1,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~80 packages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structural linter (AST-based)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;markdownlint (Ruby)&lt;/td&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;~2,009&lt;/td&gt;
&lt;td&gt;~50&lt;/td&gt;
&lt;td&gt;Structural linter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mdformat&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;~726&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Auto-formatter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gomarklint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structural linter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I'll be honest: &lt;strong&gt;gomarklint currently has 8 rules.&lt;/strong&gt; Compared to markdownlint's 60, that's a significant gap. But rule count isn't the whole story.&lt;/p&gt;




&lt;h2&gt;
  
  
  What makes gomarklint different
&lt;/h2&gt;

&lt;p&gt;Digging into how these tools work revealed some genuine differentiators.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Live HTTP link validation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gomarklint &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--enable-link-check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither markdownlint nor remark-lint validates whether external URLs actually resolve. gomarklint makes real HTTP requests and reports unreachable links — with concurrent validation that can check ~2,000 links in under 10 seconds.&lt;/p&gt;

&lt;p&gt;This turns out to be a meaningful differentiator. Dead external links are one of the most common ways documentation silently degrades over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Unclosed code block detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// This code block was never closed...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most linters use tolerant AST parsers that silently repair structural errors. gomarklint uses a text-based approach that catches the raw structural breakage — the kind that makes everything below a fence render as a code block.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Single static binary — no runtime required
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Download the binary — no Node.js, no Python, no Go needed&lt;/span&gt;
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; gomarklint_Darwin_x86_64.tar.gz
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;gomarklint /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Node.js. No Python. One line and you're ready. This makes CI integration trivially simple, especially for Go projects or polyglot repos where you don't want to manage a JS/Python environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Frontmatter-aware parsing
&lt;/h3&gt;

&lt;p&gt;gomarklint strips YAML/TOML frontmatter before applying rules, so it works cleanly with Hugo, Jekyll, and other SSG-based documentation setups without false positives.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Goroutine-based concurrent processing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;185 files, 104,000+ lines — under 60ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fast enough to run on every save locally. Practical even at scale in CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where gomarklint stands today
&lt;/h2&gt;

&lt;p&gt;gomarklint's current 8 rules map to well-known equivalents in the ecosystem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;gomarklint Rule&lt;/th&gt;
&lt;th&gt;markdownlint&lt;/th&gt;
&lt;th&gt;remark-lint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;final-blank-line&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MD047&lt;/td&gt;
&lt;td&gt;&lt;code&gt;final-newline&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unclosed-code-block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;— (unique)&lt;/td&gt;
&lt;td&gt;— (unique)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;empty-alt-text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MD045&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heading-level&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MD001 + MD041&lt;/td&gt;
&lt;td&gt;&lt;code&gt;heading-increment&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duplicate-heading&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MD024&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-duplicate-headings&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-multiple-blank-lines&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MD012&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-consecutive-blank-lines&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;external-link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;— (unique)&lt;/td&gt;
&lt;td&gt;— (unique)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-setext-headings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MD003&lt;/td&gt;
&lt;td&gt;&lt;code&gt;heading-style&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two rules with no equivalent — &lt;code&gt;unclosed-code-block&lt;/code&gt; and &lt;code&gt;external-link&lt;/code&gt; — are where gomarklint offers something the major tools don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's coming next
&lt;/h2&gt;

&lt;p&gt;The gap analysis made the roadmap clear. Here's what I'm planning to add, starting with the highest-impact rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Priority 1 — Simple to implement, high real-world value:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fenced-code-language&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fenced code blocks must specify a language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;single-h1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only one H1 heading per document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blanks-around-headings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Headings must be surrounded by blank lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-bare-urls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URLs must use proper Markdown link syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-trailing-spaces&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No trailing whitespace at end of lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-emphasis-as-heading&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bold/italic text must not substitute for headings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-empty-links&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Links must not have an empty destination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blanks-around-lists&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists must be surrounded by blank lines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each rule will be designed with the same pattern: a toggle flag, a config field, unit tests, a benchmark, and documentation. No shortcuts.&lt;/p&gt;

&lt;p&gt;The full gap analysis and planned rule set is tracked in &lt;a href="https://github.com/shinagawa-web/gomarklint/issues/76" rel="noopener noreferrer"&gt;Issue #76&lt;/a&gt;.&lt;/p&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/shinagawa-web/gomarklint@latest
gomarklint init
gomarklint &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full documentation: &lt;strong&gt;&lt;a href="https://shinagawa-web.github.io/gomarklint/" rel="noopener noreferrer"&gt;https://shinagawa-web.github.io/gomarklint/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you try it and hit an edge case, open an issue. If you want to contribute a rule, PRs are very welcome. The codebase is small and the patterns are consistent — a new rule is typically 50–100 lines of Go plus tests.&lt;/p&gt;




&lt;p&gt;Docs break quietly. Let's make them break loudly — in CI, before they ship.&lt;/p&gt;

</description>
      <category>go</category>
      <category>markdown</category>
      <category>opensource</category>
      <category>linter</category>
    </item>
    <item>
      <title>Beyond Lines: Announcing "gosemdiff" – A Logic-Aware Diff Tool for Go</title>
      <dc:creator>Kazu</dc:creator>
      <pubDate>Fri, 13 Feb 2026 14:50:00 +0000</pubDate>
      <link>https://dev.to/_402ccbd6e5cb02871506/beyond-lines-announcing-gosemdiff-a-logic-aware-diff-tool-for-go-403p</link>
      <guid>https://dev.to/_402ccbd6e5cb02871506/beyond-lines-announcing-gosemdiff-a-logic-aware-diff-tool-for-go-403p</guid>
      <description>&lt;p&gt;Today marks a personal milestone: I have finally finished a massive, six-month-long refactoring of my other project, &lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;gomarklint&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;After spending half a year staring at Go code structures and AST nodes, I realized something painful. Our current tools are still "dumb" when it comes to understanding the &lt;em&gt;intent&lt;/em&gt; of our changes. We are still reviewing code line-by-line, even though we think in structures and logic.&lt;/p&gt;

&lt;p&gt;That’s why I’m excited to announce my next OSS project: &lt;a href="https://github.com/shinagawa-web/gosemdiff" rel="noopener noreferrer"&gt;gosemdiff&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Current Solutions (Including AI)
&lt;/h2&gt;

&lt;p&gt;We now have AI tools that can summarize Pull Requests for us. They are helpful, but let's be honest: &lt;strong&gt;AI summaries are often "vibes-based."&lt;/strong&gt; An AI might say, &lt;em&gt;"This PR refactors the processing logic,"&lt;/em&gt; but it can't mathematically guarantee that the logic remains unchanged. AI can hallucinate, overlook edge cases, or fail to distinguish between a variable rename and a subtle logic bug.&lt;/p&gt;

&lt;p&gt;As engineers, we don't need a "guess." We need &lt;strong&gt;proof&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Standard &lt;code&gt;git diff&lt;/code&gt; treats code as text. But code is not just text; it’s a tree of logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: gosemdiff
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;gosemdiff&lt;/strong&gt; is a semantic diff tool designed specifically for Go. Instead of comparing lines, it parses your source code into an &lt;strong&gt;Abstract Syntax Tree (AST)&lt;/strong&gt; and compares the underlying structures.&lt;/p&gt;

&lt;p&gt;The goal is to provide a "Truth Machine" for your Pull Requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Concepts:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semantic Labeling&lt;/strong&gt;: Automatically identify changes as &lt;code&gt;MOVE&lt;/code&gt;, &lt;code&gt;RENAME&lt;/code&gt;, or &lt;code&gt;LOGIC CHANGE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic Integrity&lt;/strong&gt;: Prove that a refactor hasn't changed the execution flow by comparing normalized AST hashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test-Gap Detection&lt;/strong&gt;: Alert you if a logic change was made without any corresponding updates to your &lt;code&gt;*_test.go&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-Noise Mode&lt;/strong&gt;: Completely ignore comments, formatting, and import ordering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Roadmap to v1.0.0
&lt;/h2&gt;

&lt;p&gt;I’ve laid out an ambitious roadmap in the repository, moving from basic function inventorying to a fully integrated GitHub Action. &lt;/p&gt;

&lt;p&gt;The ultimate goal? A CI bot that tells your reviewers: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This PR is 90% refactoring. Logic changes detected in only 2 files. One logic change is missing a unit test."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  I Need Your Feedback!
&lt;/h2&gt;

&lt;p&gt;I’m taking a short break to recharge after the &lt;code&gt;gomarklint&lt;/code&gt; marathon, and full-scale development on &lt;code&gt;gosemdiff&lt;/code&gt; will kick off in about two months. &lt;/p&gt;

&lt;p&gt;However, the "Grand Vision" is already live in the README. I want to build this for the community, so I need your input:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What kind of diffs annoy you the most during code reviews?&lt;/li&gt;
&lt;li&gt;What "semantic" features would make your life easier?&lt;/li&gt;
&lt;li&gt;What’s your dream CI summary?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Please check out the repo and leave your ideas, feature requests, or "what-ifs" in the GitHub Issues!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/shinagawa-web/gosemdiff" rel="noopener noreferrer"&gt;gosemdiff&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's make code reviews about logic again, not just lines.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>analytics</category>
      <category>refactoring</category>
    </item>
    <item>
      <title>Making Parallel HTTP Requests Stable in Go: Lessons from Building a Markdown Linter</title>
      <dc:creator>Kazu</dc:creator>
      <pubDate>Thu, 15 Jan 2026 13:28:45 +0000</pubDate>
      <link>https://dev.to/_402ccbd6e5cb02871506/making-parallel-http-requests-stable-in-go-lessons-from-building-a-markdown-linter-1j36</link>
      <guid>https://dev.to/_402ccbd6e5cb02871506/making-parallel-http-requests-stable-in-go-lessons-from-building-a-markdown-linter-1j36</guid>
      <description>&lt;p&gt;When building &lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;gomarklint&lt;/a&gt;, a Go-based Markdown linter, I faced a challenge: checking 100,000+ lines of documentation for broken links. Parallelizing this with Goroutines seemed like a "no-brainer," but it immediately led to Flaky Tests in CI environments.&lt;/p&gt;

&lt;p&gt;Speed is easy in Go; stability is the real challenge. Here are the three patterns I implemented to achieve both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cache: Preventing 'Barrages'
&lt;/h2&gt;

&lt;p&gt;In a large docset, the same URL appears dozens of times. Naive concurrency sends a request for every single occurrence, which looks like a DoS attack to the host.&lt;/p&gt;

&lt;p&gt;Using sync.Map, I implemented a simple URL cache to ensure each unique URL is only checked once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;urlCache&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;

&lt;span class="c"&gt;// Check if we've seen this URL before&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;urlCache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&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;checkResult&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Semaphore: Respecting Resource Limits
&lt;/h2&gt;

&lt;p&gt;Even with a cache, checking 1,000 unique URLs simultaneously can exhaust local file descriptors or trigger rate limits.&lt;br&gt;
I used a buffered channel as a semaphore to cap the number of active Goroutines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;maxConcurrency&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;maxConcurrency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt; &lt;span class="c"&gt;// Acquire token&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&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;lt;-&lt;/span&gt;&lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="p"&gt;}()&lt;/span&gt; &lt;span class="c"&gt;// Release token&lt;/span&gt;
        &lt;span class="n"&gt;checkURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Retry: Tolerating Network 'Whims'
&lt;/h2&gt;

&lt;p&gt;Networks are inherently unreliable. A momentary blip shouldn't fail your entire CI build. I implemented Exponential Backoff to distinguish between permanent failures (404) and transient ones (5xx, timeouts).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Only retry if it's a server error or network timeout&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c"&gt;// retry...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The "Negative Caching" Trap
&lt;/h2&gt;

&lt;p&gt;The most elusive bug was caching only the status code. If a request failed with a timeout, I stored status: 0. Subsequent checks retrieved 0 but didn't know an error had occurred, leading to inconsistent logic.&lt;/p&gt;

&lt;p&gt;The Fix: Cache the entire result, including the error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;checkResult&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt;    &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// Store the pointer to this struct in your cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion: Is it 100% Stable?
&lt;/h2&gt;

&lt;p&gt;Not quite. Even with these, "Cache Stampedes" (multiple Goroutines hitting the same uncached URL at the exact same millisecond) remain a concern.&lt;/p&gt;

&lt;p&gt;I'm currently exploring &lt;code&gt;golang.org/x/sync/singleflight&lt;/code&gt; to solve this. If you have experience tuning http.Client for massive parallel checks, I'd love to hear your thoughts in the comments or on &lt;a href="https://github.com/shinagawa-web/gomarklint/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>go</category>
      <category>networking</category>
      <category>performance</category>
    </item>
    <item>
      <title>Super Fast Markdown Linting for Go Developers: Meet gomarklint</title>
      <dc:creator>Kazu</dc:creator>
      <pubDate>Tue, 13 Jan 2026 07:49:10 +0000</pubDate>
      <link>https://dev.to/_402ccbd6e5cb02871506/super-fast-markdown-linting-for-go-developers-meet-gomarklint-3ikd</link>
      <guid>https://dev.to/_402ccbd6e5cb02871506/super-fast-markdown-linting-for-go-developers-meet-gomarklint-3ikd</guid>
      <description>&lt;h2&gt;
  
  
  The "Why" (The Motivation)
&lt;/h2&gt;

&lt;p&gt;Documentation is the heart of any project, but keeping it consistent is a nightmare.&lt;/p&gt;

&lt;p&gt;While working on various Go projects, I realized a few things about my workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Context Switching Costs: I love Go's speed and simplicity. Having to install Node.js or Ruby just to run a Markdown linter in a Go project felt "heavy."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CI Fatigue: In large repositories, documentation checks shouldn't take seconds—they should take milliseconds. Every second saved in CI is a win for developer experience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The "Broken Link" Problem: There’s nothing more embarrassing than shipping a README with dead links. I needed a tool that catches these issues instantly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I couldn't find a tool that was Go-native, ultra-fast, and zero-config by default, so I decided to build one.&lt;/p&gt;

&lt;p&gt;The goal for gomarklint was simple: Make Markdown linting so fast and easy that you never have an excuse to skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed Performance
&lt;/h2&gt;

&lt;p&gt;When I say "fast," I mean Go-fast.&lt;/p&gt;

&lt;p&gt;In many CI/CD pipelines, linting documentation is often the bottleneck that adds unnecessary seconds (or even minutes) to every PR. gomarklint changes that. By leveraging Go's concurrency and efficient string handling, it delivers near-instant feedback.&lt;/p&gt;

&lt;p&gt;The Benchmark: I tested gomarklint against a large documentation set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total Files: 180 Markdown files&lt;/li&gt;
&lt;li&gt;Total Volume: 100,000+ lines of text&lt;/li&gt;
&lt;li&gt;Execution Time: &amp;lt; 50ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To put that in perspective, 50ms is literally faster than the blink of a human eye. You can run this on every single file save without ever noticing a stutter in your workflow.&lt;/p&gt;

&lt;p&gt;By removing the overhead of a virtual machine or a heavy runtime, &lt;code&gt;gomarklint&lt;/code&gt; ensures that your documentation quality stays high without sacrificing your velocity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;p&gt;gomarklint doesn't just check syntax; it enforces a logical structure for your documentation. Here are the core rules it handles out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Heading Hierarchy Enforcement&lt;/strong&gt;: Ever seen a document jump from an H2 directly to an H4? It breaks the visual flow and accessibility. gomarklint ensures your heading levels follow a strict, logical sequence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Duplicate Heading Detection&lt;/strong&gt;: Identical headings in the same file can break anchor links (e.g., #features vs #features-1). We catch these early so your internal navigation never breaks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Broken Link Checker (Internal &amp;amp; External)&lt;/strong&gt;: This is my favorite. It scans your Markdown for links and validates them. No more 404s for your users when they click on a "Getting Started" guide or an external API reference.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Configuration via JSON&lt;/strong&gt;: While it works great with zero config, you can easily tweak rules or ignore specific paths using a simple &lt;code&gt;.gomarklint.json&lt;/code&gt; file.&lt;/p&gt;&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%2Fbhdmi7uoazb3b119roaf.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%2Fbhdmi7uoazb3b119roaf.png" alt=" " width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# install (choose one)
go install github.com/shinagawa-web/gomarklint@latest

# or clone and build manually
git clone https://github.com/shinagawa-web/gomarklint
cd gomarklint
make build   # or: go build ./cmd/gomarklint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1) Initialize config (optional but recommended)&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This creates .gomarklint.json with sensible defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "include": ["."],
  "ignore": ["node_modules", "vendor"],
  "minHeadingLevel": 2,
  "enableHeadingLevelCheck": true,
  "enableDuplicateHeadingCheck": true,
  "enableLinkCheck": false,
  "skipLinkPatterns": [],
  "outputFormat": "text"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can edit it anytime — CLI flags override config values.&lt;/p&gt;

&lt;p&gt;2) Run it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# lint current directory recursively
gomarklint ./...

# lint specific targets
gomarklint docs README.md internal/handbook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Next? (Roadmap)
&lt;/h2&gt;

&lt;p&gt;gomarklint is already stable and fast, but I have a clear vision for where it’s headed. I’m actively working on expanding its rule set to cover even more edge cases and best practices.&lt;/p&gt;

&lt;p&gt;Here’s what you can expect in the coming updates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;max-line-length Enforcement&lt;/strong&gt;: To keep your Markdown source files readable in any editor or GitHub's UI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image Alt-Text Validation&lt;/strong&gt;: Improving accessibility by ensuring every image has a descriptive alt attribute.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom Rules via JSON&lt;/strong&gt;: Giving you the power to define your own project-specific rules in .gomarklint.json.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auto-fixing (The Dream)&lt;/strong&gt;: While currently focused on linting, I’m exploring ways to automatically fix simple issues like heading level skips.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are Open for Contributions! If you have a rule in mind that would make your documentation better, or if you find a bug, please open an Issue or a Pull Request on GitHub. I’d love to build the future of this tool together with the community.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;Building gomarklint has been an incredible journey into the world of Go performance and static analysis. It started as a small tool for my own workflow, but I realized that many other developers are likely facing the same "slow linting" frustration.&lt;/p&gt;

&lt;p&gt;If you're looking for a way to keep your documentation spotless without adding bloat to your CI/CD, I’d be honored if you gave gomarklint a try.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check it out on GitHub&lt;/strong&gt;: &lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;shinagawa-web/gomarklint&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Give it a&lt;/strong&gt; ⭐: If you find the project useful, a Star would mean the world to me and helps others discover the tool!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m really curious to hear from you: &lt;strong&gt;What’s the most annoying thing you’ve encountered with Markdown formatting?&lt;/strong&gt; Let’s chat in the comments below!&lt;/p&gt;

&lt;p&gt;Happy hacking! 🚀&lt;/p&gt;

</description>
      <category>go</category>
      <category>performance</category>
      <category>showdev</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Building a Culture of Documentation Quality in CI/CD</title>
      <dc:creator>Kazu</dc:creator>
      <pubDate>Tue, 28 Oct 2025 23:55:59 +0000</pubDate>
      <link>https://dev.to/_402ccbd6e5cb02871506/building-a-culture-of-documentation-quality-in-cicd-352d</link>
      <guid>https://dev.to/_402ccbd6e5cb02871506/building-a-culture-of-documentation-quality-in-cicd-352d</guid>
      <description>&lt;h2&gt;
  
  
  Introduction: Documentation as a Cultural Signal
&lt;/h2&gt;

&lt;p&gt;In many engineering teams, documentation is treated as an afterthought—something we write when there is time, if there is time. Code reviews are systematic, CI pipelines are automated, testing culture is discussed and refined. Yet documentation, which shapes how teams share knowledge and coordinate decisions, is often left to individual discipline and goodwill.&lt;/p&gt;

&lt;p&gt;But documentation is not just a collection of text files. It is a cultural signal.&lt;/p&gt;

&lt;p&gt;How teams write (and maintain) documentation communicates what they value:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether knowledge is shared or siloed&lt;/li&gt;
&lt;li&gt;Whether onboarding is a guided process or a form of survival&lt;/li&gt;
&lt;li&gt;Whether decisions are remembered or rediscovered&lt;/li&gt;
&lt;li&gt;Whether the product’s reality matches its documentation—or not&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When documentation decays, trust decays with it. And trust is expensive to regain.&lt;/p&gt;

&lt;p&gt;The challenge, then, is not simply “writing more documentation,” but building a culture where documentation is treated as a part of the product itself—reviewed, maintained, and continuously improved alongside code.&lt;/p&gt;

&lt;p&gt;This is where treating documentation quality as a first-class citizen in CI/CD pipelines becomes transformative.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Broken Links Become Broken Trust
&lt;/h2&gt;

&lt;p&gt;Documentation is more than a reference—it is a promise.&lt;/p&gt;

&lt;p&gt;When we publish architecture diagrams, onboarding guides, operational runbooks, or product specifications, we are making a statement to our teammates:&lt;br&gt;
“This is how the system works. You can rely on this.”&lt;/p&gt;

&lt;p&gt;So what happens when a link is broken?&lt;br&gt;
Or when a README references outdated behavior?&lt;br&gt;
Or when a guide includes instructions that no longer reflect reality?&lt;/p&gt;

&lt;p&gt;People stop trusting the documentation.&lt;/p&gt;

&lt;p&gt;And once trust is gone, even perfectly correct documentation will be questioned.&lt;br&gt;
Teams start asking around instead of reading.&lt;br&gt;
Knowledge becomes interpersonal again.&lt;br&gt;
The organization quietly shifts from a shared knowledge model to a tribal knowledge model.&lt;/p&gt;

&lt;p&gt;This has real costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Onboarding slows down&lt;/li&gt;
&lt;li&gt;Decision-making becomes opaque&lt;/li&gt;
&lt;li&gt;Past lessons are lost and re-learned&lt;/li&gt;
&lt;li&gt;Review and collaboration rely on memory instead of artifacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why documentation quality is not a matter of neatness.&lt;br&gt;
It is a matter of organizational reliability.&lt;/p&gt;

&lt;p&gt;Broken links are not just broken links.&lt;br&gt;
Broken links are broken trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Documentation is Hard to Keep Healthy
&lt;/h2&gt;

&lt;p&gt;If we agree that documentation matters, the next question is:&lt;br&gt;
Why is it so hard to keep documentation healthy?&lt;/p&gt;

&lt;p&gt;Unlike code, documentation often has no single owner.&lt;br&gt;
It sits in a gray area between product, engineering, design, support, and operations.&lt;br&gt;
Everyone uses it.&lt;br&gt;
Everyone benefits from it.&lt;br&gt;
But no one is explicitly responsible for maintaining it.&lt;/p&gt;

&lt;p&gt;And unlike code, documentation does not fail loudly.&lt;br&gt;
There is no compiler complaining.&lt;br&gt;
No CI job turning red.&lt;br&gt;
No stack trace when the meaning shifts or when a sentence becomes outdated.&lt;/p&gt;

&lt;p&gt;Documentation decays silently.&lt;/p&gt;

&lt;p&gt;Because of this, teams tend to rely on good intentions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Someone will update it.”&lt;/li&gt;
&lt;li&gt;“We’ll fix it during the next sprint.”&lt;/li&gt;
&lt;li&gt;“We’ll remember this decision anyway.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But people don’t remember.&lt;br&gt;
Teams grow. People leave. Systems evolve.&lt;/p&gt;

&lt;p&gt;Without intentional mechanisms for maintenance, documentation inevitably drifts away from reality.&lt;/p&gt;

&lt;p&gt;The challenge is not that teams don’t care.&lt;br&gt;
The challenge is that documentation lacks the same structural safeguards that protect code quality.&lt;/p&gt;

&lt;p&gt;To keep documentation healthy, we need systems—not just effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat Documentation Like Code: Enter Linting
&lt;/h2&gt;

&lt;p&gt;If documentation decays because it lacks structural safeguards,&lt;br&gt;
then the solution is not “more effort,” but better infrastructure.&lt;/p&gt;

&lt;p&gt;We don’t maintain code quality through motivation or goodwill.&lt;br&gt;
We maintain it through systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code reviews&lt;/li&gt;
&lt;li&gt;Static analysis&lt;/li&gt;
&lt;li&gt;Typed contracts&lt;/li&gt;
&lt;li&gt;CI pipelines&lt;/li&gt;
&lt;li&gt;Test suites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These mechanisms don’t replace human judgment—they support it.&lt;br&gt;
They make the healthy behavior the default behavior.&lt;/p&gt;

&lt;p&gt;We can apply the same thinking to documentation.&lt;/p&gt;

&lt;p&gt;If we want our documentation to stay reliable,&lt;br&gt;
we need tools that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detect inconsistencies early&lt;/li&gt;
&lt;li&gt;Make issues visible, not silent&lt;/li&gt;
&lt;li&gt;Encourage correction at the moment of change&lt;/li&gt;
&lt;li&gt;Keep shared language stable across contributors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where &lt;strong&gt;documentation linting&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;Linting documentation is not about enforcing perfection.&lt;br&gt;
It’s about protecting shared understanding.&lt;/p&gt;

&lt;p&gt;It allows teams to shift from&lt;br&gt;
“we update documentation when we remember”&lt;br&gt;
to&lt;br&gt;
“documentation accuracy is continuously verified.”&lt;/p&gt;

&lt;p&gt;Not tighter control.&lt;br&gt;
Just better support for the culture we want to create.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing gomarklint: A Practical Linter for Markdown
&lt;/h2&gt;

&lt;p&gt;When we introduced linting into our documentation workflow, we didn’t want a tool that enforced aesthetics, personal style, or formatting fads.&lt;br&gt;
We wanted something simpler and more fundamental:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A tool that helps teams keep documentation trustworthy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This led to gomarklint — a Markdown linter designed to protect the structural integrity of documentation, without dictating how people should write.&lt;/p&gt;

&lt;p&gt;gomarklint focuses on things that directly affect shared understanding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecting broken or missing links&lt;/li&gt;
&lt;li&gt;Ensuring heading levels follow a coherent structure&lt;/li&gt;
&lt;li&gt;Highlighting ambiguous references or placeholder text&lt;/li&gt;
&lt;li&gt;Surfacing subtle inconsistencies that accumulate over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, it focuses on the parts of documentation that erode trust when they decay.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is intentionally minimal:&lt;/li&gt;
&lt;li&gt;No heavy configuration&lt;/li&gt;
&lt;li&gt;No stylistic judgment&lt;/li&gt;
&lt;li&gt;No unnecessary rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just the core checks that preserve reliability and continuity across contributors.&lt;/p&gt;

&lt;p&gt;This matters because a tool influences culture.&lt;br&gt;
If the tool is heavy, people avoid using it.&lt;br&gt;
If it is lightweight and aligned with purpose, it becomes part of everyday practice.&lt;/p&gt;

&lt;p&gt;gomarklint was built to be that kind of tool:&lt;br&gt;
Not restrictive.&lt;br&gt;
Just supportive.&lt;br&gt;
Not opinionated.&lt;br&gt;
Just reliable.&lt;/p&gt;

&lt;p&gt;A small guardrail that keeps the path clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;https://github.com/shinagawa-web/gomarklint&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Our Review Culture Changed After Adoption
&lt;/h2&gt;

&lt;p&gt;When we added documentation linting to our workflow, the most meaningful change was not the number of issues caught.&lt;br&gt;
It was the shift in how the team talked about documentation.&lt;/p&gt;

&lt;p&gt;Before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation updates were “nice to have”&lt;/li&gt;
&lt;li&gt;Reviewers focused almost entirely on code&lt;/li&gt;
&lt;li&gt;Outdated sections were noticed only when they caused confusion&lt;/li&gt;
&lt;li&gt;Small inconsistencies were tolerated because fixing them felt optional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After introducing gomarklint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation changes became part of the review conversation&lt;/li&gt;
&lt;li&gt;Reviewers felt permission to point out unclear naming, missing references, or outdated instructions&lt;/li&gt;
&lt;li&gt;Updating documentation became a natural part of making changes—not an afterthought&lt;/li&gt;
&lt;li&gt;The threshold for “good enough” rose, but without adding pressure or judgment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The presence of linting changed the default expectation:&lt;br&gt;
Documentation should reflect reality.&lt;/p&gt;

&lt;p&gt;And because the tool handles detection, the emotional weight disappears.&lt;br&gt;
Feedback shifted from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Why didn’t you update this?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The linter caught this—let’s align it.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This matters.&lt;br&gt;
Not because it catches errors, but because it reframes ownership:&lt;br&gt;
Documentation is something the team maintains, together.&lt;/p&gt;

&lt;p&gt;Not heroic effort.&lt;br&gt;
Not personal responsibility.&lt;br&gt;
Just shared care, supported by lightweight automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Documentation Quality is Organizational Health
&lt;/h2&gt;

&lt;p&gt;Documentation is not just a record of what we build.&lt;br&gt;
It is a reflection of &lt;strong&gt;how we work&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When documentation is reliable, knowledge is shared.&lt;br&gt;
Onboarding is smooth.&lt;br&gt;
Decisions have continuity.&lt;br&gt;
Teams move with confidence.&lt;/p&gt;

&lt;p&gt;When it decays, the opposite happens—slowly, quietly, and expensively.&lt;/p&gt;

&lt;p&gt;Treating documentation as part of the product is not about increasing process or control.&lt;br&gt;
It is about supporting the culture we want to have:&lt;/p&gt;

&lt;p&gt;A culture where knowledge is shared.&lt;br&gt;
Where change is understood.&lt;br&gt;
Where trust is maintained.&lt;/p&gt;

&lt;p&gt;Linting is not the solution by itself.&lt;br&gt;
But it is a small, powerful guardrail—one that keeps our shared understanding aligned with reality.&lt;/p&gt;

&lt;p&gt;Documentation quality is not a matter of neatness.&lt;br&gt;
It is a matter of organizational health.&lt;/p&gt;

&lt;p&gt;And when we care for it intentionally,&lt;br&gt;
the entire team feels the difference.&lt;/p&gt;




&lt;p&gt;If you'd like to explore the tool mentioned in this article:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gomarklint&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;https://github.com/shinagawa-web/gomarklint&lt;/a&gt;&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>cicd</category>
      <category>documentation</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Inside gomarklint: Architecture, Rule Engine, and How to Extend It</title>
      <dc:creator>Kazu</dc:creator>
      <pubDate>Mon, 13 Oct 2025 00:00:13 +0000</pubDate>
      <link>https://dev.to/_402ccbd6e5cb02871506/inside-gomarklint-architecture-rule-engine-and-how-to-extend-it-1377</link>
      <guid>https://dev.to/_402ccbd6e5cb02871506/inside-gomarklint-architecture-rule-engine-and-how-to-extend-it-1377</guid>
      <description>&lt;p&gt;When I released gomarklint, most people asked the same two questions:&lt;br&gt;
“How does it run so fast?” and “Can I add my own lint rules?”&lt;/p&gt;

&lt;p&gt;In this post, we’ll dive into the internals of gomarklint — how it parses Markdown efficiently, how rules are defined and executed, and how you can extend it to fit your own documentation style guide.&lt;br&gt;
If you’re a Go developer interested in building your own linter or contributing to gomarklint, this article is your roadmap inside the engine.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/shinagawa-web" rel="noopener noreferrer"&gt;
        shinagawa-web
      &lt;/a&gt; / &lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;
        gomarklint
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A fast and configurable Markdown linter written in Go
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;gomarklint&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/shinagawa-web/gomarklint/actions/workflows/test.yml/badge.svg"&gt;&lt;img src="https://github.com/shinagawa-web/gomarklint/actions/workflows/test.yml/badge.svg" alt="Test"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/3a62ea1c605d28ca8b76a989447c82d628955be97440820aa775fdaf031acd42/68747470733a2f2f636f6465636f762e696f2f67682f7368696e61676177612d7765622f676f6d61726b6c696e742f67726170682f62616467652e7376673f746f6b656e3d354d4743595a5a593753" alt="codecov"&gt;&lt;/a&gt;
&lt;a href="https://goreportcard.com/report/github.com/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5cc9b8570278e51d95dd927720684afeddb2594c78ae005a0886b29b2ca15039/68747470733a2f2f676f7265706f7274636172642e636f6d2f62616467652f6769746875622e636f6d2f7368696e61676177612d7765622f676f6d61726b6c696e74" alt="Go Report Card"&gt;&lt;/a&gt;
&lt;a href="https://pkg.go.dev/github.com/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6dfb8608e07cc6f5d15a43cf57ad1bcca3ab07f55909cc6b48380c79b1b4d277/68747470733a2f2f706b672e676f2e6465762f62616467652f6769746875622e636f6d2f7368696e61676177612d7765622f676f6d61726b6c696e742e737667" alt="Go Reference"&gt;&lt;/a&gt;
&lt;a href="https://github.com/shinagawa-web/gomarklint/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;English | &lt;a href="https://github.com/shinagawa-web/gomarklint/README.ja.md" rel="noopener noreferrer"&gt;日本語&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A fast, opinionated Markdown linter for engineering teams. Built in Go, designed for CI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Download binary&lt;/strong&gt; (no Go required):&lt;/p&gt;
&lt;p&gt;Download the latest binary for your platform from &lt;a href="https://github.com/shinagawa-web/gomarklint/releases/latest" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; macOS / Linux&lt;/span&gt;
tar -xzf gomarklint_Darwin_x86_64.tar.gz
sudo mv gomarklint /usr/local/bin/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or install to user-local directory (no sudo required)&lt;/span&gt;
mkdir -p &lt;span class="pl-k"&gt;~&lt;/span&gt;/.local/bin &lt;span class="pl-k"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mv gomarklint &lt;span class="pl-k"&gt;~&lt;/span&gt;/.local/bin/&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-powershell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Windows (PowerShell)&lt;/span&gt;
&lt;span class="pl-c1"&gt;Expand-Archive&lt;/span&gt; &lt;span class="pl-k"&gt;-&lt;/span&gt;Path gomarklint_Windows_x86_64.zip &lt;span class="pl-k"&gt;-&lt;/span&gt;DestinationPath &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;LOCALAPPDATA&lt;/span&gt;\Programs\gomarklint&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Add to PATH (run once)&lt;/span&gt;
[&lt;span class="pl-k"&gt;Environment&lt;/span&gt;]::SetEnvironmentVariable(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;PATH&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-k"&gt;,&lt;/span&gt; &lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;PATH&lt;/span&gt; &lt;span class="pl-k"&gt;+&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;;&lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;LOCALAPPDATA&lt;/span&gt;\Programs\gomarklint&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-k"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;User&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;)&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Via &lt;code&gt;go install&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go install github.com/shinagawa-web/gomarklint@latest&lt;/pre&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Catch broken links and headings before your docs ship.&lt;/li&gt;
&lt;li&gt;Enforce predictable structure (no more "why is this H4 under H2?").&lt;/li&gt;
&lt;li&gt;Output that's friendly for both humans and machines (JSON).&lt;/li&gt;
&lt;li&gt;Process &lt;strong&gt;100,000+ lines in ~170ms&lt;/strong&gt; — fast…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;h2&gt;
  
  
  Introduction: Why Architecture Matters
&lt;/h2&gt;

&lt;p&gt;When I released gomarklint, most people asked me the same two questions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How does it run so fast?”&lt;br&gt;
“Can I add my own lint rules?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Speed and simplicity don’t happen by accident — they come from design.&lt;br&gt;
gomarklint was built around three principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Performance first: it should scan tens of thousands of lines in milliseconds.&lt;/li&gt;
&lt;li&gt;Simplicity: configuration and execution must stay minimal.&lt;/li&gt;
&lt;li&gt;Extensibility: rules should be easy to read, add, and maintain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this post, I’ll open the hood of gomarklint — showing how it parses Markdown efficiently, how rules are executed, and how you can extend it to match your documentation style guide.&lt;/p&gt;
&lt;h2&gt;
  
  
  Parsing Markdown Efficiently
&lt;/h2&gt;

&lt;p&gt;At the heart of gomarklint is an extremely lightweight file parser.&lt;br&gt;
It doesn’t try to build a full Markdown AST. Instead, it performs structured text scanning that’s optimized for speed and memory locality.&lt;/p&gt;

&lt;p&gt;The typical flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;File Discovery: gomarklint walks through your repository recursively, ignoring hidden directories (.git, .vscode) and symbolic links.&lt;/li&gt;
&lt;li&gt;Line Reader: files are streamed line-by-line instead of loaded entirely into memory.&lt;/li&gt;
&lt;li&gt;Frontmatter Skipping: YAML frontmatters (--- ... ---) are automatically ignored to prevent false positives.&lt;/li&gt;
&lt;li&gt;Rule Evaluation: each active rule receives a slice of lines and emits lint errors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This minimal I/O footprint lets gomarklint handle 50,000+ lines in under 50ms — even on modest CI runners.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Rule Engine: Anatomy of a Lint Rule
&lt;/h2&gt;

&lt;p&gt;Rules in gomarklint are intentionally simple.&lt;br&gt;
Each rule is just a Go function that accepts file content and returns a list of &lt;code&gt;LintError&lt;/code&gt;s:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;LintError&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;File&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Line&lt;/span&gt;    &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;RuleFunc&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;LintError&lt;/span&gt;

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

&lt;/div&gt;


&lt;p&gt;A small example — detecting duplicate headings:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;CheckDuplicateHeadings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;LintError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;LintError&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;lines&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&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;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LintError&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;Line&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"duplicate heading: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&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="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&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="n"&gt;errs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;


&lt;p&gt;This pattern repeats across all built-in rules — headings, code blocks, blank lines, and more.&lt;br&gt;
gomarklint aggregates rule results concurrently, merges them, sorts by file/line, and prints them in a deterministic order.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding Your Own Rules
&lt;/h2&gt;

&lt;p&gt;Creating a custom rule is straightforward.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new file under &lt;code&gt;internal/rule/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Implement your rule function — for example, forbid “TODO” comments:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NoTODO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;LintError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;LintError&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;lines&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"TODO"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LintError&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Line&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"found TODO comment"&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errs&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;Register it in the rule set (usually in &lt;code&gt;rule/register.go&lt;/code&gt;):
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;Rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;RuleFunc&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;CheckDuplicateHeadings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NoTODO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c"&gt;// ...other rules&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;Rebuild and test:
&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="c"&gt;# Run all tests&lt;/span&gt;
go &lt;span class="nb"&gt;test&lt;/span&gt; ./...

&lt;span class="c"&gt;# Show CLI help from local source&lt;/span&gt;
go run &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--help&lt;/span&gt;

&lt;span class="c"&gt;# Generate a default .gomarklint.json (from your local build)&lt;/span&gt;
go run &lt;span class="nb"&gt;.&lt;/span&gt; init

&lt;span class="c"&gt;# Lint the included sample files in ./testdata&lt;/span&gt;
go run &lt;span class="nb"&gt;.&lt;/span&gt; testdata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can immediately see your new rule in action.&lt;br&gt;
If it’s useful for others, open a Pull Request — gomarklint’s rule library welcomes contributions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Configuration and Extensibility
&lt;/h2&gt;

&lt;p&gt;All configuration lives in .gomarklint.json, allowing teams to share consistent rules:&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;"ignore"&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;"CHANGELOG.md"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&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;"CheckDuplicateHeadings"&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;"NoTODO"&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;"ExternalLinks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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 can be toggled on/off, and some accept parameters (like link checking patterns or minimum heading depth).&lt;/p&gt;

&lt;p&gt;Future versions of gomarklint will likely support pluggable external rules, where Go plugins or YAML-based configuration can load custom logic at runtime — without forking the repo.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lessons from Designing for Extensibility
&lt;/h2&gt;

&lt;p&gt;Building gomarklint taught me that simplicity scales better than abstraction.&lt;/p&gt;

&lt;p&gt;Many linters become slower or harder to extend as they grow, because every rule depends on a shared complex framework.&lt;br&gt;
gomarklint took the opposite path — keep each rule isolated, independent, and stateless.&lt;/p&gt;

&lt;p&gt;That design choice allowed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linear scalability with repository size&lt;/li&gt;
&lt;li&gt;Easier testing and debugging&lt;/li&gt;
&lt;li&gt;Low entry barrier for contributors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extensibility doesn’t always mean adding layers.&lt;br&gt;
Sometimes, it’s about not adding them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion: Build Your Own Linter, or Improve This One
&lt;/h2&gt;

&lt;p&gt;gomarklint isn’t just a tool — it’s a lightweight foundation for building your own Markdown analysis workflows.&lt;/p&gt;

&lt;p&gt;Whether you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;want to enforce internal documentation standards,&lt;/li&gt;
&lt;li&gt;prototype your own linter ideas, or&lt;/li&gt;
&lt;li&gt;contribute a new rule back to the community —&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the architecture is open, simple, and waiting for you.&lt;/p&gt;

&lt;p&gt;Repository:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/shinagawa-web" rel="noopener noreferrer"&gt;
        shinagawa-web
      &lt;/a&gt; / &lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;
        gomarklint
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A fast and configurable Markdown linter written in Go
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;gomarklint&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/shinagawa-web/gomarklint/actions/workflows/test.yml/badge.svg"&gt;&lt;img src="https://github.com/shinagawa-web/gomarklint/actions/workflows/test.yml/badge.svg" alt="Test"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/3a62ea1c605d28ca8b76a989447c82d628955be97440820aa775fdaf031acd42/68747470733a2f2f636f6465636f762e696f2f67682f7368696e61676177612d7765622f676f6d61726b6c696e742f67726170682f62616467652e7376673f746f6b656e3d354d4743595a5a593753" alt="codecov"&gt;&lt;/a&gt;
&lt;a href="https://goreportcard.com/report/github.com/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5cc9b8570278e51d95dd927720684afeddb2594c78ae005a0886b29b2ca15039/68747470733a2f2f676f7265706f7274636172642e636f6d2f62616467652f6769746875622e636f6d2f7368696e61676177612d7765622f676f6d61726b6c696e74" alt="Go Report Card"&gt;&lt;/a&gt;
&lt;a href="https://pkg.go.dev/github.com/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6dfb8608e07cc6f5d15a43cf57ad1bcca3ab07f55909cc6b48380c79b1b4d277/68747470733a2f2f706b672e676f2e6465762f62616467652f6769746875622e636f6d2f7368696e61676177612d7765622f676f6d61726b6c696e742e737667" alt="Go Reference"&gt;&lt;/a&gt;
&lt;a href="https://github.com/shinagawa-web/gomarklint/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;English | &lt;a href="https://github.com/shinagawa-web/gomarklint/README.ja.md" rel="noopener noreferrer"&gt;日本語&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A fast, opinionated Markdown linter for engineering teams. Built in Go, designed for CI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Download binary&lt;/strong&gt; (no Go required):&lt;/p&gt;
&lt;p&gt;Download the latest binary for your platform from &lt;a href="https://github.com/shinagawa-web/gomarklint/releases/latest" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; macOS / Linux&lt;/span&gt;
tar -xzf gomarklint_Darwin_x86_64.tar.gz
sudo mv gomarklint /usr/local/bin/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or install to user-local directory (no sudo required)&lt;/span&gt;
mkdir -p &lt;span class="pl-k"&gt;~&lt;/span&gt;/.local/bin &lt;span class="pl-k"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mv gomarklint &lt;span class="pl-k"&gt;~&lt;/span&gt;/.local/bin/&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-powershell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Windows (PowerShell)&lt;/span&gt;
&lt;span class="pl-c1"&gt;Expand-Archive&lt;/span&gt; &lt;span class="pl-k"&gt;-&lt;/span&gt;Path gomarklint_Windows_x86_64.zip &lt;span class="pl-k"&gt;-&lt;/span&gt;DestinationPath &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;LOCALAPPDATA&lt;/span&gt;\Programs\gomarklint&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Add to PATH (run once)&lt;/span&gt;
[&lt;span class="pl-k"&gt;Environment&lt;/span&gt;]::SetEnvironmentVariable(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;PATH&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-k"&gt;,&lt;/span&gt; &lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;PATH&lt;/span&gt; &lt;span class="pl-k"&gt;+&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;;&lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;LOCALAPPDATA&lt;/span&gt;\Programs\gomarklint&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-k"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;User&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;)&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Via &lt;code&gt;go install&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go install github.com/shinagawa-web/gomarklint@latest&lt;/pre&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Catch broken links and headings before your docs ship.&lt;/li&gt;
&lt;li&gt;Enforce predictable structure (no more "why is this H4 under H2?").&lt;/li&gt;
&lt;li&gt;Output that's friendly for both humans and machines (JSON).&lt;/li&gt;
&lt;li&gt;Process &lt;strong&gt;100,000+ lines in ~170ms&lt;/strong&gt; — fast…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




</description>
      <category>programming</category>
      <category>go</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Inside gomarklint: Building a High-Performance Markdown Linter in Go</title>
      <dc:creator>Kazu</dc:creator>
      <pubDate>Sat, 09 Aug 2025 03:56:27 +0000</pubDate>
      <link>https://dev.to/_402ccbd6e5cb02871506/inside-gomarklint-building-a-high-performance-markdown-linter-in-go-2am1</link>
      <guid>https://dev.to/_402ccbd6e5cb02871506/inside-gomarklint-building-a-high-performance-markdown-linter-in-go-2am1</guid>
      <description>&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/shinagawa-web" rel="noopener noreferrer"&gt;
        shinagawa-web
      &lt;/a&gt; / &lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;
        gomarklint
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A fast and configurable Markdown linter written in Go
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;gomarklint&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/shinagawa-web/gomarklint/actions/workflows/test.yml/badge.svg"&gt;&lt;img src="https://github.com/shinagawa-web/gomarklint/actions/workflows/test.yml/badge.svg" alt="Test"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/3a62ea1c605d28ca8b76a989447c82d628955be97440820aa775fdaf031acd42/68747470733a2f2f636f6465636f762e696f2f67682f7368696e61676177612d7765622f676f6d61726b6c696e742f67726170682f62616467652e7376673f746f6b656e3d354d4743595a5a593753" alt="codecov"&gt;&lt;/a&gt;
&lt;a href="https://goreportcard.com/report/github.com/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5cc9b8570278e51d95dd927720684afeddb2594c78ae005a0886b29b2ca15039/68747470733a2f2f676f7265706f7274636172642e636f6d2f62616467652f6769746875622e636f6d2f7368696e61676177612d7765622f676f6d61726b6c696e74" alt="Go Report Card"&gt;&lt;/a&gt;
&lt;a href="https://pkg.go.dev/github.com/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6dfb8608e07cc6f5d15a43cf57ad1bcca3ab07f55909cc6b48380c79b1b4d277/68747470733a2f2f706b672e676f2e6465762f62616467652f6769746875622e636f6d2f7368696e61676177612d7765622f676f6d61726b6c696e742e737667" alt="Go Reference"&gt;&lt;/a&gt;
&lt;a href="https://github.com/shinagawa-web/gomarklint/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;English | &lt;a href="https://github.com/shinagawa-web/gomarklint/README.ja.md" rel="noopener noreferrer"&gt;日本語&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A fast, opinionated Markdown linter for engineering teams. Built in Go, designed for CI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Download binary&lt;/strong&gt; (no Go required):&lt;/p&gt;
&lt;p&gt;Download the latest binary for your platform from &lt;a href="https://github.com/shinagawa-web/gomarklint/releases/latest" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; macOS / Linux&lt;/span&gt;
tar -xzf gomarklint_Darwin_x86_64.tar.gz
sudo mv gomarklint /usr/local/bin/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or install to user-local directory (no sudo required)&lt;/span&gt;
mkdir -p &lt;span class="pl-k"&gt;~&lt;/span&gt;/.local/bin &lt;span class="pl-k"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mv gomarklint &lt;span class="pl-k"&gt;~&lt;/span&gt;/.local/bin/&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-powershell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Windows (PowerShell)&lt;/span&gt;
&lt;span class="pl-c1"&gt;Expand-Archive&lt;/span&gt; &lt;span class="pl-k"&gt;-&lt;/span&gt;Path gomarklint_Windows_x86_64.zip &lt;span class="pl-k"&gt;-&lt;/span&gt;DestinationPath &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;LOCALAPPDATA&lt;/span&gt;\Programs\gomarklint&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Add to PATH (run once)&lt;/span&gt;
[&lt;span class="pl-k"&gt;Environment&lt;/span&gt;]::SetEnvironmentVariable(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;PATH&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-k"&gt;,&lt;/span&gt; &lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;PATH&lt;/span&gt; &lt;span class="pl-k"&gt;+&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;;&lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;LOCALAPPDATA&lt;/span&gt;\Programs\gomarklint&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-k"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;User&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;)&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Via &lt;code&gt;go install&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go install github.com/shinagawa-web/gomarklint@latest&lt;/pre&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Catch broken links and headings before your docs ship.&lt;/li&gt;
&lt;li&gt;Enforce predictable structure (no more "why is this H4 under H2?").&lt;/li&gt;
&lt;li&gt;Output that's friendly for both humans and machines (JSON).&lt;/li&gt;
&lt;li&gt;Process &lt;strong&gt;100,000+ lines in ~170ms&lt;/strong&gt; — fast…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h1&gt;
  
  
  Intro
&lt;/h1&gt;

&lt;p&gt;Most Markdown linters focus on correctness, but often at the expense of speed, flexibility, or ease of integration. When you’re working in a large repository with thousands of Markdown files, even a small slowdown in linting can make every CI run feel sluggish.&lt;/p&gt;

&lt;p&gt;gomarklint is my answer to that problem — a fast, minimal, CI-friendly Markdown linter written in Go. It checks for common issues like inconsistent heading levels, duplicate headings, unclosed code blocks, and optional external link validation. It’s designed to scan tens of thousands of lines in under 50ms, while staying lightweight enough to configure in seconds.&lt;/p&gt;

&lt;p&gt;In this article, we’ll go under the hood of gomarklint: how it’s structured, how it parses Markdown efficiently, how the rules engine works, and which optimizations make it fast. But before we dive into the internals, let’s take a quick look at how to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start — Using gomarklint
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;

&lt;p&gt;You can install via Go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/shinagawa-web/gomarklint@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Initialize Config
&lt;/h3&gt;

&lt;p&gt;This creates a &lt;code&gt;.gomarklint.json&lt;/code&gt; file with default rules and file patterns.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gomarklint init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Run Linting
&lt;/h3&gt;

&lt;p&gt;By default, it checks for heading consistency, duplicate headings, unclosed code blocks, and missing trailing blank lines.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gomarklint ./docs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Optional — GitHub Actions Integration
&lt;/h3&gt;

&lt;p&gt;Use the &lt;a href="https://github.com/marketplace/actions/gomarklint-markdown-linter" rel="noopener noreferrer"&gt;official GitHub Action&lt;/a&gt; to run linting automatically on every PR.&lt;/p&gt;
&lt;h2&gt;
  
  
  Overview of the Architecture
&lt;/h2&gt;

&lt;p&gt;gomarklint’s structure is intentionally simple. The &lt;code&gt;cmd/&lt;/code&gt; package contains the CLI entry point, built with Cobra for easy flag handling. Core logic lives in &lt;code&gt;internal/&lt;/code&gt;, which is split into &lt;code&gt;rule/&lt;/code&gt; for linting rules and &lt;code&gt;parser/&lt;/code&gt; for file handling. This separation makes it easy to add new rules without touching CLI code.&lt;/p&gt;
&lt;h2&gt;
  
  
  Parsing Markdown Files Efficiently
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;parser.ExpandPaths()&lt;/code&gt; function recursively finds &lt;code&gt;.md&lt;/code&gt; files while ignoring hidden directories and symlinks. It also applies &lt;code&gt;include&lt;/code&gt; and &lt;code&gt;ignore&lt;/code&gt; patterns from the config file. YAML frontmatter is detected and skipped so that headings inside it don’t trigger false positives.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rule Engine Design
&lt;/h2&gt;

&lt;p&gt;Each rule is a self-contained function that returns a list of &lt;code&gt;LintError&lt;/code&gt; structs, each containing the file name, line number, and message. Errors are sorted by line number before output. This keeps the codebase predictable and easy to extend.&lt;/p&gt;
&lt;h2&gt;
  
  
  Output &amp;amp; CI Integration
&lt;/h2&gt;

&lt;p&gt;gomarklint supports both text and JSON output. JSON mode is especially useful for CI pipelines, where structured output can be parsed by other tools. The program exits with a non-zero status if any errors are found, but only when running in CI (&lt;code&gt;GITHUB_ACTIONS=true&lt;/code&gt;), so local runs don’t break your workflow unnecessarily.&lt;/p&gt;
&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;Because performance was a priority from the start, file reading and rule checks are optimized to minimize allocations. In most cases, link checking is the only operation that significantly impacts runtime, and it’s disabled by default for speed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try It on Your Project
&lt;/h2&gt;

&lt;p&gt;If you want to keep your Markdown clean and consistent without slowing down your workflow, try gomarklint today:&lt;/p&gt;

&lt;p&gt;Repository:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/shinagawa-web" rel="noopener noreferrer"&gt;
        shinagawa-web
      &lt;/a&gt; / &lt;a href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;
        gomarklint
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A fast and configurable Markdown linter written in Go
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;gomarklint&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/shinagawa-web/gomarklint/actions/workflows/test.yml/badge.svg"&gt;&lt;img src="https://github.com/shinagawa-web/gomarklint/actions/workflows/test.yml/badge.svg" alt="Test"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/3a62ea1c605d28ca8b76a989447c82d628955be97440820aa775fdaf031acd42/68747470733a2f2f636f6465636f762e696f2f67682f7368696e61676177612d7765622f676f6d61726b6c696e742f67726170682f62616467652e7376673f746f6b656e3d354d4743595a5a593753" alt="codecov"&gt;&lt;/a&gt;
&lt;a href="https://goreportcard.com/report/github.com/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5cc9b8570278e51d95dd927720684afeddb2594c78ae005a0886b29b2ca15039/68747470733a2f2f676f7265706f7274636172642e636f6d2f62616467652f6769746875622e636f6d2f7368696e61676177612d7765622f676f6d61726b6c696e74" alt="Go Report Card"&gt;&lt;/a&gt;
&lt;a href="https://pkg.go.dev/github.com/shinagawa-web/gomarklint" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6dfb8608e07cc6f5d15a43cf57ad1bcca3ab07f55909cc6b48380c79b1b4d277/68747470733a2f2f706b672e676f2e6465762f62616467652f6769746875622e636f6d2f7368696e61676177612d7765622f676f6d61726b6c696e742e737667" alt="Go Reference"&gt;&lt;/a&gt;
&lt;a href="https://github.com/shinagawa-web/gomarklint/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;English | &lt;a href="https://github.com/shinagawa-web/gomarklint/README.ja.md" rel="noopener noreferrer"&gt;日本語&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A fast, opinionated Markdown linter for engineering teams. Built in Go, designed for CI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Download binary&lt;/strong&gt; (no Go required):&lt;/p&gt;
&lt;p&gt;Download the latest binary for your platform from &lt;a href="https://github.com/shinagawa-web/gomarklint/releases/latest" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; macOS / Linux&lt;/span&gt;
tar -xzf gomarklint_Darwin_x86_64.tar.gz
sudo mv gomarklint /usr/local/bin/
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; or install to user-local directory (no sudo required)&lt;/span&gt;
mkdir -p &lt;span class="pl-k"&gt;~&lt;/span&gt;/.local/bin &lt;span class="pl-k"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mv gomarklint &lt;span class="pl-k"&gt;~&lt;/span&gt;/.local/bin/&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-powershell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Windows (PowerShell)&lt;/span&gt;
&lt;span class="pl-c1"&gt;Expand-Archive&lt;/span&gt; &lt;span class="pl-k"&gt;-&lt;/span&gt;Path gomarklint_Windows_x86_64.zip &lt;span class="pl-k"&gt;-&lt;/span&gt;DestinationPath &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;LOCALAPPDATA&lt;/span&gt;\Programs\gomarklint&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Add to PATH (run once)&lt;/span&gt;
[&lt;span class="pl-k"&gt;Environment&lt;/span&gt;]::SetEnvironmentVariable(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;PATH&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-k"&gt;,&lt;/span&gt; &lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;PATH&lt;/span&gt; &lt;span class="pl-k"&gt;+&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;;&lt;span class="pl-smi"&gt;$&lt;span class="pl-c1"&gt;env:&lt;/span&gt;LOCALAPPDATA&lt;/span&gt;\Programs\gomarklint&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-k"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;User&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;)&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Via &lt;code&gt;go install&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go install github.com/shinagawa-web/gomarklint@latest&lt;/pre&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Catch broken links and headings before your docs ship.&lt;/li&gt;
&lt;li&gt;Enforce predictable structure (no more "why is this H4 under H2?").&lt;/li&gt;
&lt;li&gt;Output that's friendly for both humans and machines (JSON).&lt;/li&gt;
&lt;li&gt;Process &lt;strong&gt;100,000+ lines in ~170ms&lt;/strong&gt; — fast…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/shinagawa-web/gomarklint" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;GitHub Action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/marketplace/actions/gomarklint-markdown-linter" rel="noopener noreferrer"&gt;https://github.com/marketplace/actions/gomarklint-markdown-linter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clone the repo, install it, and run gomarklint init — you’ll be linting your docs in under a minute.&lt;/p&gt;

</description>
      <category>go</category>
      <category>markdown</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
