How It Works

Self-describing JSON-LD with @view

The Idea

JSON-LD describes what data is. The @view property adds how to display it:

{
  "@type": "schema:Person",
  "@view": "https://jsonos.com/examples/src/panes/person.js",
  "schema:name": "Marie Curie"
}

The data carries its own renderer. No external configuration needed.

One line to render: Include solid-shim and it auto-detects @view.

One-Line Embed

A complete self-rendering page:

<!-- Your data with @view -->
<script type="application/ld+json">
{
  "@type": "schema:Person",
  "@view": "https://jsonos.com/examples/src/panes/person.js",
  "schema:name": "Marie Curie"
}
</script>

<!-- One line - auto-renders when @view is present -->
<script src="https://unpkg.com/solid-shim/dist/mashlib.min.js"></script>

That's it. The mashlib detects @view in your JSON-LD and renders automatically.

What Happens

1

Page loads mashlib.min.js

Brings in $rdf (rdflib), the RDF store, and @view support.

2

Finds JSON-LD with @view

Scans for <script type="application/ld+json"> containing @view.

3

Parses JSON-LD into RDF store

Converts to triples: subject → predicate → object

4

Loads the pane from @view URL

Dynamic import: import(data['@view'])

5

Calls pane.render()

Pane reads from store, returns DOM element, gets inserted into page.

Create Your Own Pane

Panes are ES modules that transform RDF data into DOM elements. Here's everything you need to build one.

Full Pane Template

// my-pane.js - Complete pane structure
export default {
  // Required: unique name
  name: 'myCustomPane',

  // Optional: icon (data URI or URL)
  icon: 'data:image/svg+xml;base64,...',

  // Optional: label function for tab/menu display
  label: function(subject, context) {
    const store = context.session.store;
    const SCHEMA = $rdf.Namespace('http://schema.org/');

    // Return string if this pane handles this type, null otherwise
    const types = store.findTypeURIs(subject);
    if (types[SCHEMA('Person').uri]) {
      return 'Person';
    }
    return null;
  },

  // Required: render function
  render: function(subject, context) {
    const store = context.session.store;
    const dom = context.dom;

    // Build and return a DOM element
    const div = dom.createElement('div');
    div.textContent = 'Hello from my pane!';
    return div;
  }
};

The Context Object

The context parameter gives you everything you need:

PropertyDescription
context.session.storeThe RDF store with your parsed JSON-LD data
context.domThe document object for creating elements
context.sessionSession info (logged-in user, etc.)

Reading Data from the Store

Use $rdf.Namespace to create vocabulary helpers:

const SCHEMA = $rdf.Namespace('http://schema.org/');
const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/');
const RDF = $rdf.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#');

Store API Reference

MethodReturnsUse Case
store.anyValue(subject, predicate)String or nullGet a single text value
store.any(subject, predicate)Node or nullGet a linked resource or literal
store.each(subject, predicate)Array of NodesGet all values (e.g., multiple authors)
store.findTypeURIs(subject)Object of type URIsCheck what types a subject has
node.uriStringGet URI from a NamedNode
node.valueStringGet value from a Literal or URI string

Complete Example: Recipe Pane

