v0.38.0
Released October 7, 2026 · All releases
Rune transform consolidation. Rune transforms lose code that did nothing: metas emitted only to be filtered back out, an unused schema.org channel, and guards that guarded nothing, with rendered output unchanged. Two declarations now reach the author: a mixed list-and-tag field no longer drops the tags written in it, and refrakt reference shows each field's authoring grammar.
Background: the milestone · A mixed list|tag:x field with emitTag silently drops the authored tags · The reference drops the field-level authoring grammar
Changes
- New rune-authoring helpers in
@refrakt-md/runes(WORK-602, WORK-603) renderNodes(value, config)returns aRenderableNodeCursorover the transformed content. It replacesnew RenderableNodeCursor(Markdoc.transform(asNodes(x), config) as RenderableTreeNode[]).bodyOnly()is the content model of a rune whose children are all body. It returns a fresh object per call.fieldMetas(attrs, config, spec)builds a rune's property metas from one declaration. A bare string is the default for the attribute of the same name.{ from: ['attrs.x', 'file.x'], default }takes the first non-empty source. The spec is plain data and round-trips through JSON. Any root other thanattrsorfileis rejected.groupByHeading(nodes, { initial, heading, item, other? })is the shared walk where a heading sets the running group and list items become entries.- The built-in runes now use these helpers: 78 cursor constructions, 49 content models, 12 property maps (including the five plan runes'
created/modifiedfallback to file dates) and six heading-grouped list parsers. Neither helper is mandatory. Rendered output is unchanged. - Breaking: remove the imperative schema.org channel from
createComponentRenderable(WORK-599) TransformResult.schema,TransformResult.typeofandInlineTransformResult.schemaOrgTypeare gone, along with the unusedschemaOrgTypefields onAccordionPropsandAccordionItemProps.- SPEC-130 moved every rune to the declarative table on
createContentModelSchema({ schema }). The imperative fields kept type-checking but were no longer what the appliers and the structured-data pipeline are built around, and no rune in this repository passed them. A surface that still compiles but no longer does what its docs say is worse than none. - Migrating a third-party plugin: move the type and property mappings into a schema table on the rune's
createContentModelSchemacall. See "Declaring schema.org output" in the plugin authoring guide. - Rendered output is unchanged for every built-in rune.
- Keep authored tags in a mixed
emitTagfield, and show the list-item grammar inrefrakt reference(WORK-604, WORK-606) - Fixes silent content loss. A field declared
match: 'list|tag:x'withemitTagconverted the list items and dropped every tag the author wrote (BUG-030). Authored tags now keep their place, so the field resolves to one document-ordered array of tags. Alist-only field behaves as before. No built-in rune declared this combination, so rendered output is unchanged. refrakt referencenow shows how a list item is read (BUG-031). A field with anitemModellists each item field: bold, italic, a link and itshref, text matching a pattern, the remaining text, a nested list. Where a field has them, it also shows thetemplatesnippet ("Written as") and the tag each item becomes (emitTag). A pattern with a non-ASCII character names it, so playlist's date field now says its—is U+2014 and a typed-will not match.SerializedContentFieldgains anitemModelprojection: regex source and flags as strings,'remainder'as a literal.- Stop emitting property-mapped meta tags into rune
children. - Every rune that mapped a meta under
propertiesand also listed it inchildrenhad it filtered straight back out bycreateComponentRenderable; the value already lives in thedata-rune-fieldsbag. The emission is removed from 62 tag files and the auto-breadcrumb builder. Rendered output is unchanged: structure contracts, the structured-data baseline, andrefrakt inspectacross every rune and variant are identical before and after. - The one exception is content that is already invalid. A
blogwith nofolder(a required attribute) no longer renders an empty<meta data-field="folder">. - Rune configs are now plain data, and
plan migrate idskeeps the published claimant (WORK-608, WORK-607) - Breaking:
styles[…].transformtakes a name, not a function (WORK-608). Use one of the named transforms already used by meta fields and structure entries. That vocabulary now has three more entries: align: an alignment keyword becomes a CSSalign-*value (wasresolveValign).fr:"2 1"becomes2fr 1fr(wasratioToFr).gap: a gap preset becomes a spacing token (wasresolveGap).- The type is exported as
NamedTransform. Replacetransform: resolveValignwithtransform: 'align'. The helpers stay exported. After this changepostTransformis the only function aRuneConfigcarries, so a rune config can cross a JSON boundary. A test checks every core and plugin rune for this. Structure contracts now record these transform names; before,JSON.stringifysilently dropped them because they were functions. Rendered output is unchanged. plan migrate ids --against <ref>(WORK-607, BUG-026). When two files claim one ID, the one already on the base ref keeps it, and the branch-local claimant is renumbered. Before this, the claimant that moved was picked by sorting filenames, which could renumber the entity already referenced from merged commits and published CHANGELOGs. Without--against, a collision is now refused rather than resolved by filename. Every renumber and refusal names the claimant that kept the ID and why.plan validate --againstnow suggests the matchingmigrate ids --againstcommand.- Simplify rune transforms: optional slots and shared text helpers (WORK-600, WORK-601)
propertiesandrefsoncreateComponentRenderablenow acceptnullas well asundefined; both are skipped. The 52...(x ? { k: x } : {})guards inside slot literals are now plaink: x.extractText(node)(concatenated text of an AST node) is exported from@refrakt-md/runesand replaces six identical local copies. Three plugin-local text helpers on rendered trees now use the exportedtextContent.- Rendered output is unchanged.