Files
webstudio/packages/content-engine
Oleg Isonen 2df130171f feat: Resolve asset references as structured metadata (#6184)
## Summary

- add exact `$ref` asset values that resolve to Asset Manager metadata
- keep plain local asset paths returning URL strings for backward
compatibility
- derive reference metadata from the canonical asset object so newly
added asset fields flow through automatically
- let Assets query output fields select only the metadata returned,
including nested arrays
- carry the same resolved shape through preview, generated preview,
published runtime, and static export
- document the syntax and accessible image binding in Content Engine and
MCP docs

## Design

The source document declares only the relationship:

```yaml
featureImage:
  $ref: ./assets/hero.png
```

The Assets query controls projection with fields such as
`properties.featureImage.src` and `properties.featureImage.description`.
Selecting `properties.featureImage` returns the canonical Asset Manager
asset metadata together with the resolved `src`. The Content Engine
runtime contract accepts the canonical asset object instead of
maintaining a second metadata field list; `url` becomes `src`, and
internal `contentRef` is not exposed.

Plain strings retain their existing URL-string result. Existing document
`$ref` behavior is preserved by resolving structured asset metadata only
when the target is a known non-document asset.

## Verification

- [x] Content Engine full test suite: 298 tests
- [x] SDK full test suite: 517 tests
- [x] Asset repository tests: 39 tests
- [x] CLI prebuild tests: 58 tests
- [x] Content Engine, SDK, Asset Uploader, CLI, and Project Build
typechecks
- [x] Repository lint
- [x] Content Engine and MCP documentation generation and tests
- [x] Generated API check
- [x] Local fixture build and generated fixture update
- [x] New behavior tests demonstrated fail before/pass after the
implementation
- [ ] Complete fixture sync (development API currently rejects this
branch's newer CLI protocol; local fixture build passes)
- [ ] Agent evaluations (not run; require explicit approval)

## Checklist

- [x] Local structured references return field-selected metadata
- [x] Newly added canonical asset fields flow through without a Content
Engine whitelist change
- [x] Plain-path queries remain backward compatible
- [x] Nested object and array projection
- [x] External URL strings remain usable
- [x] Missing or null descriptions allow `??` fallback to the title
- [x] Expanded metadata counts toward query result-size limits
- [x] Documentation impact reviewed and Content Engine and MCP docs
updated
- [ ] Missing asset targets return a non-failing unresolved value and
dedicated diagnostic
- [ ] Apply the resource and Image binding changes to the separate
Webstudio blog project

## Known limitation

A `$ref` whose asset target no longer exists is not yet converted into
an unresolved asset value with a dedicated diagnostic. This PR
intentionally does not claim that acceptance criterion yet.

Ref #6179
2026-08-19 01:11:41 +02:00
..

Content engine

The content engine compiles file-based Assets into a portable query database. JSON and Markdown documents can reference other Assets without embedding their complete source in that database.

Document references

A reference is an object with exactly one $ref property whose value is a URI reference:

{
  "author": { "$ref": "./authors/ada.json" }
}

References can appear at any depth in a JSON document. In Markdown, references can appear at any depth in YAML frontmatter:

---
title: Building with Markdown
author:
  $ref: ./authors/ada.json
---

Article body.

An object with additional properties is ordinary content, not a reference. The $ref spelling is familiar from JSON Schema, but this syntax does not implement JSON Schema resolution, identifiers, anchors, or schema evaluation.

Reference paths use standard URI-reference resolution relative to the source document. The fragment selects which representation of the target to insert:

Reference Selected target value
./author.json Complete JSON value
./author.json#/profile/name Value at the JSON Pointer /profile/name
./article.md Original Markdown source, including frontmatter
./article.md#body Markdown body without frontmatter
./article.md#frontmatter Markdown frontmatter as an object

JSON Pointer escaping follows RFC 6901. For example, #/a~1b selects the property named a/b. JSON Pointer fragments apply only to JSON documents; #body and #frontmatter apply only to Markdown documents.

Authored URLs identify Assets during compilation. They do not permit arbitrary runtime network requests. Every target must match a document in the compiled Asset catalog.

Resolution behavior

At build time, the content engine:

  1. Parses bounded JSON sources and Markdown frontmatter.
  2. Resolves each static URI reference to a document identity and revision.
  3. Rejects missing targets, duplicate identities, revision conflicts, and dependency cycles.
  4. Stores the validated dependency graph with searchable metadata while source payloads remain in Asset storage.

At runtime, the server compares the query's filter, sort, and output property paths with the graph's reference locations. If a reference can affect the query, the server resolves the relevant source documents before executing the query; otherwise it uses the normal query path without loading graph sources. Non-reference filters first narrow the candidate sources. The server follows only root references that intersect the query, preserves unselected sibling markers, and computes the selected references' complete downstream dependency closure. It loads independent documents concurrently, loads shared storage content once, validates storage identities, revisions, formats, and payload limits, and assembles dependencies before their consumers. A resolved value replaces its exact { "$ref": "..." } marker.

Filtering, sorting, pagination, and projection therefore operate on resolved values when their property paths intersect a reference. Only properties selected by the Assets query are returned. Resolution cannot expose an unselected property merely because it exists in a fetched document. markdown-body-ref content selection is also graph-backed. Compilation retains the Markdown document identity instead of its source bytes, and runtime hydration fetches only the documents selected after filtering, sorting, and pagination. The resolved query result continues to expose the body at content.text. Whole-document and range content modes retain their embedded-content behavior.

Document source caches are keyed by storage reference, revision, and format, so content from one identity cannot be combined silently with different graph metadata. Published runtime assets must also point at the graph node's exact storage reference before the source is fetched. Resolution is bounded by document-count, per-document byte, aggregate-byte, and concurrency limits. A missing document, invalid representation, stale identity, limit violation, or cancellation fails the query instead of returning a partially assembled graph.

Assets without a document graph keep the existing embedded-content behavior and do not require migration.