Authoring content
Base frontmatter for every content document, in-body shortcodes, categories and authors, hand localization, and the optional Content API.
Drop a .md or .mdx file in packages/content/{locale}/{section}/. The path is the URL: en/blog/hello-world.md serves /blog/hello-world, and in a page or alternatives section index.md serves /{section} itself. Every section shares the frontmatter below; the alternatives page adds required fields of its own.
Base frontmatter
| Field | Type | Notes |
|---|---|---|
title | string | Required. The H1. |
date | YYYY-MM-DD | Required on every document, any template. Sort order and datePublished. |
description | string | Required in practice: the excerpt is description, else seo.description, else empty. |
dateModified | YYYY-MM-DD | Shown as "Updated" and emitted in JSON-LD. |
lastVerified | YYYY-MM-DD | When you last checked the facts. Required by the alternatives page. |
primaryKeyword | string | One page per query per locale; a second claimant is a guard error, earlier date wins. |
author | string | Must match a slug in blogAuthors. |
category | string | Article sections only. Must match a slug in contentCategories[section]. |
tags | string[] | Free-form. Tag archives are noindex, follow, out of the sitemap. |
featured / draft | boolean | The newest one fills the featured slot at the top of the section homepage and is the only document held out of the blocks below it / hidden from every listing and feed. |
image / imageAlt | string | Hero image and alt text, used as the social preview instead of the generated card. Add imageWidth / imageHeight. |
imagePosition | ImagePosition | Per-document override of config.content.defaults.imagePosition. |
features | Partial<ContentFeatures> | Display overrides, see Content sections. |
faq | { q, a }[] | Rendered as native <details> and as FAQPage JSON-LD. |
related | string[] | Sibling slugs to cross-link first. |
article | boolean | page template only: advertises og:type: article and adds an Article node to the page's JSON-LD. |
translate | boolean | false keeps the translator away from the file. |
seo | { title?, description?, noindex? } | Per-document metadata overrides. |
Shortcodes in the body
Usable in any body with no import; they read live config, so no page holds a stale value.
| Shortcode | Renders |
|---|---|
<Related slug="supastarter" /> | In-body link to a sibling in the SAME section, carrying its title. An unresolved or draft slug renders nothing. |
<CompareTable headers rows caption compact /> | A prose comparison table - see Alternatives page. |
<Callout type="warn" title="..." /> | A boxed note. |
<SiteName />, <BaseUrl />, <SupportEmail />, <BusinessName />, <BusinessAddress />, <RegistrationNumber /> | Config values. |
Categories, authors, section copy
contentCategories (packages/config/src/content.ts), then add content.categories.{section}.{slug} with name and description to packages/i18n/translations/en/web.json. Only edit en; other locales are generated.blogAuthors (packages/config/src/blog.ts) with slug and name, plus optional jobTitle, bio, avatar and social. Authors are site-wide.content.sections.{slug} with name, description, meta_title and meta_description. Every key falls back, so a new section never 500s.Only categories with a published document appear in listings and the sitemap, and a category needs three or more before it earns its own block on the section homepage - see Content sections.
Localizing by hand
The pre-commit translate hook mirrors en into every locale. Two escapes:
translate: falsein the frontmatter, bare and lowercase. The translator skips the source; a copy made before you set the flag stays as it is._-prefixed filenames._template.mdis never translated, collected or served.
Structured template frontmatter translates badly: copy the file into the target locale and translate it yourself.
The Content API
config.contentApi serves the same Markdown over HTTP for crawlers and automation. Generated projects ship { enabled: false }; the routes answer 404 until you turn it on.
- Every endpoint needs the
X-Content-Api-Keyheader, compared timing-safely againstCONTENT_API_KEY(min 16 chars;503unset,401wrong). GET/POST/PUT/DELETEon/v1/content/:collection[/:slug], plusGET /v1/content/blog/{categories,authors}- see API Layer.- Slugs are validated against
^[a-z0-9-]+$with path-traversal confinement, so nested documents are unreachable and_files never list. Writes: 10/min.
With gitCommit: true each write auto-commits via simple-git, which needs a writable repository. On read-only serverless filesystems commits fail silently and the changes are lost on the next deploy.
Content sections
Buyer-defined URL prefixes typed by config.content, each with a listing page and one of four templates, all rendered by a single shared route family.
Alternatives pages
The alternatives roundups - the index at /alternatives anchored to your own product and a conquest page per rival - their frontmatter, the derived table, the ranked blocks, and the guard that noindexes a thin page.