Posts on mlsystems.dev live in src/content/posts/. Each post is a folder containing an index.mdx plus any images, components, or data that belong with it.
Looking for the contribution process (forking, branching, PR review)? See CONTRIBUTING.md. Adding yourself as an author? See becoming-an-author.md.
mkdir src/content/posts/your-slug-here
touch src/content/posts/your-slug-here/index.mdxThe folder name becomes the URL slug: your-slug-here/ → mlsystems.dev/blog/your-slug-here.
Keep slugs short, lowercase, hyphenated, and stable — once published, changing the slug breaks inbound links.
Everything related to one article lives together:
src/content/posts/your-slug-here/
├── index.mdx ← the article
├── hero.png ← optional cover image (used as OG card)
├── benchmark.png ← inline image used in the body
├── ThroughputViz.tsx ← optional custom React component just for this post
└── data.json ← optional data used by that component
Delete the folder → everything for that post goes with it. No orphan images, no leftover components.
---
title: 'My new article'
summary: 'One-sentence pitch that shows up in the index and on social cards.'
authors: ['yourhandle'] # one or more handles from src/content/authors
date: '2026-12-01'
readMin: 12
topic: 'Inference & Serving'
topicId: 'inference'
tags: ['attention', 'kernels']
cover: './hero.png' # optional — see "Cover image" below
featured: true # surface on home page (optional, default false)
draft: false # hide from /blog and sitemap (optional, default false)
---Validation: all frontmatter is validated by Zod schemas in src/content/config.ts. Missing fields, bad types, unknown topicId values, or unknown author handles fail the build with a clear error.
Required: title, summary, authors, date, readMin, topic, topicId.
Optional: tags, cover, featured, draft.
authors is an array — list as many handles as needed. Each renders as a clickable byline link.
authors: ['lchen', 'priya', 'naoko']topicId must match one of the topics defined in src/lib/data.ts. Current values: inference, training, architecture, distributed, quantization, rag, multimodal, agents, evals, mlops. topic is the human-readable label.
By default every post gets an auto-generated Open Graph card with your title, authors, and the site's brand bar. If you want a custom cover instead — your own diagram, a paper figure, a chart — add a cover field.
Drop the image in your post folder and reference it relatively:
cover: './hero.png'Astro will:
- Validate the file exists at build time (broken paths fail the build, not production)
- Generate WebP / AVIF + responsive
srcsetautomatically - Add a content-hash to the filename for permanent CDN caching
- Use it as
og:imageandtwitter:image— your social shares show this instead of the generated card
For images already hosted on a CDN:
cover: 'https://res.cloudinary.com/yourname/image/upload/v1/hero.jpg'The URL is used as-is. No build-time optimization (the CDN should handle that), but no orphan risk either.
When cover is set, the build skips Satori OG card generation for that post — your image is the OG card. At scale (hundreds of posts), this saves real build time.
Standard Markdown works:
**bold**, _italic_, [links](https://example.com), `inline code`,
> blockquotes,
## Headings
1. Numbered lists
2. Just like that.
- Bullets
- Also fine.Fenced code blocks get syntax highlighting via Shiki. Always specify the language:
```python
def attention(q, k, v):
return softmax(q @ k.T / d**0.5) @ v
```Supported: python, cuda, cpp, rust, go, typescript, bash, yaml, json, diff, and many more.
For images inside the article body, always use <Image> from astro:assets — not plain markdown image syntax. This gives you WebP/AVIF, lazy loading, and proper width/height to prevent layout shift.
import { Image } from 'astro:assets';
import flash from './flash-attention.png';
<Image src={flash} alt="FlashAttention memory access pattern" />Wrap with a caption using the <Figure> component:
<Figure caption="FlashAttention tiles attention to reduce HBM traffic.">
<Image src={flash} alt="FlashAttention memory access pattern" />
</Figure>Every image needs alt text — the build warns if it's missing.
LaTeX inside $...$ (inline) or $$...$$ (block). KaTeX rendering can be enabled — open an issue if you need it.
One of the strongest reasons to use the folder pattern: each article can ship its own interactive components, scoped to that post.
src/content/posts/your-slug-here/
├── index.mdx
├── ThroughputViz.tsx ← lives only with this post
└── data.json
ThroughputViz.tsx:
import data from './data.json';
export default function ThroughputViz() {
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}index.mdx:
import ThroughputViz from './ThroughputViz';
<ThroughputViz client:visible />Astro hydration directives (client:load, client:visible, client:idle) all work normally. Only the components actually imported get bundled.
This keeps src/components/ clean — global components stay site-wide, one-off post visualizations live with their post and get deleted when the post does.
Available in any post — import is automatic:
<Figure caption="...">— image + caption wrapper<Note>— callout box
npm run devVisit http://localhost:4321/blog/your-slug-here to see your post rendered. The dev server hot-reloads on every save.
Before opening a PR, run a production build to catch schema errors:
npm run buildOnce your PR is merged, the build pipeline automatically:
- Generates a static page at
/blog/your-slug-here - Adds the post to
/blog(archive) and the topic pages - Adds it to
/sitemap-index.xmland/rss.xml - Indexes the post for full-text search (Pagefind)
- Generates a custom OG card (or uses your
coverif set), JSON-LD structured data, canonical URL
No manual steps. Drop the folder in, open a PR, you're published.
Specific guidance on tone, length, and quality lives in CONTRIBUTING.md. Short version: be specific, show your work, cite your claims, write for practitioners.