check-markdown-files: pre-commit checks for blog postings written in Markdown

Posted by ads on Monday, 2025-07-28
Posted in [Hugo][Software]

I wrote a thing which helps me write better blog postings.

After switching my blogs from S9Y to a static blogging engine (goHugo), I found a number of preventable issues. Certain syntax errors, forgot tags, or forgot a description, or forgot a preview picture, or forgot an image. This kind of things. So I wrote check-markdown-files, which now scans all my blog postings before committing the posting to git. Any known error is raised, and aborts the commit. Each problem has an override switch in case it is a false positive.

How does this work

The script check-markdown-files.py looks for a check-markdown-files.conf in the repository. This configuration file enables or disables checks for this blog - not every blog needs the same checks, or all checks for that matter.

By default, check-markdown-files.py scans every index.md which is newer than check-markdown-files.conf - this avoids re-scanning the entire blog all the time. You can force a full scan with the -a option. This relies on Hugo Page Bundles, which place every blog posting in a separate directory, with one index.md per blog posting.

Examples!

That’s nice, but show it to me!

Certainly, here’s an example from the time I was writing this blog posting:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
File: content/post/2025-07-28_check-markdown-files-pre-posting-checks-for-blog-postings-written-in-markdown/index.md
Found 1 line with whitespaces at the end
  Use 'skip_whitespaces_at_end' in 'suppresswarnings' to silence this warning
Found 'changeme' tag!
  Use 'skip_changeme_tag' in 'suppresswarnings' to silence this warning
Found 'changeme' category!
  Use 'skip_changeme_category' in 'suppresswarnings' to silence this warning
Found no preview image in header
  Use 'skip_preview_thumbnail' in 'suppresswarnings' to silence this warning
Found no preview description in header
  Use 'skip_preview_description' in 'suppresswarnings' to silence this warning
Frontmatter entry too short: description (0 < 10 chars): description
  Use 'skip_header_field_length_description' in 'suppresswarnings' to silence this warning
Frontmatter entry too short: thumbnail (0 < 5 chars): thumbnail
  Use 'skip_header_field_length_thumbnail' in 'suppresswarnings' to silence this warning
Removing whitespaces at end of lines
INFO: File is CHANGED!
INFO: Write changed file (content/post/2025-07-28_check-markdown-files-pre-posting-checks-for-blog-postings-written-in-markdown/index.md)

Ok, this blog posting is still in draft stage, here are the issues found:

  • Whitespaces at the end of lines (this can both raise a warning as well as remove the whitespaces for you - configurable). It’s just something I don’t like, trailing white spaces in a text.
  • Found changeme in tags and categories: my Archetype template has changeme in both Frontmatter fields by default, this forces me to change both fields and don’t forget about it. The fields and the blocked content are configurable.
  • No preview in description: Forces me to add a proper description as blog preview.
  • No thumbnail as preview image: Forces me to add a preview image and not rely on the first image in the blog posting.
  • No preview image in header: This check finds an image before the content summary field, which will look ugly in previews. Using description and thumbnail overrides this problem.

Overrides?

Some of the checks and the issues raised must be resolved. Like when I forgot to add a description, or tags.

Other checks raise potential issues, which may or may not be a problem.

As example, the check_double_brackets check will scan the posting for opening (( and closing )). While this is often correct, as example when adding a Wikipedia link which includes () in a Markdown link, sometimes it’s not. My editor automatically adds a closing ) when I write an opening ( - and I might have missed that and manually added another ).

After seeing the warning, I scan the blog posting if the (( or )) are indeed correct, and add an override:

1
2
3
suppresswarnings:
- skip_double_brackets_opening
- skip_double_brackets_closing

Other common mistakes I make, to list a few:

  • I use Markdown headers ### or ####. While that is sometimes ok, I aim to stick with ##. Hence the script in my configuration will raise a warning for such headers.
  • Missing tags: The script allows a list of words, which when appear in the blog posting also are supposed to be tags. Everything from Linux distribution names, city names to country names or whatever you add in the list. This check makes it easy for me not to forget to add tags.
  • cross-linked tags: When I add one tag, I also want to have another related tag. Like united-states and usa. Well, in my old blog I sometimes added one, or the other. And ended up with both tags, and search engine crawlers looking for both tags. Now I enforce that blog postings have both tags, if the posting has one tag.
  • Something simple: I and I'm. The I is written uppercase in English, but I might miss it. This is a warning.
  • http:// links. Let’s make sure the web is encrypted, every http:// link is a warning. Can be overridden, because not every URL is available as https://.
  • Embarassing: I posted blog postings which include a local goHugo preview link (http://localhost:1313/). Obviouslly this link does not work, therefore this is a warning.
  • Image sizes: scans every image in the blog posting directory, and raises a warning if it is larger than a defined threshold. Ignores files which are ignored by git. This ensures that I resize pictures before posting a blog post.
  • Image does not exist: scans the blog posting and raises an error for every local image which does not exist in the filesystem.
  • Forbidden words: list of words which can’t appear in a blog posting.
  • Forbidden websites: list of websites which can’t appear in a posting. This helps me, in the past I posted links to crappy link aggregator websites, instead using the real link. Now the crappy website is a warning.
  • FIXME are a warning: when I want to fix something later, I add a FIXME. This check helps me not to forget to actually fix the issue!
  • Double uppercase: I have the tendency to release the shift key too slow, and sometimes end up with TWo UPpercase CHaracters. This check finds those problems.

The GitHub repository has a full list of available checks and overrides, which can be enabled on a per-blog basis. By the time I write this blog posting, 38 different checks are available.

Installation

Your blog must live in a git repository, that’s kind of a prerequisite. And it must use Page Bundles, or at least directories for blog postings.

  • Check out the git repository somewhere on your system.
  • Point .git/hooks/pre-commit to the check-markdown-files.py script.
  • Copy check-markdown-files.conf into your blog directory, and modify it.

Whenever you run git commit, the check-markdown-files.py script will run and check all newly modified blog postings.

You can also run it manually on one blog posting:

1
check-markdown-files.py content/post/2025-07-28_check-markdown-files-pre-commit-checks-for-blog-postings-written-in-markdown/

Or

1
check-markdown-files.py content/post/2025-07-28_check-markdown-files-pre-commit-checks-for-blog-postings-written-in-markdown/index.md

Both versions work.

Feature requests

New feature requests are welcome. Please open an issue on GitHub or post a comment. Please describe what kind of checks you are looking for.

There’s also a large number of tests included which helps adding new features without breaking existing functionality.

Other tools

While posting previously on Social Media about this tool, I was pointed to Vale Linting. This is not the same though, check-markdown-files.py does a large number of additionally checks beyond simple linting of the text.


Categories: [Hugo] [Software]

Comments

With an account on the Fediverse or Mastodon, you can respond to this post. Known replies are displayed below:



Note: This will load data from mastodon.social.

Learn how this is implemented here.


Share: