From b76f238b2d11b1b3ad59c7d6cf698f45a6ccd00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Mon, 17 Aug 2026 23:30:59 +0200 Subject: [PATCH 1/3] Add app-building tutorial section to docs Ten-stage tutorial that rebuilds the Northwind Traders demo from an empty dataspace to a published app, with UI and CLI tracks per stage. All commands are lifted from the demo's install scripts; screenshot slots are marked with XML comments for later automation. Co-Authored-By: Claude Fable 5 --- docs/tutorial.ttl | 37 ++++++++++++++ docs/tutorial/app-as-repository.ttl | 51 ++++++++++++++++++++ docs/tutorial/beyond-low-code.ttl | 26 ++++++++++ docs/tutorial/composition.ttl | 39 +++++++++++++++ docs/tutorial/data.ttl | 75 +++++++++++++++++++++++++++++ docs/tutorial/hello-dataspace.ttl | 69 ++++++++++++++++++++++++++ docs/tutorial/insight.ttl | 73 ++++++++++++++++++++++++++++ docs/tutorial/media.ttl | 45 +++++++++++++++++ docs/tutorial/model.ttl | 73 ++++++++++++++++++++++++++++ docs/tutorial/publish.ttl | 33 +++++++++++++ docs/tutorial/structure.ttl | 54 +++++++++++++++++++++ 11 files changed, 575 insertions(+) create mode 100644 docs/tutorial.ttl create mode 100644 docs/tutorial/app-as-repository.ttl create mode 100644 docs/tutorial/beyond-low-code.ttl create mode 100644 docs/tutorial/composition.ttl create mode 100644 docs/tutorial/data.ttl create mode 100644 docs/tutorial/hello-dataspace.ttl create mode 100644 docs/tutorial/insight.ttl create mode 100644 docs/tutorial/media.ttl create mode 100644 docs/tutorial/model.ttl create mode 100644 docs/tutorial/publish.ttl create mode 100644 docs/tutorial/structure.ttl diff --git a/docs/tutorial.ttl b/docs/tutorial.ttl new file mode 100644 index 0000000..77c0b8e --- /dev/null +++ b/docs/tutorial.ttl @@ -0,0 +1,37 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Container ; + dct:title "Tutorial" ; + dct:description "Build your first Knowledge Graph application, from an empty dataspace to a published app" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Build your first Knowledge Graph application, from an empty dataspace to a published app

+

This tutorial rebuilds the Northwind Traders demo application: the iconic sample + database, reborn as an RDF Knowledge Graph. Every stage ends with a result you can see in the browser. Most steps are shown two ways: point-and-click in the user interface, and as + CLI commands that you can script, version and replay.

+

Before you start you need a running LinkedDataHub instance and the owner's WebID certificate. The Get started section covers both. The command line + examples assume the bin/ scripts are on your PATH and use these shell variables throughout:

+
base="https://localhost:4443/"
+cert_pem_file="ssl/owner/cert.pem"
+cert_password="..."
+
+

Stages

+
    +
  1. Hello dataspace — give your app an identity and a landing page
  2. +
  3. Structure — lay out the document hierarchy
  4. +
  5. Model — define the vocabulary that describes your domain
  6. +
  7. Data — import CSV data using SPARQL mappings
  8. +
  9. Media — upload files and link them to resources
  10. +
  11. Insight — turn SPARQL queries into charts and views
  12. +
  13. Composition — assemble the landing page from content blocks
  14. +
  15. Publish — grant public access and ship it
  16. +
  17. App as a repository — make the app reproducible
  18. +
  19. Beyond low-code — where to go when data alone is not enough
  20. +
+
+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/app-as-repository.ttl b/docs/tutorial/app-as-repository.ttl new file mode 100644 index 0000000..fe2fd02 --- /dev/null +++ b/docs/tutorial/app-as-repository.ttl @@ -0,0 +1,51 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "App as a repository" ; + dct:description "Make the app reproducible: a git repository that installs into any instance" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Make the app reproducible: a git repository that installs into any instance

+

Every stage so far produced files — document descriptions, mapping queries, CSV data, media. Kept in a repository with a small install script, they are the application: versionable, + diffable, reviewable, and replayable into any LinkedDataHub instance. This is the layout the Northwind Traders repository uses:

