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.
Prefer not to write MDX by hand? Visit /write — a visual editor that produces a ready-to-publish post folder you can download and drop straight into
src/content/posts/. No Markdown or setup required. The rest of this guide describes the folder that portal generates, for authors who want to write it directly.
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.
KaTeX rendering is enabled. Write LaTeX inside $...$ for inline math or $$...$$ on its own lines for display equations:
The attention scale factor is $1/\sqrt{d_k}$.
$$
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V
$$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.
These are available in every post automatically — no import needed.
Wrap any image, diagram, or inline SVG with a caption:
<Figure caption="FlashAttention tiles attention to reduce HBM traffic.">
<Image src={flash} alt="FlashAttention memory access pattern" />
</Figure>Sizing is optional. By default a figure sits at a comfortable size, centered — small diagrams stay small and are not stretched to fill the page. If a visual deserves more room, set width and it grows, breaking out wider than the text column (capped so it never gets too wide to read on large screens):
<Figure caption="A wide architecture diagram." width={900}>
<Image src={arch} alt="..." />
</Figure>width takes a number (pixels) or any CSS length ("90%", "48rem"). Leave it off to use the default.
Place multiple images side by side. They flow into as many columns as fit and wrap to the next row automatically on narrower screens — you don't lay anything out by hand:
<Gallery>
<Image src={a} alt="..." />
<Image src={b} alt="..." />
<Image src={c} alt="..." />
</Gallery>min sets how small a cell may get before wrapping (default 240px). Lower it to fit more per row:
<Gallery min={160}>...</Gallery>Embed a YouTube video. It loads only when the reader clicks play (keeps the page fast and private — no YouTube scripts until then):
<Video id="dQw4w9WgXcQ" caption="Kent Beck on the origins of TDD." />id is the YouTube video ID — the part after v= in the URL (youtube.com/watch?v=dQw4w9WgXcQ → dQw4w9WgXcQ). caption is optional.
To break a long piece into sections with a visual divider, put --- on its own line (blank line above and below). It renders as a centered · · ·:
Some paragraph.
---
The next part.<Note>A short aside the reader should not miss.</Note>A plain Markdown table just works and picks up the site's default style:
| Input | How it becomes numbers | Length |
| ----------- | ---------------------- | ------ |
| Temperature | already a number | 1 |
| A word | learned embedding | ~768 |Formatting inside cells is normal Markdown — bold, italic, code, and links all work with no extra effort:
| **Temperature** | it's _already_ a number | `[28.4]` |Color is not part of Markdown, but because this is MDX you can drop in a span when you truly need one. Use it sparingly, and never as the only way you convey meaning (color-blind readers):
| <span style="color: var(--accent)">Warning</span> | ... |Table designs. Wrap a table in <Table> to change its look. There are two independent controls: the border style (variant) and zebra shading (zebra), which can be combined.
variant — the border style (pick one; omit for the default):
| variant | look |
|---|---|
rule (default) |
header underline + hairline row separators |
lined |
full grid, every cell bordered |
plain |
header underline only, no row lines |
zebra — a boolean that shades alternating rows. It stacks on top of any border style, so <Table variant="lined" zebra> gives you a full grid and shaded rows.
<Table variant="lined" zebra>
| Model | Params | VRAM (FP16) |
| ----- | -------------- | ----------- |
| 7B | 7,000,000,000 | 14 GB |
| 70B | 70,000,000,000 | 140 GB |
</Table>Keep the blank lines around the Markdown table inside <Table> — MDX needs them to parse it as a table.
Every heading automatically gets a small copy-link icon that appears when the reader hovers the heading; clicking it copies a direct link to that section. Just write headings normally — there is nothing to add.
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.