diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ba7207..77fa4b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,24 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Every release corresponds to a `staging` to `main` pull request and a matching
`vX.Y.Z` tag on `main`.
+## [0.4.0] - 2026-07-17
+
+### Added
+
+- New thread: **"Cron was easy because the job was dumb"** — a deep, practical
+ guide to Claude routines (cron jobs whose payload is an agent). Covers routine
+ anatomy, a gallery of real routines with schedules and prompts, idempotency
+ patterns, a cron syntax refresher, routines vs. loops, and observability.
+ Includes a companion video link and a new `claude` tag.
+- Hand-drawn **pen annotations** for thread prose (`Underline`, `Circle`, `Box`,
+ `Strike`, `Highlight`) that draw themselves on scroll and are gated behind
+ `prefers-reduced-motion`. Wired into MDX, so any thread can use them.
+- **"On this page"** table of contents: a sticky right rail with scroll-spy that
+ highlights the current section and jumps to any heading.
+- **Cron diagram** component: a styled five-field breakdown replacing brittle
+ ASCII art.
+- **Copy to clipboard** buttons on thread code blocks.
+
## [0.3.0] - 2026-07-12
### Added
diff --git a/package-lock.json b/package-lock.json
index bead250..4913903 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "binarysemaphore",
- "version": "0.3.0",
+ "version": "0.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "binarysemaphore",
- "version": "0.3.0",
+ "version": "0.4.0",
"dependencies": {
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
diff --git a/package.json b/package.json
index a1152ce..b8a0e57 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "binarysemaphore",
- "version": "0.3.0",
+ "version": "0.4.0",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/src/app/threads/[slug]/page.tsx b/src/app/threads/[slug]/page.tsx
index d635851..8d61496 100644
--- a/src/app/threads/[slug]/page.tsx
+++ b/src/app/threads/[slug]/page.tsx
@@ -11,6 +11,7 @@ import { threadCovers } from "@/lib/thread-covers";
import { Photo } from "@/components/photo";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
+import { TableOfContents } from "@/components/table-of-contents";
type Params = { slug: string };
@@ -67,6 +68,7 @@ export default async function ThreadPage({
+
+
+
+
diff --git a/src/components/annotate.tsx b/src/components/annotate.tsx
new file mode 100644
index 0000000..540a754
--- /dev/null
+++ b/src/components/annotate.tsx
@@ -0,0 +1,189 @@
+"use client";
+
+/**
+ * Hand-drawn "pen" annotations for long-form thread prose. Wrap any inline text
+ * to circle / underline / box / strike / highlight it, the way you'd mark up a
+ * printed page. Each mark draws itself when it scrolls into view (respecting
+ * `prefers-reduced-motion`, which shows it instantly).
+ *
+ * Wired into MDX in `src/mdx-components.tsx`, so threads can write:
+ * Cron was easy because the job was dumb.
+ *
+ * Stroke colour comes from `currentColor`; set it with a text utility via
+ * `className` (default is the brand accent, i.e. a red-pen look).
+ */
+
+import { useEffect, useRef, useState, type ReactNode } from "react";
+
+type MarkProps = {
+ children: ReactNode;
+ /** Tailwind colour utility for the stroke, e.g. "text-accent-strong". */
+ className?: string;
+};
+
+/** Reveal once, when the element scrolls into view. Reduced-motion => instant. */
+function useDrawn() {
+ const ref = useRef(null);
+ const [drawn, setDrawn] = useState(false);
+
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+
+ const reduce =
+ typeof window !== "undefined" &&
+ window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
+ if (reduce) {
+ const raf = requestAnimationFrame(() => setDrawn(true));
+ return () => cancelAnimationFrame(raf);
+ }
+
+ const io = new IntersectionObserver(
+ (entries) => {
+ for (const entry of entries) {
+ if (entry.isIntersecting) {
+ setDrawn(true);
+ io.disconnect();
+ }
+ }
+ },
+ { threshold: 0.85, rootMargin: "0px 0px -8% 0px" },
+ );
+ io.observe(el);
+ return () => io.disconnect();
+ }, []);
+
+ return { ref, drawn };
+}
+
+/** Common style for a self-drawing stroke path. */
+function strokeStyle(drawn: boolean, ms: number) {
+ return {
+ strokeDasharray: 1,
+ strokeDashoffset: drawn ? 0 : 1,
+ transition: `stroke-dashoffset ${ms}ms cubic-bezier(0.65, 0, 0.35, 1)`,
+ } as const;
+}
+
+/** Slightly wobbly underline that sweeps in under the text. */
+export function Underline({ children, className = "text-blue/80" }: MarkProps) {
+ const { ref, drawn } = useDrawn();
+ return (
+
+ {children}
+
+
+ );
+}
+
+/** Open, hand-drawn ellipse looped around the text. */
+export function Circle({ children, className = "text-violet/80" }: MarkProps) {
+ const { ref, drawn } = useDrawn();
+ return (
+
+ {children}
+
+
+ );
+}
+
+/** Rough rectangle drawn around the text. */
+export function Box({ children, className = "text-lime" }: MarkProps) {
+ const { ref, drawn } = useDrawn();
+ return (
+
+ {children}
+
+
+ );
+}
+
+/** A line struck through the text. */
+export function Strike({ children, className = "text-subtle" }: MarkProps) {
+ const { ref, drawn } = useDrawn();
+ return (
+
+ {children}
+
+
+ );
+}
+
+/** Marker-style highlight that wipes in behind the text. */
+export function Highlight({ children, className = "bg-sun/40" }: MarkProps) {
+ const { ref, drawn } = useDrawn();
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/src/components/code-block.tsx b/src/components/code-block.tsx
new file mode 100644
index 0000000..a02a8a2
--- /dev/null
+++ b/src/components/code-block.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+/**
+ * Thread code blocks with a copy-to-clipboard button.
+ *
+ * Registered as the `pre` element in `src/mdx-components.tsx`. The incoming
+ * props (including rehype-pretty-code's `data-*` attributes and classes) are
+ * spread straight onto the real
, so the highlighted output is preserved
+ * exactly; we only wrap it to position the button.
+ */
+
+import { useRef, useState, type ComponentPropsWithoutRef } from "react";
+
+export function Pre(props: ComponentPropsWithoutRef<"pre">) {
+ const ref = useRef(null);
+ const [copied, setCopied] = useState(false);
+ const timer = useRef | null>(null);
+
+ async function copy() {
+ const text = ref.current?.textContent ?? "";
+ if (!text) return;
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ if (timer.current) clearTimeout(timer.current);
+ timer.current = setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // Clipboard unavailable (insecure context, denied permission): leave the
+ // button idle rather than pretending it worked.
+ }
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/cron-diagram.tsx b/src/components/cron-diagram.tsx
new file mode 100644
index 0000000..766de0e
--- /dev/null
+++ b/src/components/cron-diagram.tsx
@@ -0,0 +1,64 @@
+/**
+ * A clean cron-expression breakdown for use in threads: each field sits above
+ * the name and range it maps to, replacing fragile ASCII box-art. Pure server
+ * component (no client JS). Wildcards render in the accent colour.
+ *
+ * Usage in MDX:
+ */
+
+const FIELDS = [
+ { name: "minute", range: "0–59" },
+ { name: "hour", range: "0–23" },
+ { name: "day-of-month", range: "1–31" },
+ { name: "month", range: "1–12" },
+ { name: "day-of-week", range: "0–7" },
+] as const;
+
+export function CronDiagram({
+ expr = "0 8 * * *",
+ note,
+}: {
+ expr?: string;
+ note?: string;
+}) {
+ const tokens = expr.trim().split(/\s+/).slice(0, 5);
+
+ return (
+
+
+ {note ? (
+
+ → {note}
+
+ ) : null}
+
+ );
+}
diff --git a/src/components/table-of-contents.tsx b/src/components/table-of-contents.tsx
new file mode 100644
index 0000000..d646531
--- /dev/null
+++ b/src/components/table-of-contents.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+/**
+ * "On this page" navigator for a thread. Reads the rendered article's h2/h3
+ * headings (which already carry ids from rehype-slug), lists them, and
+ * highlights the section you're currently reading via IntersectionObserver.
+ * Clicking a heading jumps to it (native anchor + the headings' scroll-mt).
+ *
+ * Renders nothing when there are fewer than two headings, and is hidden on
+ * narrow screens by its parent (scrolling is fine there).
+ */
+
+import { useEffect, useState } from "react";
+
+type Item = { id: string; text: string; level: 2 | 3 };
+
+export function TableOfContents() {
+ const [items, setItems] = useState([]);
+ const [activeId, setActiveId] = useState("");
+
+ useEffect(() => {
+ let io: IntersectionObserver | undefined;
+
+ // Read the rendered headings after paint so we're not calling setState
+ // synchronously in the effect body, and so ids from rehype-slug exist.
+ const raf = requestAnimationFrame(() => {
+ const headings = Array.from(
+ document.querySelectorAll(".thread h2, .thread h3"),
+ ).filter((h) => h.id);
+
+ setItems(
+ headings.map((h) => ({
+ id: h.id,
+ text: h.textContent ?? "",
+ level: h.tagName === "H3" ? 3 : 2,
+ })),
+ );
+
+ if (headings.length < 2) return;
+
+ // Track which heading is nearest the top of the viewport.
+ const visible = new Map();
+ io = new IntersectionObserver(
+ (entries) => {
+ for (const entry of entries) {
+ const id = (entry.target as HTMLElement).id;
+ if (entry.isIntersecting) visible.set(id, entry.boundingClientRect.top);
+ else visible.delete(id);
+ }
+ if (visible.size > 0) {
+ const top = [...visible.entries()].sort((a, b) => a[1] - b[1])[0][0];
+ setActiveId(top);
+ }
+ },
+ { rootMargin: "-80px 0px -68% 0px", threshold: 0 },
+ );
+
+ headings.forEach((h) => io!.observe(h));
+ });
+
+ return () => {
+ cancelAnimationFrame(raf);
+ io?.disconnect();
+ };
+ }, []);
+
+ if (items.length < 2) return null;
+
+ return (
+
+ );
+}
diff --git a/src/content/threads/claude-routines-and-cron.mdx b/src/content/threads/claude-routines-and-cron.mdx
new file mode 100644
index 0000000..0cf3143
--- /dev/null
+++ b/src/content/threads/claude-routines-and-cron.mdx
@@ -0,0 +1,274 @@
+---
+title: "Cron was easy because the job was dumb"
+description: "A deep, practical guide to Claude routines: cron jobs whose payload is an agent, not a script. What that breaks (determinism, idempotency, retries, overlap), a gallery of routines worth building, and how to harden one so it behaves when nobody is watching."
+date: 2026-07-17
+tags: ["ai", "agents", "claude", "cron", "distributed-systems"]
+---
+
+Cron is one of the most durable pieces of software ever written. You give it five fields and
+a command, and for forty years it has run that command on time and gotten out of the way. It
+works because it makes almost no promises. It fires a process; it
+does not care what the process does, whether it succeeded, or whether the
+last one is still running.
+
+Claude *routines* — scheduled agents that run on a cron expression — look like the same idea
+with a smarter payload. Point one at "every morning at 8, review yesterday's failed CI runs
+and open an issue for anything that looks like a real regression," pick a schedule, and it
+runs on its own. Underneath, the scheduling primitive really is just cron.
+
+But the payload is no longer a script. It's a
+nondeterministic agent that reads, decides, and writes to the outside world.
+That single swap invalidates most of the assumptions cron got to be simple by ignoring. This is
+a long, practical note about which assumptions break, the routines actually worth building, and
+how to design one so it behaves when nobody is watching.
+
+> **Prefer to watch?** There's a video on this same topic on YouTube:
+> [youtube.com/watch?v=eSP7PLTXNy8](https://www.youtube.com/watch?v=eSP7PLTXNy8).
+
+## The anatomy of a routine
+
+Strip away the framing and a routine has exactly three parts:
+
+1. **A schedule** — a cron expression (`0 8 * * *`) that decides *when* it wakes up.
+2. **A prompt** — the self-contained instruction that *is* the job.
+3. **A grant** — the tools and permissions the run is allowed to use (a repo, an MCP server,
+ a Slack channel), which decides *what it can touch*.
+
+A plain cron job has the first and third of these; the second is a shell command. The whole
+shift is that the second is now a paragraph of natural language handed to a model. Everything
+strange about routines falls out of that one substitution.
+
+### A routine is a cold start every time
+
+The first thing to internalize: a scheduled run does **not** inherit the conversation you were
+in when you created it. It starts cold. No memory of "the above," no working
+directory you had open, no half-finished reasoning. The prompt you save is
+the entire context the next run gets.
+
+This is the right design — a job that only works because of state trapped in one person's
+terminal session is not a job, it's a demo — but it flips how you write the instruction. You're
+not talking to an assistant that already knows what you meant. You're writing a spec for
+a stranger who will execute it once, alone, months from now:
+
+```text
+Bad: "do that check we talked about and let me know"
+Good: "Fetch the last 24h of runs from the `deploy-production` workflow in
+ BiSemaphore/binarysemaphore. For each failure, read the logs. If the
+ failure is a real regression (not a flaky timeout or a cancelled run),
+ open a GitHub issue titled `CI regression: ` with the failing step
+ and a link. If everything passed, do nothing and post no message."
+```
+
+Everything the run needs — repo, tool, definition of "real regression," what to do on the happy
+path — is in the text, because there is nothing else. The discipline this forces is the same
+discipline behind a good `README` or a good on-call runbook:
+assume the reader has none of your context,
+because they don't.
+
+## A gallery of routines worth building
+
+The abstract argument lands better against concrete jobs. Here are routines that earn their
+keep, each with a schedule and the shape of its prompt. Notice how every one ends with an
+explicit otherwise, do nothing — that clause is load-bearing, and we'll come
+back to why.
+
+**Nightly CI regression triage** — turn a wall of red into a short list of real bugs.
+
+```text
+schedule: 0 7 * * 1-5 # 07:00 on weekdays
+prompt: Read failed runs from the last 24h in . Cluster them.
+ Open one issue per distinct real regression (skip flaky timeouts and
+ cancelled runs), each with the failing step and a permalink. If an
+ open issue with that title already exists, comment instead. Otherwise
+ do nothing.
+```
+
+**Morning dependency & advisory digest** — the security email you'd never read, summarized.
+
+```text
+schedule: 30 8 * * 1 # 08:30 every Monday
+prompt: List dependencies with a new release or CVE since last Monday. For
+ each, one line: package, old -> new, why it matters, breaking? Post a
+ single message to #eng. If nothing changed, say "no updates" once.
+```
+
+**Stale issue & PR sweep** — the janitorial pass nobody volunteers for.
+
+```text
+schedule: 0 16 * * 5 # 16:00 every Friday
+prompt: Find issues with no activity in 30 days and PRs green but unmerged for
+ 7. Add a "stale" comment with a specific next step, not a generic ping.
+ Never close anything. Summarize what you touched in one message.
+```
+
+**On-call handoff** — a written summary at the shift boundary.
+
+```text
+schedule: 0 9,21 * * * # 09:00 and 21:00 daily
+prompt: Summarize alerts, deploys, and incident-channel activity since the
+ last handoff. What's still open, what to watch. Post to #on-call.
+```
+
+**Docs freshness check** — catch the doc that now lies.
+
+```text
+schedule: 0 3 * * 2 # 03:00 every Tuesday
+prompt: Find docs referencing code paths, flags, or env vars that no longer
+ exist in the repo. Open one issue listing them with file:line. If the
+ list is empty, do nothing.
+```
+
+Two patterns run through the whole gallery. First, **narrow verbs**: "cluster," "comment,"
+"summarize," never "manage". Second, **a defined empty case**: the boring
+night, where the right output is nothing at all. A routine that can't stay quiet becomes noise
+you learn to ignore, and an ignored routine is worse than no routine.
+
+## Cron's promises, and which ones the agent revokes
+
+Cron's simplicity came from a short list of things it refused to guarantee. Each one was fine
+when the payload was `backup.sh`. Each one is now yours to think about.
+
+| Cron quietly assumes | Fine for a script because | Now that the payload is an agent |
+| --- | --- | --- |
+| The job is deterministic | Same input, same output | Same prompt can produce different actions each run |
+| Retrying is safe | Re-running `rsync` is idempotent | A retry might file the issue *again* |
+| Overlap is your problem | Two `backup.sh` rarely race | Two runs can both act on the same event |
+| Exit code 0 means success | The script knows if it failed | "Success" is a judgement the model is making |
+| Missed runs just… don't happen | You'll notice a stale backup | A skipped morning review is silent |
+
+None of these are reasons not to use routines. They're the checklist you'd apply to any
+unattended distributed job, surfaced by the fact that the payload can now think. The good news
+is the fixes are old and well understood.
+
+### Make the action idempotent, not the agent
+
+You cannot make the model deterministic, and you shouldn't try. What you *can* do is
+make the side effect safe to repeat. This is the same move you make with
+any at-least-once system: push idempotency into
+the write, not the caller. A few patterns cover almost everything:
+
+- **Check before write.** "Open an issue *if one with this title doesn't already exist*." The
+ title becomes a natural idempotency key.
+- **Scope by period.** "Post the digest *for this ISO week*, and check the channel for one
+ already posted this week first." The time window is the key.
+- **Prefer upsert over insert.** Update-or-create against a stable identifier instead of
+ blindly appending, so the second run converges instead of duplicating.
+- **Make "nothing to do" a first-class outcome.** The happy path for most runs is a no-op, and
+ the prompt should say so explicitly, or the model will invent activity to look useful.
+
+Here's the same routine written the naive way and the safe way. The diff is small and it is the
+whole ballgame:
+
+```diff
+- Open a GitHub issue for the regression.
+- Post a summary to #eng.
++ If an open issue titled "CI regression: " exists, add a comment; else open it.
++ Post the summary tagged with today's date; if today's is already in #eng, skip.
+```
+
+The routine is allowed to run twice — because eventually it will — and the second run is a
+no-op. A cron job for a dumb script gets this for free. A routine gets it because you designed
+the *target* of the action to tolerate a duplicate.
+
+### Decide what overlap means before it happens
+
+If your review job takes eleven minutes and you schedule it every five, cron will happily start
+a second one while the first is still going. For `backup.sh` that's an annoyance. For an agent
+with write access it's two runs reasoning about the same
+world and possibly both acting on it — the classic double-file, now with extra
+steps.
+
+Pick a schedule with generous headroom over the worst-case runtime, and lean on the idempotent
+writes above as the real backstop, since headroom is a guess and
+idempotency is a guarantee.
+
+## A cron refresher, because the boring part still bites
+
+The agent is the interesting part, but the cron expression itself is still where a lot of
+routines quietly go wrong. Worth having the five fields cold:
+
+
+
+Each field takes more than a single number:
+
+- `*` — every value. `*/15` — every 15th (a **step**): `*/15 * * * *` is every quarter hour.
+- `1-5` — a **range** (Monday to Friday in the weekday field).
+- `1,15` — a **list** (the 1st and the 15th).
+- `@daily`, `@hourly`, `@weekly`, `@reboot` — shorthands some schedulers accept for the common
+ cases.
+
+Three failure modes survive intact into the AI era:
+
+- **Time zones.** `0 8 * * *` is 8am in *some* zone. If you don't know which, you don't know
+ when your routine runs. State the zone explicitly; don't let a server's
+ `UTC` default decide your "every morning" is happening at midnight local.
+- **Daylight saving.** Twice a year a wall-clock time either doesn't exist or happens twice. A
+ job at `2:30` on the spring-forward night can be skipped entirely; on the fall-back night it
+ can fire twice. If the run matters, keep it out of the 1am–3am window, and — again — make it
+ idempotent so a double-fire is harmless.
+- **The day-of-month / day-of-week trap.** When you set *both* of those fields, cron treats
+ them as OR, not AND. `0 9 13 * 5` is not "the 13th if it's a Friday." It's
+ "the 13th, *and also* every Friday." This has been surprising people since the 1970s and it
+ will surprise you too. Want "the 13th only when it's Friday"? You can't say it in one
+ expression; schedule the 13th and let the job check the weekday itself.
+
+The routine wrapper doesn't rescue you from any of this. Cron semantics are cron semantics; the
+agent just does something more expensive than usual when the expression is wrong.
+
+## Routines vs. loops: schedule vs. session
+
+There's a neighbouring feature worth drawing a line against, because they solve different
+problems. A **loop** re-runs a prompt on an interval *inside the current session* — it keeps
+your context and is meant for "watch this thing until it's done": babysit a deploy, poll a build,
+iterate on a task while you're around. A **routine** is a scheduled *cloud* run with no session
+and no you: it wakes up cold, does one self-contained job, and exits.
+
+The rule of thumb: if the work needs the conversation you're in right now,
+it's a loop. If the work should happen whether or not you're at your
+machine, it's a routine. Reaching for a
+routine to babysit something you're actively watching gives you a cold agent fumbling for
+context it would have had for free in-session. Reaching for a loop to run a nightly job means it
+only runs on the nights you happened to leave the session open.
+
+## Knowing it ran: observability for something you never see
+
+A cron job mailed you its stdout. A routine, left alone, is a black box that may or may not be
+doing its work at 3am. Before you trust one, wire up three things:
+
+- **A heartbeat.** Have it leave a trace even on the empty case — a one-line "checked CI, all
+ green" — at least until you trust it.
+ A silent routine is indistinguishable from a broken one, and "it hasn't
+ posted in a week" should read as *alarming*, not *calm*.
+- **A dry run.** For anything with a write, run it once in a mode where it reports what it
+ *would* do instead of doing it. You are
+ reviewing the model's judgement before you let it act unattended; do
+ that while you're watching.
+- **A bounded blast radius.** Same instinct as [starting an MCP server
+ read-only](/threads/an-mcp-server-for-your-notes): give the routine exactly the reach its job
+ needs and nothing more. An unattended agent with broad write access is a
+ standing risk that fires on a timer.
+
+## Design a routine like an unattended job, because it is one
+
+Strip away the novelty and a routine is a cron-scheduled batch job whose worker happens to be a
+language model. The engineering that makes it trustworthy is the same engineering that makes any
+unattended job trustworthy, and none of it is new:
+
+1. **Keep the scope narrow.** One routine, one job. "Review CI failures"
+ is a routine. "Manage the repo" is a wish. A tight objective is easier to write, cheaper to
+ run, and far easier to trust when nobody's watching.
+2. **Make success and failure observable.** Leave a trace even on the "nothing to do" path.
+ Cron mailed you the job's stdout for exactly this reason.
+3. **Make the writes idempotent.** The single highest-leverage habit.
+ Assume at-least-once, design for it, stop worrying about double-fires.
+4. **Bound the blast radius.** Give the routine exactly the reach its job needs and nothing
+ more.
+5. **Define the empty case.** Say what "nothing happened" looks like, or the model will fill the
+ silence.
+
+Cron got to be simple because it never had to understand its payload. Routines hand the payload
+a mind, which is genuinely useful — a nightly job that can *read logs and judge* is worth a
+lot — but the price is that all the guarantees cron waved off are now yours to provide. Provide
+them, and a routine is a tireless teammate that shows up on
+schedule. Skip them, and it's a very articulate way to do the wrong thing at 8am
+every day.
diff --git a/src/mdx-components.tsx b/src/mdx-components.tsx
index 0662326..7eb3950 100644
--- a/src/mdx-components.tsx
+++ b/src/mdx-components.tsx
@@ -1,6 +1,9 @@
import type { MDXComponents } from "mdx/types";
import Link from "next/link";
import type { AnchorHTMLAttributes } from "react";
+import { Underline, Circle, Box, Strike, Highlight } from "@/components/annotate";
+import { CronDiagram } from "@/components/cron-diagram";
+import { Pre } from "@/components/code-block";
/**
* Global MDX element styling for thread content. Required by @next/mdx in the
@@ -84,6 +87,16 @@ const components: MDXComponents = {
td: (props) => (
),
+ // Hand-drawn pen annotations, usable inline in any thread.
+ Underline,
+ Circle,
+ Box,
+ Strike,
+ Highlight,
+ // Cron expression breakdown.
+ CronDiagram,
+ // Code blocks, with a copy-to-clipboard button.
+ pre: Pre,
};
export function useMDXComponents(): MDXComponents {