blog-emit
Emit a canonical blog draft (blogs/<slug>/index.md) to a downstream target. Runs the deterministic emitter core (resolves file= snippets, renders journeys/callouts/diagrams per the target), then deliv
SKILL.md.claude/skills/blog-emit/SKILL.md
GitHubblog-emit: emit a draft to a downstream target
A blog index.md is canonical, portable CommonMark — richness is a property of
the renderer, never the source (see
blogs/CONVENTIONS.md §5). This skill is the
delivery half of the emitter: it runs the deterministic core to produce a
target-shaped render, then delivers it to that target and records the delivery so a
re-emit updates in place rather than duplicating.
The design is a target-agnostic spine + one per-target runbook:
- Deterministic core —
emit/, a committed Node package. It does everything scriptable: resolvefile=snippets and render each rich construct (::::journey,:::callout,likec4blocks, code captions) the way the chosen target needs — upgraded to site components (unitycatalog, delta). It dispatches on--target; each target is a module inemit/targets/. Do not reimplement any of this in the agent — run the script. - Delivery adapter (this skill) — the part that needs judgment, MCP tools, or
filesystem writes a Node script can't/shouldn't do. The **spine below (steps 1–2,
- is identical for every target**; the target-specific delivery mechanics live in
a single sidecar runbook,
references/<target>-target.md, which you load in step 0 and follow in step 3.
- is identical for every target**; the target-specific delivery mechanics live in
a single sidecar runbook,
The source of truth for what a target's render contains is
blogs/CONVENTIONS.md §5,
emit/README.md, and the target module
(emit/targets/<target>.mjs) — read them if a construct's rendering is unclear;
don't restate them here.
Steps
0. Resolve the targets and load their runbooks
The draft's target: is one site or a list of sites to cross-post to; the first is
the primary. Take the targets from --target if given, else from that list, and
confirm the list with the user before delivering (each delivers to an external
site). Valid targets are the delivery modules in emit/targets/ (unitycatalog,
delta); a listed target without one (openlakehouse) can't be emitted.
Work through the targets one at a time, primary first, running steps 1–4 for each. For each target, read the matching runbook and follow it for step 3 — only that one (this is the progressive-disclosure seam; don't load the others):
| target | runbook | delivers to |
|---|---|---|
unitycatalog |
references/unitycatalog-target.md |
../unitycatalog-website (Astro MDX) |
delta |
references/delta-target.md |
../website — delta-io/website (Astro MDX) |
1. Run the deterministic core
From the repo root:
cd emit && bun install # first run only (installs unified/remark deps)
bun emit.mjs --slug <slug> --target <target>
(--target a,b or no --target at all emits several targets in one run; render the
cross-posts again after the primary's delivery is recorded, see below.)
This writes, under blogs/<slug>/dist/<target>/:
- the render —
index.mdx(unitycatalog, delta); assets.json—{ slug, target, primary, canonical, title, existing, images: [...] };likec4-webcomponent.mjs— the interactive-diagram bundle (rich targets, only when the post has alikec4block).
It also prints whether delivery should CREATE or UPDATE. The core refuses a
private post and warns when the post isn't ready: rendering a draft to check
the target is fine, but deliver only a ready post (merged to main is the
release, ADR-0002), unless the user explicitly asks for a draft delivery.
2. Read the render + manifest
- the render file — deliver this, never the raw
index.md(itsfile=fences are empty and its:::/::::markers would leak). dist/<target>/assets.json:existingis the create-vs-update decision, sourced from the post's.emitted.jsonsidecar under the<target>key:null→ CREATE; an object → UPDATE that delivery in place. Its shape is{ post_dir, updated }— the runbook you loaded describes it.canonicalis set on a cross-post: the primary's delivered URL, which the render's frontmatter carries so the site points search engines at the primary. It isnullon the primary. On a cross-post it isnullonly while the primary has no delivery recorded, and the core printscanonical: pending: deliver the primary first (through step 4), then re-run step 1 for the cross-post. Never deliver a cross-post without its canonical.- each
images[]entry is{ filename, localPath, altText };localPathis the real file on disk to upload/copy. LikeC4 views aren't images: the render's<LikeC4View>draws them from the web-component bundle.
3. Deliver — follow the runbook from step 0
Do the CREATE-or-UPDATE delivery exactly as the target's runbook specifies. That is the only target-specific part of this skill.
4. Record the delivery + report
The delivery mapping lives in a committed sidecar dotfile next to the draft,
blogs/<slug>/.emitted.json, keyed by target — self-contained in the post's
folder (it travels with the post, no global registry) while index.md stays pure.
- On CREATE, read the sidecar (treat missing as
{}), set the<target>key to the block the runbook specifies, leaving any other target keys untouched, write it back, and commit it (tracked, not gitignored). This is what makes the next emit an UPDATE, not a duplicate. - On UPDATE, refresh that target's
updatedto today's date. - Report the result (post dir + preview URL) to the user, then continue with the next target. A cross-post's render needs the primary's sidecar entry, so record the primary before you emit the cross-posts.
What this skill must NOT do (all targets)
- Never edit
index.md— the draft is canonical. The only per-post state this skill writes isblogs/<slug>/.emitted.json(the delivery mapping); it never touches any other file underblogs/<slug>/besides the generateddist/(which the core owns). - Never feed raw
index.mdto the target — always deliver the core'sdist/<target>/render (see step 2). - Never reimplement the core's transforms — if the render looks wrong, fix the
core (
emit/, a plugin, or the target module), not the agent. - Never publish/share on the user's behalf. This is the shared invariant behind every runbook's "do NOT push": never push or open a PR on the sibling repo without explicit instruction. Delivery stops at a local artifact the user then chooses to release.
Adding a target
A new target is: (1) a new emit/targets/<name>.mjs (reusing the whole
resolve → render core — declare its constructs, frontmatter, and flags, per the
module docs in emit/targets/unitycatalog.mjs); (2) a new references/<name>-target.md
runbook; (3) a row in the step-0 table above. The core and the .emitted.json ledger
are already target-keyed, so idempotency carries over for free. The delta target was
added exactly this way — its runbook doubles as a worked example, including the
one-time sibling-repo site wiring a rich MDX target needs.