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:
- Chart explainer — single chart, "what / why / when / wiring".
- Release summary — what's new in a version, ordered by impact.
- 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>:
- 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).
- Register in registry —
docs/src/blog/entries.js imports
the new file and adds it to blogEntries.
- 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
{
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):
- 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.
- 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.
- The thing itself — live demo, or release-note bullets, or
the comparative pair. This is the meat. Show, don't tell.
- 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).
- 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.
- Wiring it up — minimal code snippet showing the prop shape.
For releases, link to the changelog and migration notes.
- 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:
- 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.
- 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).
- 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:
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:
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:
# 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.
1---2name: blog-post3description: 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.4---56# Writing a Semiotic blog post78This skill writes one entry for the Semiotic blog. The blog lives at9`/blog/`; entries live at `/blog/<slug>/`. Three story shapes are10supported — pick the one that matches what's being written:11121. **Chart explainer** — single chart, "what / why / when / wiring".132. **Release summary** — what's new in a version, ordered by impact.143. **Narrative / case study** — comparative posts ("X vs Y"),15 walkthroughs, recreations of historical visualizations.1617Every entry follows the same structure regardless of shape. The18**why-care** section is non-negotiable: the post has to give the19reader a reason to read it that lands even if they're not currently20using Semiotic. The blog isn't reference docs; it's a publication21that happens to be hosted on the docs site.2223## Before writing anything2425**Ask the user for the author byline** unless they've already given26one. Don't assume "Elijah Meeks" by default — many entries will be27co-authored or attributed to "Semiotic Team" for releases. One short28question, then proceed.2930## File structure3132For a new entry with slug `<slug>`:33341. **Body component** — `docs/src/blog/entries/<slug>.jsx`. Default35 export is `{ slug, title, subtitle, author, date, tags, excerpt,36 component, ogChart? }`. The `component` is a React function37 returning JSX (the entry's body — no header, no chrome, the38 `BlogEntryView` wraps it).392. **Register in registry** — `docs/src/blog/entries.js` imports40 the new file and adds it to `blogEntries`.413. **Register in metadata mirror** — `docs/src/blog/entries-meta.js`42 gets the same metadata object (without `component`, without the43 React imports). This mirror is read by the OG-card generator44 and the prerender script, both of which run under plain Node45 and can't load JSX.4647Both registry files must stay in sync. The OG-card generator and48prerender script read `entries-meta.js`; the React app reads49`entries.js`.5051## Required fields5253```js54{55 slug: "kebab-case-route",56 title: "Title-Case Headline",57 subtitle: "One or two sentences orienting the reader.",58 author: "Author Name", // ASK THE USER if unspecified59 date: "YYYY-MM-DD", // ISO; controls sort order60 tags: ["release"] | ["chart-explainer", "xy"] | ...,61 excerpt: "2–3 sentence preview shown on the index card.",62 component: Body, // function returning JSX63 ogChart: { component: "DifferenceChart" }, // optional, see OG step64}65```6667### Tags vocabulary6869Pick freely from:7071- **Shape**: `release`, `chart-explainer`, `case-study`, `tutorial`72- **Family**: `xy`, `network`, `geo`, `ordinal`, `realtime`,73 `hierarchy`7475Multi-tag is fine and encouraged. Don't invent new top-level tags76without checking the existing taxonomy in `entries.js`.7778## The skeleton — applies to all shapes7980Every entry MUST have these sections (with the names below as h281headings, except the intro):82831. **Opening paragraph** (no heading) — one paragraph that orients84 the reader. State the chart / topic in concrete terms. Don't85 start with "In this post we will…". Start with the thing.862. **Why this exists / why care** — answer "why should I care about87 this if I'm not currently using Semiotic?". Even chart-explainer88 posts need this: tell the data-viz audience what makes the89 chart-type interesting, then connect it to Semiotic's90 implementation.913. **The thing itself** — live demo, or release-note bullets, or92 the comparative pair. This is the meat. Show, don't tell.934. **How to read / how it works** — once the reader has seen the94 thing, walk them through how to read the visual encoding (for95 chart posts) or where to look for the API change (for releases).965. **When to reach for it / when not** — guidance. Pair every "use97 it for X" with "don't use it for Y, use Z instead". This is98 the section that earns the reader's trust.996. **Wiring it up** — minimal code snippet showing the prop shape.100 For releases, link to the changelog and migration notes.1017. **Related** — link to neighbor charts, related features, and102 the full reference page.103104The Why and the When-to-reach sections are what distinguish a105Semiotic blog post from the reference docs at `/charts/<name>`.106The reference doc tells you what's there; the blog post tells you107when you'd care.108109## Story-shape specifics110111### Chart explainer112113- Title format: `<ChartName>, explained`.114- Opening: one-sentence elevator pitch. ("DifferenceChart is the115 chart you reach for when the story is the gap between two116 series, not either series on its own.")117- Why-care section: cover the general data-viz problem the chart118 solves, NOT the Semiotic-specific API. The same audience that119 reads HN data-viz threads should find this useful. Then add a120 paragraph relating it to Semiotic's implementation (e.g. "in121 Semiotic this is wired through…").122- Live demo: one self-contained chart with inline synthetic data.123 Keep the data small enough that the reader can imagine the124 underlying rows (5–15 rows is the sweet spot).125- When-to-reach section: list 3–5 cases for it, then 3 cases126 against it pointing to the right alternative chart.127- Wiring section: ≤15 lines of code. Just the minimum props.128- **Streaming / push mode section** — REQUIRED for every chart129 explainer. Three pieces:130 1. **A live push demo** using `BlogPushDemo` from131 `docs/src/blog/components/BlogPushDemo.jsx`. Hand it a132 `chartRef`, the `frames` array (one entry per step), a133 `pushAt(ref, row, i)` callback that calls the chart's134 push method, and a `resetAt(ref)` callback that calls135 `clear()`. The demo gives the reader Play / Step / Reset136 controls and a step counter for free.137 2. **A push-mode wiring snippet** — ≤15 lines — showing the138 ref, the `push()` / `update()` calls relevant to the139 chart, and any required `*IdAccessor` (XY charts want140 `pointIdAccessor`; ordinal charts want `dataIdAccessor`;141 network HOCs use `nodeIDAccessor` / `edgeIdAccessor`).142 3. **A "why push helps here" paragraph** specific to this143 chart's nature. Generic boilerplate is worthless; the144 story has to land on a property the reader can map back145 to their own code. Examples from the seeded entries:146 - DifferenceChart: segment recomputation is cheap and147 in-buffer; setting `data` on every tick triggers148 React reconciliation that push skips.149 - QuadrantChart: `update(id, fn)` mutates one point150 without re-keying the rest; preserves hover and151 in-flight tooltips.152 - FunnelChart: bar-and-trapezoid size deltas are153 animated; `data` resets lose the animation.154 Charts that explicitly DO NOT support push (hierarchy HOCs:155 OrbitDiagram, TreeDiagram, Treemap, CirclePack) get a156 different streaming section that explains WHY push doesn't157 apply (the layout reads the full tree, not incremental158 appends) and the pattern that does work (set the `data`159 prop to a new tree; the chart's transitions still ease160 cleanly between trees).161- Tags: `["chart-explainer", "<family>"]`.162163#### Push-mode demo skeleton164165Inside the entry file, alongside the static `Body` function,166declare a `PushDemo` function that wires `BlogPushDemo`:167168```jsx169function PushDemo() {170 const chartRef = useRef(null)171 return (172 <div style={chartFrame}>173 <ThemeProvider theme="carbon-dark">174 <BlogPushDemo175 chartRef={chartRef}176 frames={DEMO_DATA} // array, one item per step177 pushAt={(ref, row) => ref?.push?.(row)}178 resetAt={(ref) => ref?.clear?.()}179 >180 <YourChart181 ref={chartRef}182 // ...the same props as the static demo, MINUS `data`183 pointIdAccessor="id" // or dataIdAccessor etc.184 />185 </BlogPushDemo>186 </ThemeProvider>187 </div>188 )189}190```191192Then reference `<PushDemo />` from inside `<Body>`'s streaming193section. Keep the same chart frame styling as the static demo so194the visual continuity between the two reads as "same chart, two195flavors."196197### Release summary198199- Title format: `Semiotic <X.Y.Z>` (no "released today" or other200 date-stamped language; the entry's own date carries that).201- Opening: one sentence summarizing the release's theme ("3.5.2 is202 mostly a factor-and-extend release."). Link to the full203 CHANGELOG entry on GitHub.204- Why-care section: optional for releases, but if there's a205 big-picture story (new hook family, new chart, architecture206 shift) tell it here.207- Sections: one h2 per major feature group, ordered by impact.208 Use the actual feature names so the reader can grep CHANGELOG.209- Upgrade notes h2: any breakages or behavior changes, even small210 ones. Be explicit about what to do if affected.211- No live demos required; link to the docs pages for new features.212- Tags: `["release"]`.213214### Narrative / case study215216- Title format: pick a memorable one. `X vs Y` works; so does217 `<famous-thing>, rebuilt in Semiotic`.218- Opening: state the comparison or the recreation in the first219 paragraph. Include the punchline. Don't bury it.220- Why-care section: the reader is here because the topic is221 interesting independently of Semiotic. Lean into that. If222 you're rebuilding Minard's map, say what makes Minard's map223 the canonical example of data-viz composition. If you're224 comparing two chart types, say what makes the question225 "which one?" hard.226- Multiple demos throughout. Comparative posts ideally show the227 two charts side-by-side or stacked.228- Add a final h2 that lists 3–5 OTHER domains where the same229 story plays out. ("This pattern also shows up in230 pull-request lifecycle, supply-chain logistics, financial231 settlement, manufacturing rework.") The blog audience often232 isn't in the example domain; the cross-references are what233 make the post useful.234- Tags: `["case-study", ...]`. Add a family tag if the post is235 centered on one chart family.236237## OG card238239Each entry produces a 1200×630 PNG at240`docs/public/blog/og/<slug>.png` for social previews. Layout:241242- Left 2/3 — designed text: "Semiotic · BLOG" brand row, large243 title, subtitle, byline + date, tags row.244- Right 1/3 — chart panel. When `ogChart` is set in metadata, the245 generator renders that chart via `semiotic/server`'s246 `renderChart` and embeds the SVG. When omitted (release-summary247 posts, narrative posts without a single canonical chart), the248 panel renders a brand placeholder.249250To add a chart preview:251252```js253ogChart: {254 component: "DifferenceChart", // chart name as known to renderChart255 props: { /* optional overrides */ }256}257```258259Supported chart components live in260`scripts/generate-blog-og-cards.mjs`'s `OG_CHART_PRESETS`. Add a261new preset there if the chart you want isn't listed — preset262fields are `chartType` + `defaults` (props object). The263`renderChart` function only knows the chart families it has264config for; check `src/components/server/serverChartConfigs.ts`265to see what's supported. Charts not in `renderChart` (e.g.266QuadrantChart, OrbitDiagram, AnscombesSankey, MinardsMarch) fall267through to the brand-only card.268269Run `npm run generate:blog-og-cards` after registering a new270entry to refresh `docs/public/blog/og/<slug>.png`. The website271build pipeline runs this automatically (it sits between272`generate:demo-gifs` and `parcel build` in `website:build`).273274## SEO / pre-rendering275276The blog inherits the docs' static-prerender path277(`scripts/prerender.mjs`). For each blog entry, the script:278279- Reads metadata from `docs/src/blog/entries-meta.js`.280- Writes `docs/build/blog/<slug>/index.html` with the title set281 to `<entry-title> — Semiotic Blog`.282- Injects per-entry `<meta name="description">`,283 `og:type=article`, `og:title`, `og:description`, `og:image`284 (the rendered card PNG), `article:published_time`,285 `article:author`, per-tag `article:tag`, the full286 `twitter:summary_large_image` block, and a `BlogPosting`287 JSON-LD payload.288289No additional wiring required — registering the entry in290`entries-meta.js` is what the prerender script reads. Crawlers291get a fully-resolved meta block; humans get the same SPA-loaded292React experience.293294## Verification295296Before declaring an entry done, run:297298```bash299# Typecheck300npm run typescript301302# OG card generation303npm run generate:blog-og-cards304305# Open in dev server after building the library306npm run website:start # → http://localhost:3000/blog/<slug>/307```308309Check that:310311- The entry appears in `/blog/` (most recent in full, or in the312 preview list below).313- The entry renders at `/blog/<slug>/` with title, subtitle,314 byline, tags, and body content.315- The OG card PNG was written and has the entry's title,316 subtitle, byline, and (if `ogChart` set) a rendered chart on317 the right.318- The site builds: `npm run website:build` succeeds and319 `docs/build/blog/<slug>/index.html` has the entry-specific320 meta tags injected into `<head>`.321322## Template starter323324A starter template is in `templates/`. Copy the shape that325matches what you're writing:326327- `templates/chart-explainer.js`328- `templates/release-summary.js`329- `templates/narrative.js`330331Each template has placeholder sections at the right heading332levels and TODO comments at each spot the author needs to fill333in. Use the templates as a checklist — every TODO needs an334answer before publishing.