devlog style guide
Detailed rules for writing, diagrams, code, and quality. The SKILL.md references this file — read it before writing any article.
1. Writing style
Voice
Hacker News peer-to-peer register. Imagine explaining to a smart engineer at your company who missed the meeting where this was decided. They know the stack, they know the domain, they don’t know this specific change.
Structure: the HN arc — a story, not a summary
The best technical posts tell a story. They follow this shape:
- State a concrete problem — not an abstract goal, a specific thing that was broken or missing. Open in the world before the fix, and make the reader feel the pain.
- Show why it was harder than it looked — false starts, constraints, the obvious solution that doesn’t work. This is the tension.
- Walk through the actual approach — the resolution, told as the payoff to that tension, not a feature list. This answers “what really changed?”
- Share honest learnings — what broke, what’s uncertain, what you’d do differently.
This arc works whether you’re explaining a 20-line bugfix or a system redesign. Scale the depth, not the structure.
The summary smell. If your sections could be shuffled into any order without loss, you wrote a changelog, not a story. Each beat should set up the next: the problem makes the reader want the resolution; the complication makes the resolution satisfying. Test it — try reordering your sections. If nothing breaks, the narrative chain isn’t there yet.
Paragraph discipline
- Lead with the claim, then support. Don’t build up to the point — state it, then explain.
- One idea per paragraph. If you’re making two points, that’s two paragraphs.
- Cut throat-clearing. Delete opening phrases like “In order to…”, “It’s worth noting that…”, “As mentioned above…“. Start with the real sentence.
- Concrete over abstract. “The cache evicts entries after 15 minutes” beats “The cache uses a time-based eviction strategy.”
- Active voice default. “The handler validates the token” not “The token is validated by the handler.” Passive is fine when the actor is genuinely irrelevant.
Jargon calibration
Use technical terms when they carry meaning — don’t explain what a mutex is. But expand project-specific acronyms on first use. The reader knows general engineering; they may not know your project’s internal vocabulary.
Humor
A wry aside or honest self-deprecation is welcome. “We added a cache, which naturally led to two new bugs” is fine. Forced jokes, puns in headings, and emoji are not. The humor should come from the technical reality. If you have to try, skip it.
Length
Shorter is better. A good devlog respects the reader’s time.
| Change size | Target word count | Diagrams | Code snippets |
|---|---|---|---|
| Small (bug fix, config change) | 400–700 | 0–1 | 1–2 |
| Medium (new feature, refactor) | 800–1200 | 1–2 | 2–4 |
| Large (system redesign, new service) | 1200–1800 | 2–3 | 3–5 |
| Architectural plan (no code yet) | 600–1000 | 1–2 | 0–2 |
These are guides, not rules. A 200-word devlog that nails the key insight is better than a 1500-word one that covers everything.
2. Headlines and section titles
Every heading is an assertion, not a label.
| Bad | Good |
|---|---|
| Authentication changes | JWT tokens now rotate on every refresh |
| Database migration | User table splits into profiles and credentials |
| Performance improvement | Cold starts dropped from 4.2s to 800ms by lazy-loading the ORM |
| Architecture | The queue now sits between the API and the workers |
Section headings (h2) follow the same rule. h3 sub-headings can be shorter labels when they’re structuring a list of related points within a section.
3. Code snippets
Inclusion criteria
A code snippet earns its place only if:
- It shows a non-obvious decision — an algorithm choice, an API design, an error-handling strategy
- It would take more prose to explain without the code than with it
- The reader needs to see the shape of the code to understand the trade-off
Boilerplate, imports, type declarations, and obvious getters stay out. If the function is 40 lines but only 8 matter, show the 8 and elide the rest with // ....
Formatting
- 10 meaningful lines max per snippet
- File path + function name as a label above the block:
src/cache/lru.ts → evict() - 2-3 sentences of explanation immediately after each snippet, answering: what does this do, and why was it done this way?
- HTML entities for
<(<),>(>),&(&) inside<code> - No syntax highlighting via JS — use CSS classes if you want minimal color, or keep it plain. The file must work without any scripts.
Prose-code relationship
Write the prose so the article reads coherently even if all code blocks were removed. The prose explains; the code illustrates. Never write “as shown above” as the sole explanation — state the point in words, then point to the code for specifics.
4. Diagrams
Show over tell — and reinforce freely
Write for visual thinkers: when a change has a shape — hierarchy, direction, sequence, spatial relationships, before/after — draw it. Default to showing, and give every major idea a visual anchor the eye can land on.
A diagram does not have to replace the prose. A visual that simply re-draws what a paragraph just said is welcome — the reader takes in the picture and the words as one pass, and the redundancy reinforces the point. Pair prose with a diagram freely; you don’t have to choose between them.
The bar is only this: the diagram must be true and legible — an accurate picture of the real change, readable at a glance. Cut diagrams that misrepresent the actual topology or are pure decoration with no content. Reinforcing the prose is good; misleading or empty is not.
SVG rules
All diagrams are inline SVG. No Mermaid rendering, no JS, no external images, no <img> tags.
Canvas and palette:
Background: #fafaf8 (matches page --bg)
Node fill: #f5f5f0 (light warm gray)
Node stroke: #1a1a1a (near-black, 1px)
Focal fill: #fff7ed (warm accent tint)
Focal stroke: #e8590c (accent orange, 1.5px)
Arrow stroke: #888 (tertiary gray, 1.2px)
Focal arrow: #e8590c (accent, 1.4px)
Label text: #1a1a1a (primary) or #555 (secondary)
Mono labels: 12px monospace, letter-spacing 0.05em
Name labels: 14px system sans, weight 500
Layout:
viewBoxbased, no fixed width/height on the<svg>element — let it scale- Typical viewBox:
0 0 700 340(landscape),0 0 500 500(square for state machines) - All coordinates divisible by 4
- Node sizes from a fixed set: width 120/140/160, height 40/56
- Arrow endpoints land exactly on node edges, not “near” them
- Arrowheads: open chevron paths (
M2 1 L8 5 L2 9), not filled triangles - Leave generous whitespace — cramped diagrams are worse than no diagram
Complexity budget:
- Max 9 nodes per diagram. If you need more, split into two diagrams.
- 1-2 focal elements per diagram (accent color). Everything else neutral.
- If two nodes always appear together, merge them into one.
- If an arrow’s meaning is obvious from layout alone, remove the label.
Diagram types
Architecture diagram — boxes and arrows showing components and connections:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client │────▶│ API │────▶│ Cache │
└─────────┘ └────┬────┘ └─────────┘
│
▼
┌─────────┐
│ DB │
└─────────┘
Use when showing how components relate. Highlight the new/changed component as focal.
Sequence diagram — vertical lifelines with horizontal arrows:
Client API Auth DB
│ │ │ │
│──request──▶ │ │ │
│ │──validate─▶ │ │
│ │ ◀──token── │ │
│ │──────────────────save──▶ │
│ ◀──response─│ │ │
Use for request flows, event ordering, async interactions. Keep to 4-5 participants max.
Flow diagram — left-to-right or top-to-bottom pipeline: Use for data transformation pipelines, build processes, deployment flows.
State machine — circles/boxes with labeled transition arrows: Use for lifecycle states (draft→published→archived), connection states, process stages.
Before/after — two small diagrams side by side: Use when the change restructures something and the reader needs to see both states to understand the difference.
Diagram anti-patterns
| Pattern | Why it fails |
|---|---|
| Every node in accent color | Nothing is focal — the accent means nothing |
| Diagram misrepresents the real topology | Worse than no diagram — it teaches the wrong mental model |
| Arrow labels without background rects | Line bleeds through text, becomes unreadable |
| Gradient fills or drop shadows | Looks “designed” instead of informational |
| More than 9 nodes | Too dense to scan — split into multiple diagrams |
| Decorative icons in nodes | Noise. Use text labels. |
| Exact same info as a nearby code snippet | Pick one: diagram or code, not both for the same point |
5. The scrutiny section
This is the most important section for review purposes. It answers: “What should I actually look at before approving this?”
What to flag
| Category | Example |
|---|---|
| Architectural fit | ”This adds a second caching layer — do we want two?” |
| New abstractions | ”The EventBus class is new — currently only used in one place” |
| Convention breaks | ”Other handlers use middleware for auth; this one checks inline” |
| Wheel-reinvention | ”This custom retry logic is similar to what @utils/retry already does” |
| Agent uncertainty | ”The token expiry value (15min) was guessed — worth verifying” |
| Missing tests | ”The error path in rotateToken() has no test coverage” |
| Naming drift | ”The function is called processData but it only validates emails” |
How to flag
Be specific, not vague. Every scrutiny point should name:
- What — the specific function, file, pattern, or decision
- Why it matters — the concrete risk if this is wrong
- What to check — what the reviewer should verify
Bad: “There may be performance implications.”
Good: “The findRelated() query joins four tables without an index on created_at. With the current 50K rows this is fine, but at 500K it’ll table-scan. Worth adding the index now or tracking as a follow-up.”
6. Anti-slop patterns
Common AI writing failures adapted for devlog context. Check against these before finalizing.
| # | Pattern | Symptom | Fix |
|---|---|---|---|
| 1 | Adjective pile-up | ”Implemented a robust, scalable, and efficient caching solution” | Name what it actually does: “Added an LRU cache in front of the token store” |
| 2 | Throat-clearing opener | ”In order to improve the authentication flow…” | Delete. Start with: “The auth flow now rotates JWT tokens on every refresh.” |
| 3 | Label headings | Section titled “Changes” or “Implementation” | Assertion: “Token rotation replaces static session keys” |
| 4 | Restating the heading | Heading says “Cache invalidation”, first sentence says “We implemented cache invalidation” | The body adds information the heading doesn’t carry |
| 5 | Vague scrutiny | ”There may be edge cases to consider” | Name the edge case: “If two requests hit rotateToken() concurrently, the second one gets a revoked-token error” |
| 6 | Invented precision | ”Reduces latency by approximately 47%“ | Only state metrics from actual measurement. If unmeasured: “Should reduce latency — not yet benchmarked” |
| 7 | Comprehensive but unclear | Covering every file touched instead of the key decisions | Cut to the 3-5 decisions that matter. Link to the full diff for the rest. |
| 8 | AI corporate speak | ”Leveraging the new architecture to unlock performance gains" | "The new architecture is faster because queries hit the cache first” |
| 9 | Empty diagram | A diagram with no real content — generic “Client → Server → Database” that isn’t the actual topology | Draw the real shape of this change. Re-visualizing the prose is fine; drawing a placeholder that says nothing is not. |
| 10 | Explaining the obvious | Paragraph explaining what REST is, or how HTTP status codes work | The reader is an engineer on the project. Skip it. |
7. HTML/CSS reference
Full CSS template
Use this as the base stylesheet. Adjust only content-specific elements (like the number of SVG nodes), never the typographic scale or color tokens.
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #fafaf8;
--text: #1a1a1a;
--text-2: #555;
--text-3: #888;
--accent: #e8590c;
--accent-bg: #fff7ed;
--border: #e5e5e0;
--code-bg: #f5f5f0;
--mono: 'SF Mono', 'JetBrains Mono', Consolas, 'Liberation Mono', monospace;
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--serif: Charter, Georgia, 'Times New Roman', serif;
}
body {
font-family: var(--serif);
font-size: 17px;
line-height: 1.7;
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
}
article {
max-width: 720px;
margin: 0 auto;
padding: 3rem 1.5rem 5rem;
}
/* --- Header --- */
.eyebrow {
font-family: var(--mono);
font-size: 0.72rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 0.5rem;
}
h1 {
font-family: var(--sans);
font-size: 1.85rem;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.02em;
color: var(--text);
margin-bottom: 0.75rem;
}
.lede {
font-size: 1.1rem;
color: var(--text-2);
line-height: 1.6;
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--border);
}
/* --- Sections --- */
h2 {
font-family: var(--sans);
font-size: 1.3rem;
font-weight: 600;
color: var(--text);
margin-top: 2.5rem;
margin-bottom: 0.75rem;
}
h3 {
font-family: var(--sans);
font-size: 1.05rem;
font-weight: 600;
color: var(--text);
margin-top: 1.8rem;
margin-bottom: 0.5rem;
}
p { margin-bottom: 1rem; }
/* --- Code blocks --- */
.code-block {
margin: 1.5rem 0;
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
}
.code-label {
display: block;
font-family: var(--mono);
font-size: 0.72rem;
color: var(--text-3);
background: var(--border);
padding: 0.35rem 0.75rem;
letter-spacing: 0.02em;
}
pre {
background: var(--code-bg);
padding: 1rem;
overflow-x: auto;
margin: 0;
}
code {
font-family: var(--mono);
font-size: 0.8rem;
line-height: 1.55;
tab-size: 2;
}
/* Inline code */
p code, li code {
font-size: 0.85em;
background: var(--code-bg);
padding: 0.15em 0.35em;
border-radius: 3px;
border: 1px solid var(--border);
}
/* --- Diagrams --- */
figure {
margin: 2rem 0;
}
figure svg {
width: 100%;
display: block;
}
figcaption {
font-family: var(--serif);
font-style: italic;
font-size: 0.88rem;
color: var(--text-2);
margin-top: 0.6rem;
line-height: 1.5;
}
/* --- Scrutiny section --- */
.scrutiny {
border-left: 3px solid var(--accent);
padding-left: 1.25rem;
margin: 2rem 0;
}
.scrutiny h2 {
color: var(--accent);
}
/* --- Lists --- */
ul, ol {
padding-left: 1.5rem;
margin-bottom: 1rem;
}
li {
margin-bottom: 0.4rem;
}
/* --- Misc --- */
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
hr {
border: none;
border-top: 1px solid var(--border);
margin: 2.5rem 0;
}
/* --- Responsive --- */
@media (max-width: 640px) {
article { padding: 2rem 1rem 3rem; }
h1 { font-size: 1.5rem; }
.code-block { margin-left: -1rem; margin-right: -1rem; border-radius: 0; border-left: none; border-right: none; }
}Code snippet HTML pattern
<div class="code-block">
<span class="code-label">path/to/file.ts → functionName()</span>
<pre><code>// key code here, max ~10 lines
// use < > & for HTML entities</code></pre>
</div>
<p>Explanation: what this code does and why it was done this way. 2-3 sentences.</p>Diagram HTML pattern
<figure>
<svg viewBox="0 0 700 340" xmlns="http://www.w3.org/2000/svg">
<!-- background -->
<rect width="100%" height="100%" fill="#fafaf8"/>
<!-- nodes, arrows, labels -->
</svg>
<figcaption>What this diagram shows, with the focal element named.</figcaption>
</figure>Scrutiny section HTML pattern
<section class="scrutiny">
<h2>What to scrutinize</h2>
<p>Point 1: specific thing to check, why it matters, and what to verify.</p>
<p>Point 2: ...</p>
</section>