Communitygithub.com

timerise-ai/blog-markdown

Agent Skill: build a file-based multilingual markdown blog — locale-partitioned posts, per-locale slugs joined by a shared translationKey, tag pages with slug identity, related posts, cover art, RSS, sitemap, hreflang, CI content validator — in Next.js App Router, no CMS

blog-markdown 是什麼?

blog-markdown is a Claude Code agent skill that agent Skill: build a file-based multilingual markdown blog — locale-partitioned posts, per-locale slugs joined by a shared translationKey, tag pages with slug identity, related posts, cover art, RSS, sitemap, hreflang, CI content validator — in Next.js App Router, no CMS.

相容平台~Claude Code~Codex CLI~Cursor
npx skills add timerise-ai/blog-markdown

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Markdown Blog (multilingual, tagged)

A blog where the content is files, not rows. 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. Get that one field right and everything else is lookups.

Written by the engineer who has shipped this module. The earlier implementation it was audited against was a multi-locale, statically generated marketing blog with its sitemap and SEO surface. The facts and rules below are what the content-layer suite and the build verify; references/provenance.md has the record.

When to use

  • 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

When NOT to use

Instead of thisUse
A hosted CMS is the source of truththat CMS's SDK; keep only references/content-model.md
A docs site / help center (categories, sidebar, search)a help-center skill — different navigation model
A single-locale blog of <10 postsplain fs + gray-matter inline; this is overhead
Rendering user-submitted markdowna sanitizing renderer; see the XSS note in references/rendering.md

Architecture

content/blog/<locale>/<localized-slug>.md   # frontmatter + body
        |
        v
 parseFrontmatter  --YAML, falls back to a line parser on malformed quotes
        |
        v
   getAllPosts(locale)  <-- memoized per locale in production
        |
        +--> translationKey ----> alternates / hreflang / language switch
        +--> tags -> tag SLUG ---> tag pages (slug is the identity, not the label)
        +--> related (EN slugs) -> related posts, with an EN fallback
        |
        v
  /blog   /blog/[slug]   /blog/tag/[slug]   sitemap.xml   feed.xml

Critical facts

  1. Relations are authored once, in the default locale, as default-locale slugs. A translated file may omit related entirely and still get the right related posts, resolved through translationKey, so every translation renders the same related section. The fallback test holds it.
  2. A tag's identity is its slug, not its label. "AI Agents" and "AI agents" slug to the same URL. Group by slug and match posts by slug, so every spelling variant's posts appear on the one tag page. Two tests hold it.
  3. gray-matter throws on a double-quoted YAML scalar containing raw quote characters — typographic quotes in a translated excerpt do it. Without a fallback parser the build stops on that one file; the line-by-line fallback keeps it loading. Verified against the library and covered by a test.
  4. 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 that threw returns {} — no error, no frontmatter. Always call matter(contents, {}); any options object opts out of the cache.
  5. 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.
  6. getPostSlugs must filter .md. The filter keeps a stray .DS_Store from becoming a .DS_Store.md read and an ENOENT. A test holds it.
  7. Tag pages have no cross-locale identity. Tags are per-locale free text, so the language switcher must fall back to the blog index on a tag URL rather than build a URL that 404s.

Hard rules

Never key cross-locale lookups on the slug. Slugs are localized on purpose. translationKey is the join column; a slug match across locales is a coincidence, not a relation.

Never let getPostBySlug decide draft visibility implicitly. Filter drafts in the loader; dynamicParams = false hides them only as a side effect and stops doing so the moment the flag changes. The draft test holds the filter.

Never render a "5 min read" constant. Compute it from the body; it costs one split, and the reading-time test holds it.

Never derive a tag page's URL from the label at read time and the label from the URL at render time without a collision check. That round trip is only lossless while every label slugs uniquely.

Quick start

  1. Set the locale contract and the frontmatter schema — content-model.md
  2. Build the loader (memoized, .md-filtered, fallback parser) — content-loader.md
  3. Wire locales, alternates and cross-locale redirects — i18n-and-routing.md
  4. Add tag grouping and tag pages — tags.md
  5. Build the three routes, metadata and JSON-LD — pages-and-seo.md
  6. Render bodies and covers — rendering.md
  7. Add the validation script and read the operator gaps — operations.md
  8. Run the fixtures and tests — testing.md

Porting this into an existing app? Fill in the seam table in adaptation.md first — it takes ten minutes and saves a rename.

Reference directory

ScenarioTrigger keywordsReference
Frontmatter fields, drafts, translation keys, relationsfrontmatter, schema, translationKey, draft, related, excerptcontent-model.md
Reading files, caching, YAML failures, reading timegray-matter, fs, cache, memoize, ENOENT, YAMLException, parsecontent-loader.md
Locales, hreflang, alternates, wrong-locale linksi18n, locale, hreflang, alternates, canonical, language switcher, redirecti18n-and-routing.md
Tag URLs, collisions, thin tag pagestag, slug, diacritics, collision, tag page, taxonomytags.md
The routes, metadata, JSON-LD, sitemap, RSSpage.tsx, generateStaticParams, generateMetadata, dynamicParams, sitemap, RSS, feed, BlogPostingpages-and-seo.md
Markdown to React, code blocks, cover imagesreact-markdown, remark-gfm, syntax highlighting, cover, motif, accent, XSSrendering.md
Validating content, build cost, editor workflowvalidation, CI check, build time, drafts, authoring, operatoroperations.md
Fixtures and the passing test suitetest, fixture, vitest, node:test, regression, asserttesting.md
Fitting it into a host app: seams, renames, probeadapt, port, host, seam, rename, CMS, integrateadaptation.md
The audit ledger: what changed, was kept and was added vs. the earlier implementationprovenance, deviations, fidelity, audit, ledgerprovenance.md

相關技能