Migrating from Other Markdown Linters#

This guide helps teams switching to gomarklint from markdownlint, remark-lint, or textlint. It covers installation replacement, rule mapping, configuration conversion, and CI migration.

Why switch?

  • Single static binary — no Node.js or Python runtime required
  • Fast concurrent linting via goroutines
  • First-class CI integration with a native GitHub Action
  • Frontmatter-aware parsing (YAML/TOML stripped before linting)
  • Live HTTP link validation built in (external-link rule)

Installation#

Remove your existing linter and install gomarklint:

# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/shinagawa-web/gomarklint/main/install.sh | sh

# Homebrew
brew install shinagawa-web/tap/gomarklint

# npm
npm install -g @shinagawa-web/gomarklint

# go install
go install github.com/shinagawa-web/gomarklint/v3@latest

Remove the old linter from your project:

# if you used markdownlint-cli2:
npm uninstall markdownlint-cli2

# if you used remark-lint:
npm uninstall remark remark-cli remark-lint remark-preset-lint-recommended

# if you used textlint:
npm uninstall textlint textlint-rule-*

If you used npm only to run the linter, you may be able to remove package.json entirely and replace the script entry with the gomarklint binary.


markdownlint#

markdownlint rule mapping#

markdownlint rulegomarklint ruleNotes
MD001 heading-incrementheading-levelChecks heading level progression
MD003 heading-styleno-setext-headingsATX enforcement only; setext detection is the default check
MD004 ul-styleconsistent-list-markerOptions: consistent | dash | asterisk | plus
MD009 no-trailing-spacesNot yet implemented
MD010 no-hard-tabsno-hard-tabs
MD012 no-multiple-blanksno-multiple-blank-lines
MD013 line-lengthmax-line-lengthDefault off; set lineLength option
MD022 blanks-around-headingsblanks-around-headings
MD024 no-duplicate-headingduplicate-heading
MD025 single-h1single-h1
MD026 no-trailing-punctuationno-trailing-punctuationpunctuation option configures the character set
MD031 blanks-around-fencesblanks-around-fences
MD032 blanks-around-listsblanks-around-lists
MD034 no-bare-urlsno-bare-urls
MD036 no-emphasis-as-headingno-emphasis-as-headingPunctuation-ending spans are excluded
MD040 fenced-code-languagefenced-code-language
MD041 first-line-headingNo direct equivalent — heading-level checks level progression but does not require a heading on the first line
MD042 no-empty-linksno-empty-linksAlso catches [](#) and [](<>)
MD045 no-alt-textempty-alt-text
MD047 single-trailing-newlinefinal-blank-line
MD048 code-fence-styleconsistent-code-fenceOptions: consistent | backtick | tilde
MD049 emphasis-styleconsistent-emphasis-styleOptions: consistent | asterisk | underscore
MD051 link-fragmentslink-fragmentsConfigurable slug algorithm; default off
MD052 reference-links-imagesNot yet implemented
MD053 link-image-styleNot yet implemented

Rules without a gomarklint equivalent yet: MD005–MD008 (list indent), MD011, MD014, MD018–MD021 (heading spaces), MD023, MD027–MD030, MD033, MD035, MD037–MD039, MD043, MD044, MD046, MD050, MD054–MD056, MD058, MD059.

markdownlint config conversion#

Before (.markdownlint.json / .markdownlint-cli2.jsonc):

{
  "default": true,
  "MD013": { "line_length": 120 },
  "MD024": false,
  "MD026": { "punctuation": ".,;:!?" }
}

After (.gomarklint.json):

{
  "default": true,
  "rules": {
    "max-line-length": { "enabled": true, "lineLength": 120 },
    "duplicate-heading": false,
    "no-trailing-punctuation": { "punctuation": ".,;:!?" }
  }
}

Key differences:

  • Rule keys are descriptive names (no-trailing-punctuation) rather than MD numbers (MD026).
  • All rule configuration lives under rules.<rule-key>.
  • false disables a rule; true enables it with defaults; an object enables it with options.

markdownlint CI migration#

Before (GitHub Actions with markdownlint-cli2):

- uses: DavidAnson/markdownlint-cli2-action@v18
  with:
    globs: "**/*.md"

After (gomarklint):

- uses: actions/setup-go@v5
  with:
    go-version-file: 'go.mod'

- uses: shinagawa-web/gomarklint-action@v1

remark-lint#

remark-lint rule mapping#

remark-lint rulegomarklint ruleNotes
final-newlinefinal-blank-line
no-consecutive-blank-linesno-multiple-blank-lines
heading-incrementheading-level
first-heading-levelNo direct equivalent — heading-level checks level progression but does not enforce position
no-duplicate-headingsduplicate-heading
heading-style (atx)no-setext-headings
no-missing-blank-linesblanks-around-headings, blanks-around-lists, blanks-around-fencesremark-lint combines these; gomarklint has separate rules
no-literal-urlsno-bare-urls
no-empty-urlno-empty-links
no-emphasis-as-headingno-emphasis-as-heading
fenced-code-flagfenced-code-language
no-multiple-toplevel-headingssingle-h1
maximum-line-lengthmax-line-lengthDefault off
no-tabsno-hard-tabs
no-heading-punctuationno-trailing-punctuation
fenced-code-markerconsistent-code-fence
emphasis-markerconsistent-emphasis-style
unordered-list-marker-styleconsistent-list-marker
no-undefined-referencesNot yet implemented (Priority 3)
hard-break-spacesno-trailing-spaces not yet implemented
linebreak-styleconsistent-line-endings not yet implemented

remark-lint config conversion#

Before (.remarkrc.mjs):

import remarkPresetLintRecommended from 'remark-preset-lint-recommended'
import remarkLintMaximumLineLength from 'remark-lint-maximum-line-length'
import remarkLintEmphasisMarker from 'remark-lint-emphasis-marker'

export default {
  plugins: [
    remarkPresetLintRecommended,
    [remarkLintMaximumLineLength, 120],
    [remarkLintEmphasisMarker, '*'],
  ],
}

After (.gomarklint.json):

{
  "default": true,
  "rules": {
    "max-line-length": { "enabled": true, "lineLength": 120 },
    "consistent-emphasis-style": { "style": "asterisk" }
  }
}

remark-lint CI migration#

Before (GitHub Actions with remark-cli):

- name: Install remark
  run: npm install -g remark-cli remark-preset-lint-recommended

- name: Lint Markdown
  run: remark --use remark-preset-lint-recommended .

After:

- uses: actions/setup-go@v5
  with:
    go-version-file: 'go.mod'

- uses: shinagawa-web/gomarklint-action@v1

textlint#

textlint focuses primarily on prose style (word choice, grammar, writing conventions). gomarklint focuses on Markdown structure. The overlap is narrow — only structural rules have equivalents.

textlint rule mapping#

textlint rule / plugingomarklint ruleNotes
@textlint-rule/no-duplicate-headingduplicate-heading
textlint-rule-no-empty-elementno-empty-links, empty-alt-textPartial overlap
textlint-rule-no-todoNo equivalent
textlint-rule-ja-*Japanese prose rules; no equivalent
textlint-rule-spellcheck-tech-wordNo equivalent

Most textlint rules (terminology, spacing, punctuation style for prose) have no structural equivalent in gomarklint. If you use textlint for writing quality checks, you can run both tools side by side during a transition period.

Running both tools in parallel#

{
  "scripts": {
    "lint:structure": "gomarklint .",
    "lint:prose": "textlint **/*.md",
    "lint": "npm run lint:structure && npm run lint:prose"
  }
}

Handling uncovered rules#

Rules not yet implemented in gomarklint (from the roadmap):

Planned rulemarkdownlint equivalentremark-lint equivalentPriority
no-trailing-spacesMD009hard-break-spacesPriority 3
no-undefined-referencesMD052/MD053no-undefined-referencesPriority 3
consistent-line-endingslinebreak-stylePriority 3
table-formattingMD055/MD056/MD058table-pipes, table-cell-paddingPriority 3
descriptive-link-textMD059Priority 3

To request new rules or track progress, see issue #76.


FAQ#

Can I run gomarklint alongside markdownlint during migration?

Yes. Both tools can run independently on the same files. Use severity warning in gomarklint while you fix violations:

{
  "default": true,
  "rules": {
    "no-bare-urls": { "severity": "warning" }
  }
}

How do I gradually adopt gomarklint?

Start with default: false and enable only the rules you want to enforce first. Add more rules incrementally as your team addresses violations:

{
  "default": false,
  "rules": {
    "final-blank-line": true,
    "fenced-code-language": true
  }
}

Does gomarklint support inline disable comments?

Yes. Use <!-- gomarklint-disable --> and <!-- gomarklint-enable --> to suppress violations for a block, or <!-- gomarklint-disable rule-name --> to disable a specific rule. See the disable comments page.

Does gomarklint support per-directory config files?

Not automatically. markdownlint-cli2 traverses directories and applies the nearest .markdownlint.json it finds — gomarklint does not do this yet. The workaround is to exclude the subdirectory from the root config and run gomarklint a second time with an explicit --config:

{
  "ignore": ["docs/"]
}
gomarklint .                                   # root config
gomarklint docs/ --config docs/.gomarklint.json  # docs-specific config

Does gomarklint support shared configs across repositories?

Not yet. There is no extends mechanism to inherit from an npm package or a remote config file. Each repository maintains its own .gomarklint.json.