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
October 7, 2026
npx skills add timerise-ai/blog-markdown
Current release
v0.1.10
Reference docs
10
Non-negotiables
4
License
MIT

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.

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

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 {}, with no error and 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"), an ENOENT and a failed build, 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 ›

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.

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

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 (13 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
  • evals/The prompts an operator types after installing (prompts.md) and one file per agent eval: the skill installed into an empty Next.js app, one prompt, no help, then type-checked, built and tested
  • .github/workflows/agent-eval.ymlThe caller of the index's reusable eval workflow: runs prompt 1 in Claude Code, Codex CLI and Gemini CLI on every published release and commits each result to evals/

Recent releases

  1. v0.1.10October 7, 2026

    Fix release, from scoring the prompt-1 agent eval runs against 0.1.9.

  2. v0.1.9October 7, 2026

    Fix release, from scoring the prompt-1 agent eval runs against 0.1.8.

  3. v0.1.8October 7, 2026

    Fix release, from scoring the prompt-1 agent eval runs against 0.1.7.

What do I tell my agent?

Say what you need in your own words; the skill supplies the how. These are starting points, and the ones we tested say how it went.

  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.

  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.

How does it do in each agent?

We install the skill into an empty Next.js app, give the agent one of the prompts above and no further help, then type-check, build and run the tests it left behind. Nothing is fixed by hand before the checks, and a failing run is published like a passing one. The procedure and every result are public, and the first prompt runs again before each release.

  • Codex CLIcodex-cli 0.161.0

    gpt-6.1-sol

    Built, checks pass
    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.
    Typecheck: passBuild: passTests: pass
    Time
    3 min
    Changed
    33 files, +2,743 lines
    Stack
    Repository files
    Skill
    v0.1.10
    Run
    Oct 7, 2026

    Result file ›Agent log ›

    What we saw

    Rubric 8/8, scored from the diff (gpt-6.1-sol, reasoning effort none). Every block that names a destination was extracted, (continued) included; the only template edits are the English-only LOCALES in the loader and the validator, the documented punctuation extension in getTagSlug, and paths.ts for unprefixed routes, a host seam. The shipped test file is verbatim and its 20 pass; the two further tests are in test/feed.test.ts, a file of their own, which the skill allows. The summary says to set SITE_URL before deployment. Not scored: the validator's slugify was not given the punctuation extension, which references/operations.md says to change together with getTagSlug.

  • Gemini CLI0.63.0

    gemini-3.8-flash

    Built, checks pass
    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.
    Typecheck: passBuild: passTests: pass
    Time
    13 min
    Changed
    42 files, +3,419 lines
    Stack
    Repository files
    Skill
    v0.1.10
    Run
    Oct 7, 2026

    Result file ›Agent log ›

    What we saw

    Rubric 8/8, scored from the summary. LOCALES is ["en"], the loader is the skill's (matter(content, {}), production memoization, .md filter skipping directories, explicit drafts, translationKey fallback), the 20 tests run under node --experimental-strip-types --test, the validator is the .mjs template, and the operator note says to set SITE_URL before releasing.

  • Claude Code2.1.293

    claude-opus-5-5

    Built, checks pass
    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.
    Typecheck: passBuild: passTests: pass
    Time
    3 min
    Changed
    44 files, +3,461 lines
    Stack
    Repository files
    Skill
    v0.1.10
    Run
    Oct 7, 2026

    Result file ›Agent log ›

    What we saw

    Rubric 8/8, scored from the summary. The suite runs 20 of 20 under Node's runner with the test file kept out of the type-check of an English-only app, the loader skips stray files, the validator shares the punctuation-stripping slug rule it chose (the documented extension in references/tags.md), plain CSS replaces the placeholder utility classes (a documented seam), and the handover says to set SITE_URL before deploying.

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 f2afd73. Every rule above links to where the repository says it. All skills