| description | Turn the code Claude just finished into a TIL article for the user's blog. Writes an MDX post with inline React demo components into $TIL_DIR/src/content/articles/YYYY-MM-DD-<slug>/, commits + pushes, waits for GitHub Pages to deploy, and opens the live article in the browser. Use when the user says "/til" or asks Claude to write a TIL entry about the recent work. |
|---|---|
| user_invocable | true |
Blog is an Astro + MDX + React site deployed to GitHub Pages. Articles are folders in src/content/articles/ following the pattern YYYY-MM-DD-<kebab-topic>/ with an index.mdx and sibling .tsx demo components.
The skill reads two required env vars:
TIL_DIR— absolute path to the local til repo (e.g.$HOME/Code/til)TIL_URL— live site base URL, no trailing slash (e.g.https://til.example.com)
Optional:
TIL_DEPLOY_WORKFLOW— GitHub Actions workflow name. Default:Deploy to GitHub Pages.TIL_BRANCH— deploy branch. Default:main.
If TIL_DIR or TIL_URL is unset, stop and tell the user to export them (e.g. in ~/.zshrc). Do not guess.
Sanity-check at the start of the run:
: "${TIL_DIR:?set TIL_DIR to your til repo path}"
: "${TIL_URL:?set TIL_URL to the live site base URL}"
test -d "$TIL_DIR/src/content/articles" || { echo "TIL_DIR ($TIL_DIR) does not look like the til repo"; exit 1; }Keep thinking light. Don't pre-plan prose in detail — write it directly into index.mdx. Parallelize all file writes. Don't re-read reference articles unless you need a concrete styling example. Target: commit + push in under ~2 minutes of wall time.
Find the scope in this order:
- Uncommitted changes —
git status --porcelain+git diff HEAD. - Branch commits not on
main/master—git log --oneline main..HEAD+git diff main...HEAD. - Last 3 commits —
git log -3 -p.
Run these in the user's current working directory (the project the user just worked in), not in $TIL_DIR. The TIL is about that work; the article lives in the til repo.
If no repo / no changes, ask which scope.
Also scan the current conversation for non-code context: decisions, trade-offs, why. That's the teaching gold the diff can't capture.
For each: one-line what, one-line why it matters here, one specific demo idea. Don't write these out in chat — go straight to index.mdx.
Aim for a 5–10 minute read: roughly 1,250–2,500 words of prose (code blocks and demos don't count toward the read time). If the topic is thin, fewer concepts with deeper explanation beats padding. If it's rich, cut the weakest concept rather than ship a 15-minute article.
Slug format: YYYY-MM-DD-<kebab-topic>. Date is today. Topic is 2–5 kebab words capturing the insight (e.g. why-usememo-fixed-rerenders, not react-stuff).
Check existing dirs to avoid collision:
ls "$TIL_DIR/src/content/articles/"If $TIL_DIR/src/content/articles/<slug>/ already exists, ask before overwriting.
Create the directory:
mkdir -p "$TIL_DIR/src/content/articles/<slug>"In a single assistant turn, emit parallel Write tool calls for:
$TIL_DIR/src/content/articles/<slug>/index.mdx— frontmatter + prose + demo imports.- One
.tsxfile per interactive demo, sibling toindex.mdx.
Frontmatter shape (matches the content collection schema in src/content.config.ts):
---
title: "Human sentence, not a slug"
date: YYYY-MM-DD
description: "One or two sentences. What the reader will learn."
tags: ["react", "typescript"]
---Frontmatter strings are plain text. title and description render as-is on the index page — no markdown parsing. No backticks, asterisks, links, or escaped quotes. Write around code terms ("as-casts" not `as` casts) instead of using inline code.
Imports. MDX imports sibling components with relative paths:
import MyDemo from './MyDemo.tsx';Render them with the client:load directive so React hydrates:
<MyDemo client:load />Voice/prose rules. Impersonal. No personal pronouns — no I, we, you, my, our, your. No references to people or agents — no Claude, the human, the user, the author, the AI, the assistant. Focus on the code, the concepts, and what the change does. Prefer the imperative or passive voice when describing actions ("Wrap the prop in useMemo" / "The component was re-rendering on every keystroke"). State observations as facts about the code, not as a narrator's experience.
Examples:
- Bad: "I wrapped the prop in
useMemobecause the human's list was thrashing." - Good: "Wrapping the prop in
useMemostopped the list from thrashing." - Bad: "You'll recognize these field names from the diff."
- Good: "The field names below come straight from the form."
- Bad: "Claude's fix was to switch to
Record." - Good: "The fix: switch to
Record."
Warm and specific is still the goal — impersonal does not mean dry. Define jargon inline. Short paragraphs (3–5 lines). Include real code snippets in fenced blocks with a short "what to notice" caption underneath.
Smart punctuation in prose. Use typographic characters in article prose and frontmatter (title, description):
- Apostrophes and single quotes:
’(U+2019),‘(U+2018) — e.g.didn’t,’90s. - Double quotes:
“”(U+201C/U+201D). - Em dash
—(U+2014) for breaks; en dash–(U+2013) for ranges; ellipsis…(U+2026) for trailing-off.
Keep ASCII in code. Inside fenced code blocks, inline `code`, demo .tsx files, and any string literal that represents code or a user-typed value, use plain ASCII (', ", --, ...). Smart quotes break syntax and look wrong in a monospace context. The split: prose reads like a book, code reads like code.
No real source paths or proprietary names. The blog is public; the human's day-job repo is not. Do not name actual file paths, internal route paths, internal model/table names, internal product or company names, or anything else that would only make sense to a coworker. Generalize:
app/javascript/components/billing/edit_address_modal.tsx→ "a billing address modal" or just "the form component"/org_deletions/${id}→/jobs/${id}or "a job-status endpoint"delete_organization_modal.tsx→ "a delete-confirmation modal"
Generic function and variable names (handleFieldChange, formErrors, checkForCompletion) are fine — they describe a pattern, not a private codebase. The bar: a reader who has never seen the source repo should still get the lesson, and nothing in the article should leak details specific to the human's employer.
Demo patterns. Every major concept gets a demo or an explicit "(no interactive demo — the snippet above is the core idea)" note. Patterns:
- Slider-driven —
<input type="range">drives a visualization (magnitudes: delay, duration, size). - Before/after toggle — one button flips naive vs improved.
- Step-through — "Next" advances stages (sorts, state machines).
- Live input → output — textarea flows through the code live (parsers, regex, validation).
Demo component rules.
- Plain React + TypeScript.
useState,useMemo, etc. — whatever the article teaches. - No new npm deps unless a demo truly needs one. The site already has React 19 + Astro 6.
- Reach for the shared demo library first. It lives at
src/components/demo/and exposesDemo,Toolbar,Spacer,Button,ToggleGroup,Input,Field,Output,OutputRow,Console,Hint,Row. Import from the article folder with a relative path (../../../components/demo). Use these instead of re-defining inline styles for buttons, mono inputs, output panels, or log output. CSS variablesvar(--mono),var(--ink-soft),var(--rule), plus tone tokensvar(--tone-good|warn|bad)exist; the components already use them. - Keep demos small and self-contained. One file, default export, no external state.
Shared demo components — quick catalog.
import {
Demo, // <Demo>{...}</Demo> — wraps body + adds "Interactive · React" label
Toolbar, // flex-wrap row of controls above the demo body; auto bottom-margin
Spacer, // push subsequent toolbar items to the right (margin-left: auto)
Button, // <Button variant="primary"|"pill" active={bool} ...>
ToggleGroup, // <ToggleGroup value onChange options={[{value, label}]} variant />
Input, // monospaced text input, full-width by default
Field, // labeled row: <Field label="raw"><Input ... /></Field>
Output, // bordered mono panel; pass `grid` for 2-col label/value layout
OutputRow, // <OutputRow label="raw:">{value}</OutputRow>
Console, // <Console lines={[{ line, tone }]} placeholder /> — tone: info|good|warn|bad
Hint, // <Hint>trailing explanatory paragraph</Hint>
Row, // flex-wrap row helper (no margin)
} from '../../../components/demo';When to use which:
- Before/after binary toggle →
<ToggleGroup variant="primary">(two options). - Long list of option chips (scenarios, samples, presets) →
<ToggleGroup variant="pill">. - Step-through "Next" buttons →
<Button>inside<Toolbar>. - Slider → plain
<input type="range">(no wrapper needed; CSS already styles it inside.demo). - Showing computed output (raw → resolved → range) →
<Output grid>with<OutputRow>children. - Log/trace output with colored lines →
<Console lines={...} />(buildConsoleLine[]as you go). - Tone color on inline text →
<span data-tone="good|warn|bad|info">…</span>. Do not hardcode hex colors for status — use tone tokens ordata-tone.
If the demo needs a control the library doesn't ship, write it inline with the same CSS variables (var(--mono), var(--rule), var(--ink-soft), var(--tone-*)) so it visually matches the rest.
Reference article. If a brand-new pattern (not covered above) needs a styling cue, peek at one existing demo under $TIL_DIR/src/content/articles/. Pick the most recent by slug date. Don't read more than one.
Before committing, run a local build to catch MDX/TS errors the deploy would reject:
cd "$TIL_DIR" && npm run buildIf the build fails, fix the errors and rebuild. Don't push broken articles.
From $TIL_DIR:
cd "$TIL_DIR"
git add "src/content/articles/<slug>"
git commit -m "Add TIL: <title>"
git push origin "${TIL_BRANCH:-main}"Commit message: single line, conventional. No Claude co-author trailer — this is the user's personal blog.
Then watch the GitHub Pages deploy:
sleep 3 # let the push register
WORKFLOW="${TIL_DEPLOY_WORKFLOW:-Deploy to GitHub Pages}"
BRANCH="${TIL_BRANCH:-main}"
RUN_ID=$(gh run list --workflow "$WORKFLOW" --branch "$BRANCH" --limit 1 --json databaseId -q '.[0].databaseId')
gh run watch "$RUN_ID" --exit-statusIf gh run watch exits non-zero, surface the failure (gh run view "$RUN_ID" --log-failed | tail -50) and stop — do not open a broken URL.
On success, open the live article:
open "$TIL_URL/<slug>/"Report the live URL to the user.
- Prose lands in the 5–10 minute read range (~1,250–2,500 words).
- Every concept has a demo or an explicit no-demo note.
- Prose readable by someone who hasn't seen the original code; jargon defined inline.
npm run buildpasses locally before pushing.- Article names the user's real files, functions, variables.
- Frontmatter
datematches the slug date. Title is a human sentence, not a slug echo. - Tags are lowercase, short, reusable across articles (
react,typescript,performance) — not hyper-specific.