What this changes
Strapi editors can place a design-system button in any markdown field, without touching code:
``markdown ::button[Boek direct jouw spot](/boeken){variant=primary} ::button[Aanmelden bijdragen](/bijdragen){variant=secondary size=sm} ``
Both options are optional; ::button[Label](/x) renders a medium primary button. The directive is inline, so it also works mid-sentence.
variant selects the *role* of a button (primary, secondary, border-primary, border-secondary, stone-primary, stone-border-primary, white), not a colour. Colours stay in _colors.scss, so a rebrand remains one edit in git rather than a search-and-replace through the Strapi database. size accepts md, sm, xs.
Variant and size resolve against an allowlist of the .btn* classes that already exist in _base.scss. An unknown value — a typo, or a Tailwind class like bg-red-500 — falls back to the default and logs a warning during the build, so an editor cannot inject arbitrary CSS classes. Link targets are limited to http:, https:, mailto:, tel: and site-relative paths; javascript: renders as href="#".
The two white-on-transparent variants are deliberately not exposed: they are only legible on a dark background, and a markdown field cannot know what it sits on.
The rendered anchor carries not-prose, otherwise @tailwindcss/typography's .prose :where(a) rule paints the button's label with a link colour and an underline.
How to try
On the preview deployment, put the two lines above into any markdown field in Strapi (for example a content page's rich text block) and open the page. Expect a violet pill and a smaller burgundy pill, matching the buttons used elsewhere on the site — no underline, no link colour.
Locally:
`` pnpm test:markdown ``
Eight cases cover the class mapping, the defaults, the allowlist fallback, class injection, javascript: hrefs, label escaping, inline rendering, and that ordinary markdown is untouched.
What this affects
Every field rendered through Markdown.astro gains the directive. Existing content is unaffected: ::button did not previously mean anything, and ordinary markdown renders identically.
Markdown.astro's renderer moves into src/utils/renderMarkdown.ts. That is not cosmetic — the old code called marked.setOptions() in Astro frontmatter, which re-runs per component render, and registering the extension there would have stacked it onto the shared marked singleton on every render. The new module builds one dedicated Marked instance at import time. Heading slugs and data-heading-depth behaviour are byte-for-byte unchanged, so the chapter sidebar in RichText.astro keeps working.
Nostr stories and events render client-side through public/scripts/render-markdown.js and are deliberately left without this directive: that content is untrusted, and an author there should not be able to draw something that looks like an official call to action.
Adds a test:markdown CI job alongside test:redirects. astro check reports the same 47 pre-existing errors as main.