The problem
What problem does Markdown blog solve?
A company blog has two jobs: read well and be found. The second job fails once there is more than one language. Translated posts get separate URLs with nothing tying them together. Tag pages split on a spelling variant. The feed goes stale. Search engines see duplicates instead of translations.
This skill builds the blog from markdown files, with one field that ties an article's language versions together: the translation key. That key drives hreflang, the language switcher, related posts and cross-language redirects. Slugs stay localized for search.
Tag pages, RSS, sitemap and cover art come with it. A validator in CI refuses a build with a broken post, so a bad file never publishes as an empty page.
The module
What does the skill build?
An agent with this skill builds one module for a Next.js App Router app, on your stack. For Markdown blog that module consists of:
- 1. Localized slugs
- 2. Tag pages
- 3. Related posts
- 4. RSS
- 5. Sitemap, hreflang and a CI content validator
Provenance
Where do the rules come from?
The whole design turns on one idea: a post's identity is its `translationKey`, not its slug. Slugs are localized for SEO and differ per language; the key is what makes three files one article, and it is what powers hreflang, the language switcher, related posts and cross-locale redirects. This skill was written by the engineer who has shipped this module; the earlier implementation it was audited against was a multi-locale, statically generated marketing blog. The templates hold the properties such a blog has to hold: every translation resolves to its siblings through the key, every tag page lists every post that carries the tag under any spelling, a file that gray-matter cannot parse still loads, and each content file is read once per build. The content-layer suite and the build verify each one; references/provenance.md has the record.Written by the engineer who has shipped this module. The earlier implementation it was audited against was the blog module of a Next.js 16 marketing site: multi-locale, statically generated, with its sitemap and SEO surface. A small content layer plus routes; its brand-specific SVG cover motifs were deliberately left behind.
The ledger separates what the audit changed, what was kept on purpose, and what has not run in production yet.
Fixed in the templates
- gray-matter's cache turns one bad file into a blank post, forever The highest-severity finding, live in the earlier implementation.
gray-matter@4memoizes by content string and writes its cache entry before parsing, so a file whose YAML throws leaves an empty{ data: {} }behind. The module's fallback parser runs on the first parse and produces correct frontmatter; the second and every later parse of that file returns{}— no error, no frontmatter — and the fallback never runs again. - Translated posts silently lost their related-posts section
getRelatedPostsread the current file's ownrelatedarray. Relations are authored as default-locale slugs, and translators generally did not copy the block, so the section simply did not render. Measured on the earlier implementation's content: every default-locale post had related posts; most translated posts had none. No error, no empty state: the section was conditional onpost.related?.length, so it vanished. - 71x file-read amplification at build time No memoization anywhere in the content layer.
getAlternateSlugscallsgetAllPostsonce per locale and runs once per page ingenerateMetadata. Measured: every content file read about 71 times per build, each read followed by a fullgray-matterparse. The cost is quadratic in post count. - A stray dotfile fails the entire build
getPostSlugsreturned every directory entry unfiltered;getPostBySlugappended.md. On any macOS checkout where Finder has opened the content folder, that isreadFileSync(".DS_Store.md")→ENOENT→ build failure, reported against a file the author never created. - Tag pages lose posts to spelling drift
getTagFromSlugresolved a URL slug to the first matching label alphabetically, then posts were filtered by that exact label. Two spellings of one tag ("AI Agents"/"AI agents") slug identically, so one variant's posts were absent from the only page they belonged on — andgenerateStaticParamsemitted duplicate params. Not triggered in the earlier implementation (checked: every locale's tag slugs, zero collisions on audit day), but the mechanism is live and hand-authored frontmatter drifts.
Show 7 moreShow fewer
- Every card claimed "5 min read" A literal
5in the card component, on every post, in every language. - Draft visibility depended on a routing flag
getPostBySlugdid not filter drafts. Drafts stayed out of production only becausedynamicParams = falsemeant an unpublished slug was never a valid route. Enabling dynamic params for any reason — a legitimate change nobody would connect to drafts — would make every draft publicly reachable. - Locale list declared five times
["en", "pl", "de"]appeared in the loader (twice), both blog route files, and the path helper — while aLOCALESconfig module existed and was used only by the sitemap. Adding a locale meant finding four other copies. - Untranslatable strings on the tag page The tag page carried an inline
{ en, pl, de }map with an English||fallback — so a fourth locale would serve English with no missing-key signal — and its meta description hardcoded English marketing copy into every locale's tag pages. Pluralization usedarticle${n !== 1 ? "s" : ""}, an English rule in code. - Dates formatted as American English for German readers
lang === "pl" ? "pl-PL" : "en-US". Also, the post page passedtimeZone: "UTC"and the card did not, so the same date could render one day apart on the index and the article. - getPostBySlug threw, and callers swallowed it Every caller wrapped it in
try/catch, which made a real disk error indistinguishable from a missing post and letgetRelatedPostsdiscard misconfigured relations withcatch { return null }. - Frontmatter regex unanchored and CRLF-intolerant
/---\n([\s\S]*?)\n---\n([\s\S]*)/matched a---horizontal rule in the body of a file with no frontmatter, and failed outright on CRLF line endings.
- gray-matter's cache turns one bad file into a blank post, forever The highest-severity finding, live in the earlier implementation.
Non-negotiables
What are the 4 rules the module never breaks?
Every module built from this skill holds these, whoever builds it. The same list is in the skill's README and SKILL.md, so the agent reads it before it writes a line.
gray-mattercaches a failed parse as an empty result.It writes its cache entry before parsing, so the second and every later parse of a file whose YAML throws returns
{}, with no error and no frontmatter. Always callmatter(contents, {}); any options object opts out of the cache. The re-parse test in the suite holds it.Relations are authored once, in the default locale.
A translated file that omits
relatedresolves throughtranslationKeyto the default locale's relations, so every translation renders the same related section. The fallback test holds it.A tag's identity is its slug, not its label.
"AI Agents"and"AI agents"slug to the same URL; group and match by slug, so every spelling variant's posts appear on the one tag page. Two tests hold it.Loading is O(posts x locales) per page unless you memoize.
Memoize per locale so each content file is read once per build; resolving alternates in
generateMetadatais otherwise a 71x file-read amplification, measured on the earlier implementation.
Fit
When should you use it, and when not?
Use it for
- Content authored as markdown in the repo, deployed with the app
- More than one language, with different slugs per language
- Tag pages, related posts, feeds, or hreflang are in scope
- You want the whole thing statically generated at build time
Not for
- A hosted CMS as the source of truthInsteadThat CMS's SDK; keep only
references/content-model.md - A docs site or help center with categories, sidebar and searchInsteadThe sibling `help-center-markdown` skill, a different navigation model
- A single-locale blog of under ten postsInsteadPlain
fsplusgray-matterinline; this skill is overhead there - Rendering user-submitted markdownInsteadA sanitizing renderer; see the XSS note in
references/rendering.md
Build it yourself
How do I install it?
One command. The skills.sh CLI installs the skill into every skills-compatible agent it finds.
$ npx skills add timerise-ai/blog-markdownClaude Code
Invoke with /blog-markdown
Codex CLI
Invoke with $blog-markdown
Gemini CLI
Invoke with /skills
Name the agents instead with -a, for example npx skills add timerise-ai/blog-markdown -a claude-code -a codex. Or clone the repository into your agent's skills folder. Nothing in it is agent-specific.
What is inside the repository (11 entries)
SKILL.mdEntry point: architecture, critical facts, hard rules, and the reference directoryreferences/adaptation.mdThe seam contract with the host app: content source, styling, i18n, routing, the renamereferences/content-model.mdFrontmatter schema, drafts, translation keys, relations, excerptsreferences/content-loader.mdReading files into posts: gray-matter, the fallback parser, memoization, reading timereferences/i18n-and-routing.mdLocales, alternates, hreflang, canonicals, language switcher, cross-locale redirectsreferences/tags.mdTag slug identity, collisions, tag pages, thin-tag thresholdsreferences/pages-and-seo.mdThe three routes,generateStaticParams, metadata, JSON-LD, sitemap, RSSreferences/rendering.mdMarkdown to React, code blocks, cover motifs and accents, the XSS boundaryreferences/operations.mdThe content validation script, build cost, authoring workflowreferences/testing.mdSeven fixtures and the 20-test content-layer suitereferences/provenance.mdThe engineering ledger: what the audit of the earlier implementation changed and how the templates verify it, what was kept, what was added
Recent releases
- v0.1.6September 21, 2026
Wording release. The skill content is unchanged from 0.1.5.
- v0.1.5September 2, 2026
Wording release. Templates and technical content are unchanged from 0.1.4.
- v0.1.4September 2, 2026
Wording release. Templates and technical content are unchanged from 0.1.3.
After installing
What do I tell my agent?
Say what you need in your own words; the skill supplies the how. These are starting points.
Add a blog to this Next.js app from markdown files in content/blog: post pages, tag pages, related posts, an RSS feed and sitemap entries, all statically generated.
Repository filesMake our markdown blog bilingual, English and Polish, with a different slug per language joined by one translation key, and hreflang between the two.
Repository filesOur blog build got slow as the posts grew. Find where the loader rereads the markdown files and fix it.
Build it with Timerise
How long does it take, and what does it cost?
We quote this module per project. The price depends on what it has to connect to. The path to a number is short and free:
Step 1
Brief
Tell us what the module must connect to. Takes minutes, in a chat.
Step 2
Prototype in 48 hours
A clickable prototype of your system and a quote, at no cost.
Step 3
Build and handoff
One project price. Source code, documentation and IP are yours.
Two ways to get Markdown blog
Build it yourself
Install the skill. Your own agent builds the module.
- MIT licensed, no strings
- Runs in Claude Code, Codex CLI and Gemini CLI
- The same rules our engineers build by
$ npx skills add timerise-ai/blog-markdownBuild it with Timerise
Send a brief. We build Markdown blog into a system you own.
- Clickable prototype and a quote within 48 hours, free
- One project price, no subscription, no commission
- Source code, documentation and IP handed over
Generated from the skill's own files at commit 648464c. Every rule above links to where the repository says it. All skills