+
northwind-traders/
+├── root.ttl              # one .ttl per document...
+├── categories.ttl
+├── categories/           # ...and a folder for the files it imports
+│   ├── categories.csv
+│   ├── categories.rq
+│   └── *.gif
+├── admin/model/
+│   ├── ns.ttl            # the namespace ontology
+│   ├── patch-ontology.ru # resets it before re-import
+│   └── import-ns.sh
+├── imports.csv           # the CSV import manifest
+├── install.sh            # replays everything against a base URI
+├── update-folder.sh
+├── .ldhignore            # files that are not documents
+└── Makefile
+
+

The convention

+

The document URL is derived from the file path: categories.ttl installs to ${base}categories/, root.ttl to ${base} itself. + update-folder.sh walks the tree applying that rule — resolving each file's relative URIs against its document URL and PUTting the result — and skips anything + matched by .ldhignore.

+
+
+

Idempotency

+

Re-running the install converges instead of duplicating: PUT replaces each document's description outright, and the one place that appends rather than replaces — the + namespace ontology — is first reset by patch-ontology.ru, a SPARQL update that clears the previously imported terms.

+
+
+

Installing

+
make install
+

prompts for the base URI, certificate and password, then runs install.sh: make public, import the ontology, replay the documents, upload the files, run the CSV imports. The + entire tutorial, replayed in one command — against localhost today, against production tomorrow.

+
+

Next: Beyond low-code

+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/beyond-low-code.ttl b/docs/tutorial/beyond-low-code.ttl new file mode 100644 index 0000000..130daf5 --- /dev/null +++ b/docs/tutorial/beyond-low-code.ttl @@ -0,0 +1,26 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Beyond low-code" ; + dct:description "Where to go when data alone is not enough" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Where to go when data alone is not enough

+

Everything in this tutorial was done with data: documents, queries and authorizations, applied over HTTP. That is the ceiling of the low-code approach — and there is a door in it. Each layer + of LinkedDataHub can be extended without forking it:

+
    +
  • Layout — write an XSLT stylesheet that imports the system one and overrides only the templates you need. Start with the + Change layout guide and the stylesheets reference.
  • +
  • Docker and Java — mount files into the stock image, build your own image on top of it, or add JAX-RS endpoints via a WAR overlay. The + Build apps guide covers each layer with working build files.
  • +
  • More examples — the LinkedDataHub-Apps repository holds the complete Northwind Traders + source alongside further demo apps and installable packages.
  • +
+

If you get stuck or build something worth showing, open a thread on GitHub Discussions.

+

Back to the Tutorial overview

+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/composition.ttl b/docs/tutorial/composition.ttl new file mode 100644 index 0000000..66145df --- /dev/null +++ b/docs/tutorial/composition.ttl @@ -0,0 +1,39 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Composition" ; + dct:description "Assemble the landing page from content blocks" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Assemble the landing page from content blocks

+

Everything the app can show is now in place — text, data, charts, views. The landing page is their composition: an ordered sequence of + content blocks on the root document. Two block types exist: ldh:XHTML holds authored markup, and + ldh:Object embeds any other resource — a chart, a view, a document. Anything that is not markup gets wrapped in an object block.

+

The demo's finished root document interleaves them into an executive dashboard:

+
<> a def:Root ;
+    dct:title "Northwind Traders" ;
+    rdf:_1 <#page-header> ;          # XHTML   — title and pitch
+    rdf:_2 <#overview-intro> ;       # XHTML   — dashboard intro
+    rdf:_3 <#sales-trend-block> ;    # Object  — monthly sales line chart
+    rdf:_4 <#revenue-by-country-block> ; # Object — geo chart
+    rdf:_5 <#top-selling-products> ; # Object  — product ranking
+    rdf:_6 <#top-manager-header> ;   # XHTML   — section heading
+    rdf:_7 <#top-manager> ;          # Object  — employee of the quarter
+    rdf:_8 <#navigation-prompt> ;    # XHTML   — where to go next
+    rdf:_9 <#select-children> .      # Object  — children view
+

The narrative rhythm — prose, then evidence, then prose — is an editorial choice, and it is made entirely in data: reordering the page is renumbering rdf:_N. In the browser the + same composition is drag-and-drop in edit mode; from the command line it is the root.ttl from the first stage grown to its final form, applied + with the same PUT.