// recipe-pane.js
export default {
  name: 'recipePane',

  render: function(subject, context) {
    const store = context.session.store;
    const dom = context.dom;
    const SCHEMA = $rdf.Namespace('http://schema.org/');

    // Read properties
    const name = store.anyValue(subject, SCHEMA('name'));
    const description = store.anyValue(subject, SCHEMA('description'));
    const image = store.any(subject, SCHEMA('image'));
    const prepTime = store.anyValue(subject, SCHEMA('prepTime'));
    const cookTime = store.anyValue(subject, SCHEMA('cookTime'));

    // Get all ingredients (array)
    const ingredients = store.each(subject, SCHEMA('recipeIngredient'));

    // Get all instructions (array)
    const instructions = store.each(subject, SCHEMA('recipeInstructions'));

    // Build the UI
    const div = dom.createElement('div');
    div.style.cssText = `
      font-family: system-ui, sans-serif;
      max-width: 600px;
      padding: 2rem;
    `;

    // Header with image
    if (image) {
      const img = dom.createElement('img');
      img.src = image.uri || image.value;
      img.style.cssText = 'width: 100%; border-radius: 12px; margin-bottom: 1rem;';
      div.appendChild(img);
    }

    // Title
    const h1 = dom.createElement('h1');
    h1.textContent = name || 'Untitled Recipe';
    h1.style.cssText = 'margin: 0 0 0.5rem; color: #1e293b;';
    div.appendChild(h1);

    // Description
    if (description) {
      const p = dom.createElement('p');
      p.textContent = description;
      p.style.cssText = 'color: #64748b; margin-bottom: 1.5rem;';
      div.appendChild(p);
    }

    // Time info
    if (prepTime || cookTime) {
      const timeDiv = dom.createElement('div');
      timeDiv.style.cssText = 'display: flex; gap: 1rem; margin-bottom: 1.5rem;';

      if (prepTime) {
        const prep = dom.createElement('span');
        prep.textContent = '⏱️ Prep: ' + formatDuration(prepTime);
        timeDiv.appendChild(prep);
      }
      if (cookTime) {
        const cook = dom.createElement('span');
        cook.textContent = '🍳 Cook: ' + formatDuration(cookTime);
        timeDiv.appendChild(cook);
      }
      div.appendChild(timeDiv);
    }

    // Ingredients list
    if (ingredients.length > 0) {
      const h2 = dom.createElement('h2');
      h2.textContent = 'Ingredients';
      h2.style.cssText = 'font-size: 1.25rem; margin: 1.5rem 0 0.75rem;';
      div.appendChild(h2);

      const ul = dom.createElement('ul');
      ul.style.cssText = 'padding-left: 1.5rem;';
      ingredients.forEach(ing => {
        const li = dom.createElement('li');
        li.textContent = ing.value;
        ul.appendChild(li);
      });
      div.appendChild(ul);
    }

    // Instructions
    if (instructions.length > 0) {
      const h2 = dom.createElement('h2');
      h2.textContent = 'Instructions';
      h2.style.cssText = 'font-size: 1.25rem; margin: 1.5rem 0 0.75rem;';
      div.appendChild(h2);

      const ol = dom.createElement('ol');
      ol.style.cssText = 'padding-left: 1.5rem;';
      instructions.forEach(step => {
        const li = dom.createElement('li');
        li.textContent = step.value;
        li.style.cssText = 'margin-bottom: 0.5rem;';
        ol.appendChild(li);
      });
      div.appendChild(ol);
    }

    return div;
  }
};

// Helper: Parse ISO duration (PT30M -> "30 min")
function formatDuration(iso) {
  const match = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?/);
  if (!match) return iso;
  const h = match[1] ? match[1] + 'h ' : '';
  const m = match[2] ? match[2] + 'min' : '';
  return h + m || iso;
}

Tips

Pro tip: Look at the existing panes for more patterns and examples.

Try It Live

The editor lets you modify both JSON-LD and pane code, with live preview:

JSON-LD { "@type": "Person", "@view": "...", "name": "Marie" } editable Pane Code export default { render(subject) { // ... editable Preview M Marie Curie Physicist & Chemist View Profile live Open Editor →

20 Schema.org Types

Examples for Person, Article, Event, Recipe, Product, Movie, Book, Review, FAQ, HowTo, Job, Business, Service, Course, Video, Music, Software, and more.

Each example is one HTML file with JSON-LD + one script tag.

View All Examples →

The @view Proposal

This implements W3C JSON-LD Syntax Issue #384.

BenefitWhy it matters
Self-describingData carries its display hint
DecentralizedAnyone can publish views
ProgressiveProcessors that don't support @view ignore it
ExtensibleNew types get views immediately

Links