blog

A Complete Guide to Writing on the Web

This post exists to test and demonstrate every kind of content element this site can render. If it looks good here, it will look good everywhere.


Headings

The heading hierarchy gives structure to a document. Use them in order, without skipping levels.

Third-level heading

A third-level heading is useful for sub-sections within an h2 section.


Paragraphs and inline formatting

Regular prose uses bold for emphasis, italics for nuance, and inline code for technical terms. You can also combine them: const x = 1 works fine.

Links look like this one to GitHub — understated, with a soft underline. They should never shout.


Unordered lists

A good unordered list is flat and scannable:

  • Local-first data model — your files, not a server
  • Instant full-text search across all notes
  • Plain markdown, readable in any editor
  • No account required, no subscription, no tracking
  • Works offline, always

Nested lists are possible but should be used sparingly:

  • Engineering
    • Frontend: React, TypeScript, CSS
    • Backend: Node.js, Go, SQLite
  • Design
    • Figma for UI work
    • Framer for prototyping

Ordered lists

Use numbered lists when sequence or priority matters:

  1. Write a first draft without editing
  2. Step away for at least an hour
  3. Re-read it as a stranger would
  4. Cut everything that doesn't earn its place
  5. Read it aloud — if it sounds wrong, it is

Code blocks

Code blocks use syntax highlighting. The language is specified after the opening fence.

JavaScript

// A simple debounce implementation
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
 
const onResize = debounce(() => {
  console.log('Window resized');
}, 200);
 
window.addEventListener('resize', onResize);

TypeScript

interface Post {
  slug: string;
  title: string;
  date: string;
  tags: string[];
}
 
async function getPost(slug: string): Promise<Post | null> {
  const res = await fetch(`/api/posts/${slug}`);
  if (!res.ok) return null;
  return res.json();
}

CSS

/* Clean card component */
.card {
  background: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 20px 24px;
  transition: box-shadow 0.2s ease;
}
 
.card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}

Shell

# Install dependencies
npm install
 
# Start the dev server
npm run dev
 
# Build for production
npm run build && npm run start

With a filename title

src/lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
 
const dir = path.join(process.cwd(), 'content/blog');
 
export function getAllPosts() {
  return fs.readdirSync(dir)
    .filter(f => f.endsWith('.md'))
    .map(f => {
      const raw = fs.readFileSync(path.join(dir, f), 'utf8');
      const { data, content } = matter(raw);
      return { slug: f.replace('.md', ''), ...data, content };
    });
}

Tables

Tables work best for structured comparisons. Keep columns to a minimum.

Framework comparison

FrameworkLanguageRenderingBest for
Next.jsTypeScriptSSR / SSG / ISRFull-stack apps
AstroTypeScriptStatic + islandsContent sites
SvelteKitTypeScriptSSR / SSGApps, sites
RemixTypeScriptSSRData-heavy apps
NuxtTypeScriptSSR / SSGVue ecosystem

CSS unit quick reference

UnitRelative toUse case
pxScreen pixelBorders, shadows
remRoot font sizeFont sizes, spacing
emParent font sizeComponent-local scaling
%Parent elementWidths, fluid layout
vw / vhViewportFull-screen sections
chCharacter widthProse line-length

Blockquotes

A blockquote is for ideas that deserve to stand apart from the prose.

The best interfaces are the ones that disappear. They do not impose themselves — they serve, and then get out of the way.

They can also attribute a source when needed:

Simplicity is not the absence of clutter; simplicity is somehow being essential — understanding the true essence of every element.

— Jonathan Ive


Images

Images come in a few flavours. Here is what each looks like.

Full-width

A standard block image stretches to the content column width, with a border and rounded corners.

A minimal desk setup with a laptop, notebook, and coffee

Full-width images work well for establishing shots and landscapes.


Smaller / half-width

You can constrain an image by wrapping it in a <div> with a max-width. This is useful for screenshots or UI mockups that look better at a smaller size.

A close-up of a keyboard

Half-width image — good for portraits, cropped screenshots, or anything where full-width feels too large.


Two images side by side

Code on a dark screen

A terminal window

Side-by-side layout for before/after comparisons or complementary visuals.


Inline image

You can embed a small image inline within a paragraph — useful for icons or badges. Here is an example: this sentence has an inline image TypeScript badge right in the middle of the text, and the line keeps flowing normally afterward.


Horizontal rule

A horizontal rule divides major document sections:


Inline code in context

When referencing code inline, use backticks: Array.prototype.map, useEffect, border-radius: 8px. The font switches to monospace automatically and the contrast keeps it readable.


Mixed content

A real post combines all of these. Here is what a technical explanation might look like in practice.

The debounce function above works by resetting a timer every time it is called. Only when the timer completes — after delay milliseconds of silence — does it call the original function.

The pattern in three steps:

  1. Call the debounced function
  2. If called again before delay ms, reset the clock
  3. When the clock runs out, fire the original function exactly once

This is useful for:

  • Search inputs that fire on every keystroke
  • Window resize handlers
  • Auto-save logic in editors

The key insight: you are not slowing things down, you are batching rapid requests into a single meaningful one.