My Blog Broke. So I Built a Tool to Prevent That.
I built a small Go CLI tool this week — blogtools. I write my posts using markdown files. Each one contains a frontmatter block — metadata placed at the very top of the file that tells my blog the post title, date, tags, and reading time.
---
title: "Hello."
date: "2026-05-17"
tags: ["Personal"]
excerpt: "Blogging is something I always wanted to try, and I finally got the courage to start."
readingTime: "2 min read"
---
While writing my first blog and testing it locally. I forgot some fields and the website broke.
That's where i got the first idea of building it. And it seems the right idea to prioritize building it, before publishing any farther posts.
And it is just too hard to check if all fields exists each time, things need to be automated that is more fun 😅 Now with blogtools I know exactly what i missed :
.\blogtools.exe frontmatter .\content\posts\hello.mdx
file .\content\posts\hello.mdx
valid false
errors 2
errors
tags: missing
readingTime: missingSo why build blogtools at all?
Why not just use a method that validates the missing metadata of a post inside my website code.
export type PostMeta = {
title: string;
date: string;
tags: string[];
excerpt: string;
readingTime: string;
};
function validateFrontmatter(data: Partial<PostMeta>, filename: string) {
const required = ['title', 'date', 'tags', 'excerpt', 'readingTime']
const missing = required.filter(field => !data[field as keyof PostMeta])
if (missing.length > 0) {
throw new Error(
`Invalid frontmatter in ${filename} — missing: ${missing.join(', ')}`
)
}
}Technically that should work, and the broken posts won't get deployed, but i would hate finding out on production, plus it would work just for this website project.
That's why I build blogtools :
- I can run it on any markdown, not just this blog.
- It works in CI, in a pre-commit hook, in any terminal
- It's not tied to Next.js or any framework
Structuring it as a package not one big file
Well simply because that would be a nightmare. Imagine a garage full of hand tools everywhere, it's unmanageable and it gets messy real quick.
So using packages is me organizing the hand tools or blog tools in my case into drawers with labels. That way when wanting to modify or upgrade a tool it would be much easier to do.
Here is what blogtools contains so far:
blogtools/
├── readingtime/
├── slugify/
└── frontmatter/
readingtime/: Package that estimates the reading time of markdown content based on average reading speed.slugify/: Package that converts strings into URL-safe slugs.frontmatter/: Package that validates the markdown frontmatter fields.
Writing unit tests
When I started, I went directly into coding the blog tools. without having an entry point main.go.
So that's where the unit test came in handy.
func TestValidate(t *testing.T) {
tests := []struct {
name string
input string
expectedValid bool
expectedErrors int
}{
{
name: "invalid date format",
input: `---
title: "Test Post"
date: "01-06-2025"
tags: ["Go"]
excerpt: "Some excerpt."
readingTime: "3 min read"
---
Content.`,
expectedValid: false,
expectedErrors: 1,
},
{
name: "empty tags array",
input: `---
title: "Test Post"
date: "2025-06-01"
tags: []
excerpt: "Some excerpt."
readingTime: "3 min read"
---
Content.`,
expectedValid: false,
expectedErrors: 1,
},
...
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Validate(tt.input)
if result.Valid != tt.expectedValid {
t.Errorf("Valid: got %v, want %v", result.Valid, tt.expectedValid)
}
if len(result.Errors) != tt.expectedErrors {
t.Errorf("Errors: got %d, want %d\nerrors: %v",
len(result.Errors), tt.expectedErrors, result.Errors)
}
})
}
}That's too much work you may say, and i would agree with you before. But here how I see it now:
- Unit tests are not one time use only — they stay with your project permanently, so when adding a new feature or new changes, you make sure the old test cases are not broken, and even if it did you will know exactly where, that's a lot of time saved.
Publishing it as a proper Go module with versioning
This was my first time publishing a Go module. I didn't know that any public GitHub repo is automatically importable as a Go module. You just tag a release and go get works.
go get github.com/ayoubnachti/blogtools@v0.1.0That's it. No registry, no publishing step, no account to create. Go's proxy handles it automatically.
I found that simpler than I expected, and more satisfying than I expected too.
Wiring it into the blog workflow
Here is how my current blog posting workflow works:
Layer 1 — pre-commit hook blogtools runs locally before every commit. If frontmatter is invalid the commit is blocked. Fastest possible feedback.
Layer 2 — Vercel build If something slips through, the build throws and deployment is cancelled. Nothing broken goes live.
Next I want to add a linter that checks for broken links, missing alt text on images, and headings that skip levels. And eventually wire the whole thing into a pre-commit hook so nothing broken ever reaches Github.
blogtools is at v0.1.0 — just getting started.