Everything this blog can do
One post that renders every style the blog supports — paragraphs, callouts, code, diagrams, images, video, tables, footnotes and maths. Keep it around as a reference.
On this page
This is the reference sheet. Everything the blog can render appears below — actually rendered, not just described — so when you forget how to drop in a diagram six months from now, this is the page to open.
Paragraph styles
Ordinary paragraphs need nothing special. Body text is set in Source Serif 4, self-hosted, at about 71 characters a line — inside the 60–75 band that reads comfortably. The type scale is fluid, so it grows with the viewport rather than snapping between breakpoints. Interface text — the nav, dates, tags, this site’s furniture — stays in the system sans, because you scan that rather than read it.
Inline formatting
You get the usual: bold, italic, inline code, links,
strikethrough, highlighted text, H2O, 106,
CDN with a tooltip, and
Ctrl + C for keys.
Quotes and callouts
Blockquotes sit behind a quiet rule, set roman rather than italic — a long quote in italics is slower to read. Good for quoting someone else; less good for things you want the reader to actually stop and notice.
— someone, probably
For those, use a callout.
Lists
Unordered, with nesting:
- A top-level item
- Another one
- A nested item
- And its sibling
- Three deep still reads fine
- Back to the top level
Ordered, for steps that happen in sequence:
- Reproduce the failure first
- Write the test that captures it
- Fix it
- Watch the test go green
Task lists work too, via GFM:
- Set the site up
- Write the reference post
- Actually write something worth reading
And definition-style pairs, using a plain table when you need two columns:
Term : A description list is not in GFM, so use a table or a bold run-in instead.
Code blocks
Fenced code is rendered by Expressive Code, so it gets a copy button, a file title, and line-level annotation — all at build time, with no JavaScript shipped for the highlighting itself.
type Entry<T> = { value: T; expires: number };
export function createCache<T>(ttlMs: number) { const store = new Map<string, Entry<T>>();
return { get(key: string): T | undefined { const hit = store.get(key); if (!hit) return undefined; if (hit.expires < Date.now()) { store.delete(key); return undefined; } return hit.value; }, set(key: string, value: T) { store.set(key, { value, expires: Date.now() + ttlMs }); }, };}Marking the lines that matter
Highlight ranges with {4-6}, and pick out a term by quoting it:
import torch
def configure(model, learning_rate: float): # These three lines are the whole point of the example. optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100) return optimizer, schedulerShowing a change
server { listen 443 ssl http2; server_name atyasar.dev;
gzip on; brotli on; brotli_types text/html text/css application/javascript;}ins and del work inside a normal language block too, so you keep syntax
colours while still showing the edit:
export default { input: 'src/index.js', output: { format: 'cjs', format: 'esm', },};Line numbers, folding and wrapping
Long files can start folded with collapse, and line numbers are opt-in:
import http from 'node:http';
const PORT = process.env.PORT ?? 3000;
const routes = { '/': () => ({ status: 200, body: 'ok' }), '/health': () => ({ status: 200, body: JSON.stringify({ up: true }) }), '/version': () => ({ status: 200, body: process.version }),};
function handle(url) { const route = routes[url]; return route ? route() : { status: 404, body: 'not found' };}
http.createServer((req, res) => { const { status, body } = handle(req.url); res.writeHead(status).end(body);}).listen(PORT);wrap stops a long line scrolling off the side — handy for log output:
ERROR Failed to resolve import "./components/Header.astro" from "src/pages/index.astro". Does the file exist? The import was added in commit ac76eb7 and the file was renamed in 5511c9b, so either the import or the rename needs to change.Terminal blocks get their own frame automatically, with no file title:
npm run build && npx serve distDiagrams
Write a mermaid fence and it renders as a diagram. Anything after the
language becomes the caption. The raw source stays in the HTML as a <pre>
fallback, so it still reads sensibly without JavaScript — and mermaid itself
is only downloaded on pages that actually contain a diagram.
Flowcharts
graph LR A[Browser] -->|HTTPS| B(CDN edge) B -->|cache miss| C[Origin] C --> D[(Postgres)] C --> E[(Redis)] B -->|cache hit| A
Sequence diagrams
sequenceDiagram participant C as Client participant A as Auth API participant R as Resource API C->>A: POST /token (refresh_token) A-->>C: 200 access_token (15 min) C->>R: GET /me (Bearer) R-->>C: 200 profile
State machines
stateDiagram-v2 [*] --> Queued Queued --> Running: worker picks up Running --> Succeeded: exit 0 Running --> Failed: exit non-zero Failed --> Queued: retry < 3 Failed --> Dead: retry exhausted Succeeded --> [*] Dead --> [*]
Entity relationships
erDiagram
AUTHOR ||--o{ POST : writes
POST ||--o{ TAG : "tagged with"
POST {
string slug PK
string title
date pubDate
bool draft
}Gantt charts, pie charts, git graphs and class diagrams all work the same way — anything mermaid supports.
Images
Import the file, then hand it to Figure. Astro generates a responsive
srcset, converts to modern formats, and sets the intrinsic size so nothing
shifts while the page loads.

Pass wide to break out past the text measure on large screens — useful for
screenshots and wide charts.

Several at once. Set sizes on gallery children so the browser doesn’t fetch
a full-width file for a half-width slot:


Three across works too:



Video
For screen recordings, drop the file in /public/video/ and use Video. With
loop set it autoplays muted and loops, the way a GIF would, but at a fraction
of the size — the clip below is 38 KB for four seconds.
Drop loop and you get a normal player with controls, which is what you want
for anything with narration:
Pass alt_src to offer a second encoding — a .webm alongside an .mp4, say.
The browser takes the first one it can play, and the MIME type is worked out
from the extension.
For YouTube, YouTube renders a thumbnail and loads nothing from Google until
the reader clicks play. A page with five embeds still costs no third-party
JavaScript on first paint.
Tables
| Approach | First paint | JS shipped | Notes |
|---|---|---|---|
| Static HTML | ~40 ms | 0 KB | What this site does |
| Islands | ~60 ms | ~15 KB | Fine for a search box |
| Full SPA | ~400 ms | ~180 KB | Hard to justify for a blog |
Columns align with :---, :---: and ---:. Wide tables scroll horizontally
on small screens instead of breaking the layout.
Footnotes and maths
Footnotes use standard GFM syntax and collect at the bottom of the post.1 You can have more than one, and they renumber themselves.2
Maths is available inline — , or — and as a display block:
Frontmatter
Every post starts with this block. Only the first three fields are required:
---title: 'Everything this blog can do'description: 'One line. Used in listings, RSS, search results and social cards.'pubDate: 2026-09-20# optional from here downupdatedDate: 2026-09-22tags: ['meta', 'astro']draft: false # true hides it from the buildcover: './cover.jpg' # social card, and hero if showCover is truecoverAlt: 'Describe the image'showCover: falsetoc: true # false hides the table of contentscanonicalUrl: 'https://example.com/original' # for cross-posts---Reading time and word count are computed at build time — no need to write them down.
What you don’t have to do
No imports for any of the components above; they’re injected into every post.
No manual table of contents. No <div> wrappers for layout. Write markdown,
reach for a component when markdown runs out, and that’s the whole workflow.