+
+

What you now see

+

The homepage is the finished application: branded header, live charts over the imported graph, and navigation into the containers — every pixel of it a rendering of one document's + description.

+ +
+

Next: Publish

+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/data.ttl b/docs/tutorial/data.ttl new file mode 100644 index 0000000..2e2fae5 --- /dev/null +++ b/docs/tutorial/data.ttl @@ -0,0 +1,75 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Data" ; + dct:description "Import CSV data using SPARQL mappings" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Import CSV data using SPARQL mappings

+

Northwind's data ships as plain CSV files. LinkedDataHub imports CSV by running a SPARQL CONSTRUCT mapping over each + row: the row's cells are bound as properties of a row resource, and the query builds the RDF you want from them. The mapping for categories, categories.rq:

+
PREFIX foaf:       <http://xmlns.com/foaf/0.1/>
+PREFIX dct:        <http://purl.org/dc/terms/>
+PREFIX schema:     <https://schema.org/>
+
+CONSTRUCT
+{
+    GRAPH ?graph
+    {
+        ?graph dct:title ?categoryName ;
+            foaf:primaryTopic ?category .
+
+        ?category a schema:ProductGroup ;
+            dct:title ?categoryName ;
+            schema:name ?categoryName ;
+            schema:identifier ?categoryID ;
+            schema:description ?description .
+    }
+}
+WHERE
+{
+    ?category_row <#categoryID> ?categoryID ;
+        <#description> ?description ;
+        <#categoryName> ?categoryName .
+
+    BIND(uri(concat(str($base), "categories/")) AS ?container)
+    BIND(uri(concat(str(?container), encode_for_uri(?categoryID), "/")) AS ?graph)
+    BIND(uri(concat(str(?graph), "#this")) AS ?category)
+}
+

The three BINDs carry the whole document model of the import:

+
    +
  • ?container — the target container from the Structure stage, resolved against $base
  • +
  • ?graph — one document (named graph) per row, minted inside the container from the row's identifier
  • +
  • ?category — the document's topic: the document is about the category, so the category is a #this fragment of it
  • +
+
+

In the browser

+

Create the import from the UI — upload the CSV file, pick the mapping query and the target container — following the Import data + guide.

+
+
+

From the command line

+

Each import pairs a mapping query with a CSV file and a target container. The demo lists them in a manifest, imports.csv:

+
query_filename,csv_filename,target,title
+categories/categories.rq,categories/categories.csv,categories/,Categories
+products/products.rq,products/products.csv,products/,Products
+orders/orders.rq,orders/orders.csv,orders/,Orders
+...
+

and replays the manifest with a small script built on the CLI's create-csv-import.sh:

+
./import-csv.sh "$base" "$cert_pem_file" "$cert_password" "$PWD/imports.csv"
+

Imports run asynchronously — each one is itself a document that records its status.

+
+
+

What you now see

+

The Categories container lists eight category documents. Open one: the category renders with its name, description and identifier, typed as a product group — with the labels coming from + the Model stage. Repeat the pattern for the remaining entities and the Knowledge Graph fills in, orders linking to products, products to suppliers and + categories.

+ +
+

Next: Media

+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/hello-dataspace.ttl b/docs/tutorial/hello-dataspace.ttl new file mode 100644 index 0000000..677e1b6 --- /dev/null +++ b/docs/tutorial/hello-dataspace.ttl @@ -0,0 +1,69 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Hello dataspace" ; + dct:description "Give your app an identity: a title, a description and a landing page" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Give your app an identity: a title, a description and a landing page

+

In LinkedDataHub everything is a document, and every document is a named graph whose name is the document's URL. Your + dataspace starts with a single document: the root document at the base URI. It doubles as the app's homepage, so making the app yours starts with describing this one + document.

+

In this stage you give the root document a title, a description and a first content block — and see the result render as your + homepage.

+
+

In the browser

+

