Self-describing JSON-LD with @view
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.
solid-shim and it auto-detects @view.
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.
Brings in $rdf (rdflib), the RDF store, and @view support.
Scans for <script type="application/ld+json"> containing @view.
Converts to triples: subject → predicate → object
Dynamic import: import(data['@view'])
Pane reads from store, returns DOM element, gets inserted into page.
Panes are ES modules that transform RDF data into DOM elements. Here's everything you need to build one.
// 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 parameter gives you everything you need:
| Property | Description |
|---|---|
context.session.store | The RDF store with your parsed JSON-LD data |
context.dom | The document object for creating elements |
context.session | Session info (logged-in user, etc.) |
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#');
| Method | Returns | Use Case |
|---|---|---|
store.anyValue(subject, predicate) | String or null | Get a single text value |
store.any(subject, predicate) | Node or null | Get a linked resource or literal |
store.each(subject, predicate) | Array of Nodes | Get all values (e.g., multiple authors) |
store.findTypeURIs(subject) | Object of type URIs | Check what types a subject has |
node.uri | String | Get URI from a NamedNode |
node.value | String | Get value from a Literal or URI string |
// 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;
}
dom.createElement instead of document.createElement for compatibilityelement.style.cssText for inline styles, or create a <style> element.uri for links, .value for text contentThe editor lets you modify both JSON-LD and pane code, with live preview:
Open Editor →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 →This implements W3C JSON-LD Syntax Issue #384.
| Benefit | Why it matters |
|---|---|
| Self-describing | Data carries its display hint |
| Decentralized | Anyone can publish views |
| Progressive | Processors that don't support @view ignore it |
| Extensible | New types get views immediately |