Content

Markdown blog

A multilingual blog served from markdown files: localized slugs tied by one translation key, tag pages, related posts, RSS, sitemap and hreflang. No CMS.

Released
September 21, 2026
npx skills add timerise-ai/blog-markdown
v0.1.6
Current release
10
Reference docs
4
Non-negotiables
MIT
License

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.

  1. 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@4 memoizes 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 getRelatedPosts read the current file's own related array. 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 on post.related?.length, so it vanished.
    • 71x file-read amplification at build time No memoization anywhere in the content layer. getAlternateSlugs calls getAllPosts once per locale and runs once per page in generateMetadata. Measured: every content file read about 71 times per build, each read followed by a full gray-matter parse. The cost is quadratic in post count.
    • A stray dotfile fails the entire build getPostSlugs returned every directory entry unfiltered; getPostBySlug appended .md. On any macOS checkout where Finder has opened the content folder, that is readFileSync(".DS_Store.md") → ENOENT → build failure, reported against a file the author never created.
    • Tag pages lose posts to spelling drift getTagFromSlug resolved 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 — and generateStaticParams emitted 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 more
    • Every card claimed "5 min read" A literal 5 in the card component, on every post, in every language.
    • Draft visibility depended on a routing flag getPostBySlug did not filter drafts. Drafts stayed out of production only because dynamicParams = false meant 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 a LOCALES config 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 used article${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 passed timeZone: "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 let getRelatedPosts discard misconfigured relations with catch { 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.
Read the full record in provenance.md

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.

  1. gray-matter caches 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 call matter(contents, {}); any options object opts out of the cache. The re-parse test in the suite holds it.

  2. Relations are authored once, in the default locale.

    A translated file that omits related resolves through translationKey to the default locale's relations, so every translation renders the same related section. The fallback test holds it.

  3. 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.

  4. 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 generateMetadata is 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 fs plus gray-matter inline; 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-markdown

Claude 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 directory
  • references/adaptation.mdThe seam contract with the host app: content source, styling, i18n, routing, the rename
  • references/content-model.mdFrontmatter schema, drafts, translation keys, relations, excerpts
  • references/content-loader.mdReading files into posts: gray-matter, the fallback parser, memoization, reading time
  • references/i18n-and-routing.mdLocales, alternates, hreflang, canonicals, language switcher, cross-locale redirects
  • references/tags.mdTag slug identity, collisions, tag pages, thin-tag thresholds
  • references/pages-and-seo.mdThe three routes, generateStaticParams, metadata, JSON-LD, sitemap, RSS
  • references/rendering.mdMarkdown to React, code blocks, cover motifs and accents, the XSS boundary
  • references/operations.mdThe content validation script, build cost, authoring workflow
  • references/testing.mdSeven fixtures and the 20-test content-layer suite
  • references/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

  1. v0.1.6September 21, 2026

    Wording release. The skill content is unchanged from 0.1.5.

  2. v0.1.5September 2, 2026

    Wording release. Templates and technical content are unchanged from 0.1.4.

  3. 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.

  1. 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 files
  2. Make our markdown blog bilingual, English and Polish, with a different slug per language joined by one translation key, and hreflang between the two.

    Repository files
  3. Our 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:

  1. Step 1

    Brief

    Tell us what the module must connect to. Takes minutes, in a chat.

  2. Step 2

    Prototype in 48 hours

    A clickable prototype of your system and a quote, at no cost.

  3. 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-markdown

Build 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
3 years of support included.

Generated from the skill's own files at commit 648464c. Every rule above links to where the repository says it. All skills