Open the base URL (e.g. https://localhost:4443/) and authenticate with the owner's WebID certificate. Switch the root document into edit mode, change the title to + Northwind Traders, fill in the description, and add an XHTML content block with a short welcome text. The Edit + content guide walks through the block editor in detail.

+ +
+
+

From the command line

+

The same change, expressed as data. Describe the root document in a root.ttl file:

+
@prefix def:    <https://w3id.org/atomgraph/linkeddatahub/default#> .
+@prefix ldh:    <https://w3id.org/atomgraph/linkeddatahub#> .
+@prefix rdf:    <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+@prefix dct:    <http://purl.org/dc/terms/> .
+
+<> a def:Root ;
+    dct:title "Northwind Traders" ;
+    dct:description "Knowledge Graph representation of the Northwind Traders sample database" ;
+    rdf:_1 <#intro> .
+
+<#intro> a ldh:XHTML ;
+    rdf:value '''<div xmlns="http://www.w3.org/1999/xhtml">
+        <div class="page-header">
+            <h2>Northwind Traders</h2>
+            <p class="lead">The iconic sample database converted to an RDF Knowledge Graph</p>
+        </div>
+    </div>'''^^rdf:XMLLiteral .
+

Then replace the root document's description with it using PUT. The turtle command resolves the relative URIs in the file against the document URL, so the same + file installs against any base URI:

+
cat root.ttl | turtle --base="$base" | put.sh \\
+    -f "$cert_pem_file" \\
+    -p "$cert_password" \\
+    -t "application/n-triples" \\
+    "$base"
+
+
+

What you now see

+

Reload the base URL: the homepage now carries your title and welcome text. You have not touched a template, a controller or a database schema — you replaced the description of one + document, and the UI is a rendering of that description.

+ +
+
+

How this works

+
    +
  • Documents — the document hierarchy and how documents map to named graphs
  • +
  • Content blocks — how rdf:_1, rdf:_2, … sequence a document's content
  • +
  • HTTP API — the PUT semantics behind put.sh
  • +
+
+

Next: Structure

+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/insight.ttl b/docs/tutorial/insight.ttl new file mode 100644 index 0000000..420c6c7 --- /dev/null +++ b/docs/tutorial/insight.ttl @@ -0,0 +1,73 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Insight" ; + dct:description "Turn SPARQL queries into charts and views" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Turn SPARQL queries into charts and views

+

With the graph populated, analysis is a query away — and in LinkedDataHub, queries, charts and + views are resources like everything else. A chart is three resources chained together: a + sp:Select query holding the SPARQL text, a ldh:ResultSetChart describing how to plot its results, and a ldh:Object block placing it on a document.

+
+

A revenue chart

+

On the Categories container, revenue per category — the query joins order items to products to categories across their documents:

+
<#category-revenue-query> a sp:Select ;
+    dct:title "Category revenue" ;
+    sp:text '''PREFIX schema: <https://schema.org/>
+
+SELECT ?categoryName (SUM(?sale) AS ?revenue)
+WHERE {
+    GRAPH ?orderGraph {
+        ?order schema:orderedItem ?orderItem .
+        ?orderItem schema:orderedItem ?product ;
+                   schema:orderQuantity ?quantity ;
+                   schema:price ?price .
+        BIND(?quantity * ?price AS ?sale)
+    }
+    GRAPH ?productGraph {
+        ?product schema:category ?category .
+    }
+    GRAPH ?categoryGraph {
+        ?category schema:name ?categoryName .
+    }
+}
+GROUP BY ?category ?categoryName
+ORDER BY DESC(?revenue)''' .
+
+<#category-revenue> a ldh:ResultSetChart ;
+    dct:title "Revenue by category" ;
+    spin:query <#category-revenue-query> ;
+    ldh:chartType <https://w3id.org/atomgraph/client#BarChart> ;
+    ldh:categoryVarName "categoryName" ;
+    ldh:seriesVarName "revenue" .
+
+<#category-revenue-block> a ldh:Object ;
+    rdf:value <#category-revenue> .
+

Add <#category-revenue-block> to the container's block sequence (rdf:_2 after the intro block) and PUT the document as before.

+
+
+

A view

+

Views render query results as document listings instead of plots — the container's own children listing is one. A grid of all categories:

+
<#select-categories-view> a ldh:View ;
+    spin:query <#select-categories-query> ;
+    ac:mode <https://w3id.org/atomgraph/client#GridMode> .
+
+
+

In the browser

+

The same resources can be created interactively: save a query from the query editor, then add a chart block that references + it.

+
+
+

What you now see

+

The Categories container is a small dashboard: a revenue bar chart, a products-per-category chart, and a grid of the categories themselves — all live views over the graph, recomputed on + each request.

+ +
+

Next: Composition

+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/media.ttl b/docs/tutorial/media.ttl new file mode 100644 index 0000000..ec43715 --- /dev/null +++ b/docs/tutorial/media.ttl @@ -0,0 +1,45 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Media" ; + dct:description "Upload files and link them to resources" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Upload files and link them to resources

+

Northwind comes with images: a photo per employee, an icon per category. Uploaded files are stored content-addressed — the file's URL is derived from the SHA-1 hash of its + content, under ${base}uploads/. The same bytes always land at the same URL, which is what makes file references stable enough to bake into import mappings.

+
+

In the browser

+

Upload a file with the file upload form as shown in the Upload file guide.

+
+
+

From the command line

+
add-file.sh \\
+    -b "$base" \\
+    -f "$cert_pem_file" \\
+    -p "$cert_password" \\
+    --title "Nancy Davolio" \\
+    --file "$PWD/employees/nancy.jpg" \\
+    --content-type "image/jpeg" \\
+    "$base"
+
+
+

Linking files to resources

+

A file becomes part of the graph the moment something points at it. The category mapping from the Data stage does exactly that — the CSV carries each image's hash, + and the mapping mints the upload URL from it:

+
?category foaf:depiction ?picture .
+...
+BIND(uri(concat(str($base), "uploads/", encode_for_uri(?pictureHash))) AS ?picture)
+
+
+

What you now see

+

Category and employee documents now render with their images — foaf:depiction is picked up by the default layout, no template work required.

+ +
+

Next: Insight

+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/model.ttl b/docs/tutorial/model.ttl new file mode 100644 index 0000000..ffbd486 --- /dev/null +++ b/docs/tutorial/model.ttl @@ -0,0 +1,73 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Model" ; + dct:description "Define the vocabulary that describes your domain" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Define the vocabulary that describes your domain

+

Each dataspace has a namespace ontology where you define the classes and properties of your domain. It lives in the admin application, which shares your base URI with + an admin. subdomain prefix: for the end-user app at https://localhost:4443/, the ontology document is + https://admin.localhost:4443/ontologies/namespace/.

+

Northwind reuses Schema.org terms rather than inventing its own, so "modeling" here means declaring which classes the app works with and how + they should be labeled:

+
@prefix :	    <#> .
+@prefix rdfs:	<http://www.w3.org/2000/01/rdf-schema#> .
+@prefix owl:    <http://www.w3.org/2002/07/owl#> .
+@prefix schema: <https://schema.org/> .
+
+: a owl:Ontology .
+
+schema:ProductGroup a owl:Class ;
+    rdfs:label "Product group" ;
+    rdfs:isDefinedBy : .
+
+schema:Product a owl:Class ;
+    rdfs:label "Product" ;
+    rdfs:isDefinedBy : .
+
+schema:Order a owl:Class ;
+    rdfs:label "Order" ;
+    rdfs:isDefinedBy : .
+
+

In the browser

+

Open the admin application and edit the namespace ontology document directly, or follow the Change model guide, which also + shows how to add SPIN constructors to classes — the templates that drive "create instance" forms in the UI.

+
+
+

From the command line

+

Append the class definitions from ns.ttl to the namespace ontology document with POST. The prepended @base makes the : prefix resolve to + ${base}ns# — the terms belong to the end-user namespace even though the ontology document lives in the admin app:

+
admin_base="https://admin.localhost:4443/"
+
+{ echo "@base <${base}ns> ."; cat ns.ttl; } | post.sh \\
+    -f "$cert_pem_file" \\
+    -p "$cert_password" \\
+    --content-type "text/turtle" \\
+    "${admin_base}ontologies/namespace/"
+

Ontologies are cached in memory, so tell the application to reload it:

+
clear-ontology.sh \\
+    -f "$cert_pem_file" \\
+    -p "$cert_password" \\
+    -b "$admin_base" \\
+    --ontology "${base}ns#"
+
+
+

What you now see

+

The ontology document in the admin app renders your classes with their labels. The visible payoff on the end-user side comes in the next stage: resources imported as + schema:Product or schema:Order arrive typed and labeled, and the model is what the UI uses to render and create them.

+
+
+

How this works

+
    +
  • Ontologies — namespace ontologies, imports and caching
  • +
  • Change model — classes, constructors and constraints from the UI
  • +
+
+

Next: Data

+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/publish.ttl b/docs/tutorial/publish.ttl new file mode 100644 index 0000000..917300a --- /dev/null +++ b/docs/tutorial/publish.ttl @@ -0,0 +1,33 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Publish" ; + dct:description "Grant public access and ship it" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Grant public access and ship it

+

So far only the owner's WebID certificate can see the app. Access is governed by ACL authorizations — documents in the admin + app, like everything else. Publishing means creating one authorization: read access on the whole dataspace for all agents.

+
+

From the command line

+
make-public.sh -b "$base" -f "$cert_pem_file" -p "$cert_password"
+

The script creates the public-read authorization in the admin application. Authorizations are data, so you can inspect the document it created — and delete it to unpublish.

+
+
+

In the browser

+

The same authorization can be created with the access control forms in the admin application — see the ACL reference for + the authorization model: who (agents, agent classes), what (documents, document classes), and which acl:mode.

+
+
+

What you now see

+

Open the base URL in a private browser window, with no certificate: the Northwind Traders app renders, charts and all. It is now a public Linked Data dataset and a public web + application — the same URLs serve both, negotiated by content type.

+ +
+

Next: App as a repository

+
"""^^rdf:XMLLiteral . diff --git a/docs/tutorial/structure.ttl b/docs/tutorial/structure.ttl new file mode 100644 index 0000000..d5821d9 --- /dev/null +++ b/docs/tutorial/structure.ttl @@ -0,0 +1,54 @@ +@prefix ldh: . +@prefix rdf: . +@prefix dh: . +@prefix dct: . + +<> a dh:Item ; + dct:title "Structure" ; + dct:description "Lay out the document hierarchy that will hold your data" ; + rdf:_1 <#content> . + +<#content> a ldh:XHTML ; + rdf:value """
+

Lay out the document hierarchy that will hold your data

+

Documents come in two kinds: containers, which hold child documents, and + items. Northwind's domain gives us the container layout directly — one container per entity type:

+
https://localhost:4443/
+├── categories/
+├── customers/
+├── employees/
+├── orders/
+├── products/
+├── regions/
+├── shippers/
+├── suppliers/
+└── territories/
+
+

In the browser

+

Create each container from the root document using the document creation form — see the Create data guide.

+
+
+

From the command line

+

A container is just a document whose description says it is one. categories.ttl:

+
@prefix dh:     <https://www.w3.org/ns/ldt/document-hierarchy#> .
+@prefix dct:    <http://purl.org/dc/terms/> .
+
+<> a dh:Container ;
+    dct:title "Categories" .
+

PUT it to the container URL, the same way the root document was updated:

+
cat categories.ttl | turtle --base="${base}categories/" | put.sh \\
+    -f "$cert_pem_file" \\
+    -p "$cert_password" \\
+    -t "application/n-triples" \\
+    "${base}categories/"
+

Repeat for the remaining eight containers — or skip ahead to App as a repository to see how the demo derives the document URL from the filename and + replays a whole folder in one command.

+
+
+

What you now see

+

The root document now lists the containers as its children, and each container URL resolves to an (empty) listing page. The URL space of your app is its information + architecture.

+ +
+

Next: Model

+
"""^^rdf:XMLLiteral . From 509266258b02a421b08af5b1494fdc9ea1072330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Tue, 18 Aug 2026 00:40:01 +0200 Subject: [PATCH 2/3] Keep XMLLiteral tags on one line for canonical XML form Jena's rdf:XMLLiteral validation in CI requires canonical form; a tag split across lines fails it. Co-Authored-By: Claude Fable 5 --- docs/tutorial/app-as-repository.ttl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/tutorial/app-as-repository.ttl b/docs/tutorial/app-as-repository.ttl index fe2fd02..fc725fb 100644 --- a/docs/tutorial/app-as-repository.ttl +++ b/docs/tutorial/app-as-repository.ttl @@ -12,8 +12,7 @@ rdf:value """

Make the app reproducible: a git repository that installs into any instance

Every stage so far produced files — document descriptions, mapping queries, CSV data, media. Kept in a repository with a small install script, they are the application: versionable, - diffable, reviewable, and replayable into any LinkedDataHub instance. This is the layout the Northwind Traders repository uses:

+ diffable, reviewable, and replayable into any LinkedDataHub instance. This is the layout the Northwind Traders repository uses:

northwind-traders/
 ├── root.ttl              # one .ttl per document...
 ├── categories.ttl

From 68f0445b3c52501ef28a250d6cbb9b44b47a7026 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= 
Date: Tue, 18 Aug 2026 09:44:11 +0200
Subject: [PATCH 3/3] Link the tutorial from about, get-started and build-apps

Add tutorial pointers on the about page, the get-started intro and the
build-apps guide, and point the tutorial's browser-track links at the
specific Create documents / Import CSV data how-to pages.

Co-Authored-By: Claude Fable 5 
---
 docs/about.ttl                 | 2 +-
 docs/get-started.ttl           | 1 +
 docs/tutorial/data.ttl         | 2 +-
 docs/tutorial/structure.ttl    | 2 +-
 docs/user-guide/build-apps.ttl | 1 +
 5 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/docs/about.ttl b/docs/about.ttl
index c696636..7110522 100644
--- a/docs/about.ttl
+++ b/docs/about.ttl
@@ -25,7 +25,7 @@
             
  • authentication using WebID and OpenID Connect
  • access control using WAC authorizations
  • -

    Checkout the user guide on application building.

    +

    Follow the tutorial to build your first app, then see the user guide on application building for the extension layers.

    Federated knowledge management and collaboration system

    diff --git a/docs/get-started.ttl b/docs/get-started.ttl index 41b67da..8ccddcc 100644 --- a/docs/get-started.ttl +++ b/docs/get-started.ttl @@ -14,6 +14,7 @@

    All the basics of LinkedDataHub. From installation to customizing the model and user interface.

    This guide will show how a LinkedDataHub application can be used to manage domain-specific RDF classes and instances. As an example, we will use SKOS concepts and concept schemes.

    Note that most management actions can also be performed using the CLI (Command Line Interface). Where applicable, the UI and CLI instructions are shown side by side. The UNESCO Thesaurus demo app demonstrates how SKOS vocabularies can be managed in LinkedDataHub.

    +

    New to LinkedDataHub? The tutorial builds a complete application, from an empty dataspace to a published app.

    Setup

    Setup is only required if you plan to run your own instance of LinkedDataHub. It consists of few steps, which involve creating a configuration file and running a diff --git a/docs/tutorial/data.ttl b/docs/tutorial/data.ttl index 2e2fae5..8fbf7db 100644 --- a/docs/tutorial/data.ttl +++ b/docs/tutorial/data.ttl @@ -49,7 +49,7 @@ WHERE

    In the browser

    -

    Create the import from the UI — upload the CSV file, pick the mapping query and the target container — following the Import data +

    Create the import from the UI — upload the CSV file, pick the mapping query and the target container — following the Import CSV data guide.

    diff --git a/docs/tutorial/structure.ttl b/docs/tutorial/structure.ttl index d5821d9..452cbd3 100644 --- a/docs/tutorial/structure.ttl +++ b/docs/tutorial/structure.ttl @@ -25,7 +25,7 @@ └── territories/

    In the browser

    -

    Create each container from the root document using the document creation form — see the Create data guide.

    +

    Create each container from the root document using the document creation form — see the Create documents guide.

    From the command line

    diff --git a/docs/user-guide/build-apps.ttl b/docs/user-guide/build-apps.ttl index a2a1012..04a76bc 100644 --- a/docs/user-guide/build-apps.ttl +++ b/docs/user-guide/build-apps.ttl @@ -11,6 +11,7 @@ <#content> a ldh:XHTML ; rdf:value """

    Using LinkedDataHub as a low-code platform for Knowledge Graph applications

    +

    If you are building your first app, start with the tutorial — this guide covers the layers beyond it.

    Every component in LinkedDataHub is data-driven and was designed with extensibility in mind. You can override behavior (e.g. Java method or XSLT template) without having to modify LinkedDataHub's codebase, and more importantly, without having to write the same logic from scratch.

    The following sections are split by component/layer and explain how to extend them when building bespoke apps.