BUG-016
ID:BUG-016Status:fixed

A track inside a playlist is not one of its tracks

/runes/media/track tells authors that "tracks can be used independently or inside a playlist rune". The composition does not work. A {% track %} inside a {% playlist %} is not a track of that playlist in the rendered HTML or in the structured data — it renders as a bare <li> in the prose body and publishes a detached top-level entity.

Found while settling SPEC-130's open question about the two MusicRecording emitters. The question turned out to rest on this composition working; it does not.

Severity:majorMilestone:v0.35.0
changeset-release/main View source

Criteria completion

Criteria completion: 0 of 6 (0%) checked; tracking started on Sep 15, no incremental history yet0%25%50%75%100%Sep 15Oct 11

Tracking started Sep 15 — check back for trends.

Branches 3
History 4
  1. 83f7b48
    statusconfirmed→fixed
    by github-actions[bot]
  2. 6f86f8c
    Created (confirmed)by github-actions[bot]
  3. 8f934df
    Content editedby Claude
    docs(plan): resolve BUG-016 toward making the composition work
  4. 989be44
    Content editedby Claude
    docs(plan): close SPEC-130's open question as D9; file BUG-016

Actual

{% playlist type="podcast" artist="Acme" %}
# The Sunday Show

- **Episode One** *Acme* (30:00)

{% track type="episode" artist="Acme" duration="42:30" %}
## Episode Two
{% /track %}
{% /playlist %}

Structure — the track lands in the body zone, not the track list:

playlist → section → div[data-name=body] → li[data-rune=track]

The <ol data-name="tracks"> contains one item (the markdown-list episode). The {% track %} is not in it. So the page carries an <li> that is not inside any list, which is invalid HTML.

Structured data — two unrelated entities instead of one:

[
  { "@type": "MusicPlaylist", "name": "The Sunday Show", "byArtist": "Acme",
    "track": { "@type": "MusicRecording", "name": "Episode One", … } },
  { "@type": "MusicRecording", "name": "Episode Two" }
]

The second entity is related to nothing. collectJsonLd nests a typed child only when the same node carries both typeof and property (packages/runes/src/seo.ts:109); playlist stamps property on the items it builds itself and never sees the {% track %}, so the track floats up.

Expected

Decided: the composition works. track is useful standalone and useful inside a playlist when the list format is too limited and the author wants full control — that is the escape hatch the docs were describing, and it is worth having. So playlist's content model accepts track tags into its tracks field, they render inside the <ol>, and they nest in the playlist's structured data like any other item.

The bar is equivalence: a nested track with no explicit type produces the same schema as the same content written as a list item. The fuller form may carry more (url, position); it may not carry less.

Withdrawing the composition was the cheaper option and was considered. It was rejected because the feature is genuinely wanted — the item model cannot express body content, cue points with prose, or per-track links, and an author who needs those has nowhere else to go.

Fixed by WORK-572.

Why this is major rather than minor

Unlike BUG-013, this one is not merely imprecise. It emits invalid HTML (<li> outside a list), publishes a detached entity that asserts a standalone recording exists where the author described a playlist item, and it is actively recommended by the documentation — so an author following the docs produces all three.

Root cause

playlist's content model matches its track list as { name: 'tracks', match: 'list' } and builds items exclusively from itemModel-extracted data (plugins/media/src/tags/playlist.ts:121-155). There is no field matching a track tag, so a {% track %} falls through to { name: 'body', match: 'any', greedy: true }.

Nothing in playlist references the track rune at all. The composition was documented but never built.

Steps to reproduce

Render the markdoc above through extractSeo and inspect the tree. Measured directly against the built packages, not inferred from the source.

Acceptance Criteria

  • {% track %} children render inside the <ol data-name="tracks"> and nest under the playlist's track property, with no detached entity
  • No <li> is emitted outside a list
  • A nested track with no explicit type produces the same JSON-LD as the same content written as a list item, asserted directly
  • /runes/media/track's claim that the composition works is true, with a worked example
  • A test covers a {% track %} inside a {% playlist %}, asserting the JSON-LD has no detached entity
  • BUG-013's "two emitters" reasoning is corrected to match this resolution

Approach

See WORK-572 for the mechanics. The two facts that shape it:

  • The parent supplies the collection property; the child may supply its own type. Nesting requires both typeof and property on the same node and only the parent knows the relationship, so the parent already has to touch every nested child — type inheritance then costs nothing extra. The child declares nothing about its parent, exactly as SPEC-130 D9 has it.
  • track's ?? 'song' default currently hides the absence it would need to detect, so the inheritance cannot fire until that moves to a fallback row.

This lands before WORK-569. Structure first, schema second: a children: mapping written while tag-built children are still not children covers half the population and gets revised the moment this ships.

Note this is not blocked on SPEC-130. The invalid <li> and the detached entity are wrong today regardless of how the schema channel is expressed.

References

  • SPEC-130 — D9, which this finding produced
  • WORK-572 — the fix
  • BUG-013 — the sibling defect; its "two emitters" section assumed this composition worked
  • WORK-569 — takes over the stamping once the table exists
  • plugins/media/src/tags/playlist.ts:121 — items built from itemModel only
  • packages/runes/src/seo.ts:109 — the nesting rule the detached entity falls out of
  • site/content/runes/media/track.md:16 — the claim

Resolution

Completed: 2026-09-16

Fixed by WORK-572 on claude/v0.35-parallel-feasibility-eia5le, in the "make the composition work" direction rather than by withdrawing it.

All three symptoms are covered by tests in plugins/media/test/playlist-track-children.test.ts:

  • The stray <li> — nested tracks now render inside <ol data-name="tracks"> rather than falling through to the greedy body field.
  • The detached entity — schema: { track } stamps property="track" onto the merged, ordered set, so nothing floats up as its own top-level entity. collectJsonLd nests a typed node only when it carries both typeof and property, and only the parent can supply the property.
  • The docs claim — /runes/media/track now has a worked mixed-form example, and the claim it makes is true.

The bar was equivalence, and it is asserted directly: the same track written as a list item and as a {% track %} child produces the same JSON-LD for the shared fields. Getting there required two fixes beyond the composition itself — the resolver could not handle a greedy itemModel field at all, and track had been dropping its own artist and duration because the metas were never pushed into children. Both are recorded in WORK-572's resolution.