# Blog Post

> Author a new entry for the Semiotic blog. Use this skill whenever the user asks for a blog post, release summary, chart explainer, or case study to publish at /blog/SLUG.

- Skill: `nteract/blog-post` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add nteract/blog-post`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nteract/blog-post/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: nteract (https://skillmd.com/u/nteract)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nteract/blog-post

---


# Writing a Semiotic blog post

This skill writes one entry for the Semiotic blog. The blog lives at
`/blog/`; entries live at `/blog/<slug>/`. Three story shapes are
supported — pick the one that matches what's being written:

1. **Chart explainer** — single chart, "what / why / when / wiring".
2. **Release summary** — what's new in a version, ordered by impact.
3. **Narrative / case study** — comparative posts ("X vs Y"),
   walkthroughs, recreations of historical visualizations.

Every entry follows the same structure regardless of shape. The
**why-care** section is non-negotiable: the post has to give the
reader a reason to read it that lands even if they're not currently
using Semiotic. The blog isn't reference docs; it's a publication
that happens to be hosted on the docs site.

## Before writing anything

**Ask the user for the author byline** unless they've already given
one. Don't assume "Elijah Meeks" by default — many entries will be
co-authored or attributed to "Semiotic Team" for releases. One short
question, then proceed.

## File structure

For a new entry with slug `<slug>`:

1. **Body component** — `docs/src/blog/entries/<slug>.jsx`. Default
   export is `{ slug, title, subtitle, author, date, tags, excerpt,
   component, ogChart? }`. The `component` is a React function
   returning JSX (the entry's body — no header, no chrome, the
   `BlogEntryView` wraps it).
2. **Register in registry** — `docs/src/blog/entries.js` imports
   the new file and adds it to `blogEntries`.
3. **Register in metadata mirror** — `docs/src/blog/entries-meta.js`
   gets the same metadata object (without `component`, without the
   React imports). This mirror is read by the OG-card generator
   and the prerender script, both of which run under plain Node
   and can't load JSX.

Both registry files must stay in sync. The OG-card generator and
prerender script read `entries-meta.js`; the React app reads
`entries.js`.

## Required fields

```js
{
  slug: "kebab-case-route",
  title: "Title-Case Headline",
  subtitle: "One or two sentences orienting the reader.",
  author: "Author Name",                  // ASK THE USER if unspecified
  date: "YYYY-MM-DD",                     // ISO; controls sort order
  tags: ["release"] | ["chart-explainer", "xy"] | ...,
  excerpt: "2–3 sentence preview shown on the index card.",
  component: Body,                        // function returning JSX
  ogChart: { component: "DifferenceChart" }, // optional, see OG step
}
```

### Tags vocabulary

Pick freely from:

- **Shape**: `release`, `chart-explainer`, `case-study`, `tutorial`
- **Family**: `xy`, `network`, `geo`, `ordinal`, `realtime`,
  `hierarchy`

Multi-tag is fine and encouraged. Don't invent new top-level tags
without checking the existing taxonomy in `entries.js`.

## The skeleton — applies to all shapes

Every entry MUST have these sections (with the names below as h2
headings, except the intro):

1. **Opening paragraph** (no heading) — one paragraph that orients
   the reader. State the chart / topic in concrete terms. Don't
   start with "In this post we will…". Start with the thing.
2. **Why this exists / why care** — answer "why should I care about
   this if I'm not currently using Semiotic?". Even chart-explainer
   posts need this: tell the data-viz audience what makes the
   chart-type interesting, then connect it to Semiotic's
   implementation.
3. **The thing itself** — live demo, or release-note bullets, or
   the comparative pair. This is the meat. Show, don't tell.
4. **How to read / how it works** — once the reader has seen the
   thing, walk them through how to read the visual encoding (for
   chart posts) or where to look for the API change (for releases).
5. **When to reach for it / when not** — guidance. Pair every "use
   it for X" with "don't use it for Y, use Z instead". This is
   the section that earns the reader's trust.
6. **Wiring it up** — minimal code snippet showing the prop shape.
   For releases, link to the changelog and migration notes.
7. **Related** — link to neighbor charts, related features, and
   the full reference page.

The Why and the When-to-reach sections are what distinguish a
Semiotic blog post from the reference docs at `/charts/<name>`.
The reference doc tells you what's there; the blog post tells you
when you'd care.

## Story-shape specifics

### Chart explainer

- Title format: `<ChartName>, explained`.
- Opening: one-sentence elevator pitch. ("DifferenceChart is the
  chart you reach for when the story is the gap between two
  series, not either series on its own.")
- Why-care section: cover the general data-viz problem the chart
  solves, NOT the Semiotic-specific API. The same audience that
  reads HN data-viz threads should find this useful. Then add a
  paragraph relating it to Semiotic's implementation (e.g. "in
  Semiotic this is wired through…").
- Live demo: one self-contained chart with inline synthetic data.
  Keep the data small enough that the reader can imagine the
  underlying rows (5–15 rows is the sweet spot).
- When-to-reach section: list 3–5 cases for it, then 3 cases
  against it pointing to the right alternative chart.
- Wiring section: ≤15 lines of code. Just the minimum props.
- **Streaming / push mode section** — REQUIRED for every chart
  explainer. Three pieces:
  1. **A live push demo** using `BlogPushDemo` from
     `docs/src/blog/components/BlogPushDemo.jsx`. Hand it a
     `chartRef`, the `frames` array (one entry per step), a
     `pushAt(ref, row, i)` callback that calls the chart's
     push method, and a `resetAt(ref)` callback that calls
     `clear()`. The demo gives the reader Play / Step / Reset
     controls and a step counter for free.
  2. **A push-mode wiring snippet** — ≤15 lines — showing the
     ref, the `push()` / `update()` calls relevant to the
     chart, and any required `*IdAccessor` (XY charts want
     `pointIdAccessor`; ordinal charts want `dataIdAccessor`;
     network HOCs use `nodeIDAccessor` / `edgeIdAccessor`).
  3. **A "why push helps here" paragraph** specific to this
     chart's nature. Generic boilerplate is worthless; the
     story has to land on a property the reader can map back
     to their own code. Examples from the seeded entries:
       - DifferenceChart: segment recomputation is cheap and
         in-buffer; setting `data` on every tick triggers
         React reconciliation that push skips.
       - QuadrantChart: `update(id, fn)` mutates one point
         without re-keying the rest; preserves hover and
         in-flight tooltips.
       - FunnelChart: bar-and-trapezoid size deltas are
         animated; `data` resets lose the animation.
  Charts that explicitly DO NOT support push (hierarchy HOCs:
  OrbitDiagram, TreeDiagram, Treemap, CirclePack) get a
  different streaming section that explains WHY push doesn't
  apply (the layout reads the full tree, not incremental
  appends) and the pattern that does work (set the `data`
  prop to a new tree; the chart's transitions still ease
  cleanly between trees).
- Tags: `["chart-explainer", "<family>"]`.

#### Push-mode demo skeleton

Inside the entry file, alongside the static `Body` function,
declare a `PushDemo` function that wires `BlogPushDemo`:

```jsx
function PushDemo() {
  const chartRef = useRef(null)
  return (
    <div style={chartFrame}>
      <ThemeProvider theme="carbon-dark">
        <BlogPushDemo
          chartRef={chartRef}
          frames={DEMO_DATA}                 // array, one item per step
          pushAt={(ref, row) => ref?.push?.(row)}
          resetAt={(ref) => ref?.clear?.()}
        >
          <YourChart
            ref={chartRef}
            // ...the same props as the static demo, MINUS `data`
            pointIdAccessor="id"             // or dataIdAccessor etc.
          />
        </BlogPushDemo>
      </ThemeProvider>
    </div>
  )
}
```

Then reference `<PushDemo />` from inside `<Body>`'s streaming
section. Keep the same chart frame styling as the static demo so
the visual continuity between the two reads as "same chart, two
flavors."

### Release summary

- Title format: `Semiotic <X.Y.Z>` (no "released today" or other
  date-stamped language; the entry's own date carries that).
- Opening: one sentence summarizing the release's theme ("3.5.2 is
  mostly a factor-and-extend release."). Link to the full
  CHANGELOG entry on GitHub.
- Why-care section: optional for releases, but if there's a
  big-picture story (new hook family, new chart, architecture
  shift) tell it here.
- Sections: one h2 per major feature group, ordered by impact.
  Use the actual feature names so the reader can grep CHANGELOG.
- Upgrade notes h2: any breakages or behavior changes, even small
  ones. Be explicit about what to do if affected.
- No live demos required; link to the docs pages for new features.
- Tags: `["release"]`.

### Narrative / case study

- Title format: pick a memorable one. `X vs Y` works; so does
  `<famous-thing>, rebuilt in Semiotic`.
- Opening: state the comparison or the recreation in the first
  paragraph. Include the punchline. Don't bury it.
- Why-care section: the reader is here because the topic is
  interesting independently of Semiotic. Lean into that. If
  you're rebuilding Minard's map, say what makes Minard's map
  the canonical example of data-viz composition. If you're
  comparing two chart types, say what makes the question
  "which one?" hard.
- Multiple demos throughout. Comparative posts ideally show the
  two charts side-by-side or stacked.
- Add a final h2 that lists 3–5 OTHER domains where the same
  story plays out. ("This pattern also shows up in
  pull-request lifecycle, supply-chain logistics, financial
  settlement, manufacturing rework.") The blog audience often
  isn't in the example domain; the cross-references are what
  make the post useful.
- Tags: `["case-study", ...]`. Add a family tag if the post is
  centered on one chart family.

## OG card

Each entry produces a 1200×630 PNG at
`docs/public/blog/og/<slug>.png` for social previews. Layout:

- Left 2/3 — designed text: "Semiotic · BLOG" brand row, large
  title, subtitle, byline + date, tags row.
- Right 1/3 — chart panel. When `ogChart` is set in metadata, the
  generator renders that chart via `semiotic/server`'s
  `renderChart` and embeds the SVG. When omitted (release-summary
  posts, narrative posts without a single canonical chart), the
  panel renders a brand placeholder.

To add a chart preview:

```js
ogChart: {
  component: "DifferenceChart",      // chart name as known to renderChart
  props: { /* optional overrides */ }
}
```

Supported chart components live in
`scripts/generate-blog-og-cards.mjs`'s `OG_CHART_PRESETS`. Add a
new preset there if the chart you want isn't listed — preset
fields are `chartType` + `defaults` (props object). The
`renderChart` function only knows the chart families it has
config for; check `src/components/server/serverChartConfigs.ts`
to see what's supported. Charts not in `renderChart` (e.g.
QuadrantChart, OrbitDiagram, AnscombesSankey, MinardsMarch) fall
through to the brand-only card.

Run `npm run generate:blog-og-cards` after registering a new
entry to refresh `docs/public/blog/og/<slug>.png`. The website
build pipeline runs this automatically (it sits between
`generate:demo-gifs` and `parcel build` in `website:build`).

## SEO / pre-rendering

The blog inherits the docs' static-prerender path
(`scripts/prerender.mjs`). For each blog entry, the script:

- Reads metadata from `docs/src/blog/entries-meta.js`.
- Writes `docs/build/blog/<slug>/index.html` with the title set
  to `<entry-title> — Semiotic Blog`.
- Injects per-entry `<meta name="description">`,
  `og:type=article`, `og:title`, `og:description`, `og:image`
  (the rendered card PNG), `article:published_time`,
  `article:author`, per-tag `article:tag`, the full
  `twitter:summary_large_image` block, and a `BlogPosting`
  JSON-LD payload.

No additional wiring required — registering the entry in
`entries-meta.js` is what the prerender script reads. Crawlers
get a fully-resolved meta block; humans get the same SPA-loaded
React experience.

## Verification

Before declaring an entry done, run:

```bash
# Typecheck
npm run typescript

# OG card generation
npm run generate:blog-og-cards

# Open in dev server after building the library
npm run website:start   # → http://localhost:3000/blog/<slug>/
```

Check that:

- The entry appears in `/blog/` (most recent in full, or in the
  preview list below).
- The entry renders at `/blog/<slug>/` with title, subtitle,
  byline, tags, and body content.
- The OG card PNG was written and has the entry's title,
  subtitle, byline, and (if `ogChart` set) a rendered chart on
  the right.
- The site builds: `npm run website:build` succeeds and
  `docs/build/blog/<slug>/index.html` has the entry-specific
  meta tags injected into `<head>`.

## Template starter

A starter template is in `templates/`. Copy the shape that
matches what you're writing:

- `templates/chart-explainer.js`
- `templates/release-summary.js`
- `templates/narrative.js`

Each template has placeholder sections at the right heading
levels and TODO comments at each spot the author needs to fill
in. Use the templates as a checklist — every TODO needs an
answer before publishing.

