Data-architectuur Jottem

A Collection of Interesting Ideas,

This version:
https://github.com/inside-out-time-machines/design
Editor:
Bob Coret (Gouda Tijdmachine)

1. Inleiding en lagenmodel

Dit document beschrijft de data-architectuur van het Jottem-platform: welke data waar wordt vastgelegd, hoe die door het platform stroomt en in welke vormen die aan de buitenwereld wordt aangeboden. Het sluitstuk is de outputcontrole: per output is nagegaan of alle benodigde data beschikbaar is in de eerdere lagen; de daarbij gevonden hiaten zijn direct in het datamodel verwerkt.

De data leeft in drie lagen:

Laag Opslag Inhoud
Bronlaag PostgreSQL (ERD), externe Object Storage (S3), AnnoRepo PostgreSQL is de bron van waarheid voor organisaties, gebruikers, media en metadata; de externe Object Storage (S3) bevat de mediabestanden (originelen, afgeleiden, thumbnails, audio); AnnoRepo bevat de W3C-annotaties; de versiegeschiedenis van elke annotatiemutatie staat in het Gebeurtenislog.
Verwerkingslaag Celery-workers, Elasticsearch, Fuseki Workers genereren afgeleiden en thumbnails, mappen metadata naar RDF (upsert in Fuseki), indexeren in Elasticsearch en versturen e-mail. Elasticsearch en Fuseki zijn afgeleide stores: ze zijn altijd volledig te herbouwen uit de bronlaag.
Outputlaag api/iiif/anno/data.iotm.nl, RSS De standaard-interfaces voor de buitenwereld: IIIF (Image, Presentation, Change Discovery), W3C Web Annotation Protocol, RDF/SPARQL/datadump met datasetbeschrijving, de REST-API’s en RSS-feeds.

Leidend principe (zie ook de systeemarchitectuur): standaarden aan de buitenkant - elke output volgt een open standaard zodat harvesters en aggregators (o.a. NDE) zonder maatwerk kunnen aansluiten - en PostgreSQL plus AnnoRepo en de Object Storage als bron van waarheid, zodat elke afgeleide store en elke output reproduceerbaar is.

2. ERD diagram

erDiagram

Organisatie {
  int id PK
  string naam
  string slug
  string beschrijving
  string website
  string email
  string identifier
  string spatialUri
  string spatialNaam
  float spatialLat
  float spatialLon
  string NAAN
  string kleurenpalet
  string logo
  string favicon
}

Project {
  uuid projectId PK
  string naam
  string slug
  string beschrijving
  string oproep
  date startDatum
  date eindDatum
  string afbeelding
  string datasetLicentie
  string terminologiebronnen
  string verrijkingen
  string uploadWijzen
  string status
  date creatieDatum
  int organisatieId FK
}

Gebruiker {
  int gebruikersId PK
  string naam
  string email
  bool naamPubliek
  date bevestigingsDatum
  date registratieDatum
  date laatsteLoginDatum
  bool favorietenPubliek
}

GebruikerRol {
  int gebruikerRolId PK
  int gebruikersId FK
  int organisatieId FK
  string rol
}

Media {
  uuid mediaId PK
  string titel
  string beschrijving
  string bestandsnaam
  string bron "upload | iiif | url"
  string bronUrl "manifest-/info.json-/foto-URL bij externe bron"
  string externeIiifService "image service-basis bij een beeldbank-bron"
  string genre
  string licentie
  int breedte
  int hoogte
  string mimeType
  string status
  string afkeurReden
  string herkenbaar
  float herkenbaarBetrouwbaarheid
  bool toestemmingPersonen
  string ark
  date creatieDatum
  date publicatieDatum
  date wijzigingsDatum
  int gebruikersId FK
  int organisatieId FK
  uuid projectId FK
}

Metadata {
  int metadataId PK
  uuid mediaId FK
  int gebruikersId FK
  string type
  string label
  string value
  string uri
}

Favoriet {
  int favorietId PK
  int gebruikersId FK
  uuid mediaId FK
  date creatieDatum
}

MediaRelatie {
  int relatieId PK
  uuid bronMediaId FK
  uuid doelMediaId FK
  string aard "zelfde-object"
  int gebruikersId FK
  date creatieDatum
}

Verwijderverzoek {
  int verzoekId PK
  uuid mediaId FK
  string reden
  string email
  string status
  string toelichting
  date creatieDatum
  date afhandelDatum
}

Melding {
  int meldingId PK
  uuid mediaId FK
  string annotatieIri
  string reden
  string status
  date creatieDatum
  date afhandelDatum
}

Gebeurtenislog {
  int logId PK
  string type
  date tijdstip
  int organisatieId FK
  uuid projectId FK
  int gebruikersId FK
  string payload
  date verwerktOp
}

%% Project: campagne op organisatieniveau (bijv. "Smaak van Gouda"), beheerd door de
%% organisatiebeheerder. Elke organisatie heeft minstens een project; elke jottem hoort
%% bij precies een project (Media.projectId verplicht). Project.status: actief / afgerond.
%% Project.datasetLicentie: licentie van de projectdataset (datasetbeschrijving per project).
%% Project.terminologiebronnen: lijst van bron-URI’s uit het NDE Termennetwerk die binnen
%% dit project beschikbaar zijn voor term-URI’s (standaard: alle bronnen).
%% Publiek zichtbare identifiers (mediaId, projectId) zijn betekenisloze UUID’s; interne
%% id’s (gebruikersId e.d.) blijven interne sleutels.
%% Rollen: een gebruiker kan meerdere rollen hebben, per organisatie (GebruikerRol);
%% de rol platformbeheerder heeft geen organisatieId. Lidmaatschap van een organisatie
%% volgt uit de GebruikerRol-rijen. GebruikerRol is de LEIDENDE bron voor autorisatie;
%% de IdP (Authentik) doet uitsluitend authenticatie (koppeling via sub-claim).
%% Media.status: nieuw / goedgekeurd / afgekeurd / gedepubliceerd; afkeurReden alleen bij
%% afgekeurd. Afgekeurde jottems kunnen door de uploader worden bijgewerkt en opnieuw
%% ingediend (terug naar nieuw); gedepubliceerd volgt uit een gehonoreerd verwijderverzoek
%% (tombstone op de duurzame URL, schoning van alle outputs).
%% Verwijderverzoek.status: open / gehonoreerd / afgewezen.
%% Gebeurtenislog: bron voor statistieken (type: login, upload, goedkeuring, afkeuring,
%% annotatie, ...); persoonsgebonden regels worden na een bewaartermijn geaggregeerd of
%% geanonimiseerd (AVG).
%% Media.herkenbaar (ja/nee) + herkenbaarBetrouwbaarheid: resultaat van de Herkenbaar API
%% bij upload (paradata, geen gebruikersmetadata); toestemmingPersonen: verklaring van de
%% uploader dat toestemming van herkenbare personen is geregeld.
%% Media.titel/beschrijving/licentie, breedte/hoogte/mimeType en wijzigingsDatum zijn
%% toegevoegd n.a.v. de outputcontrole in de data-architectuur (IIIF-label/rights/canvas,
%% RDF schema:name/license, RSS en IIIF Change Discovery).
%% Organisatie.beschrijving/website: nodig voor publisher-informatie, IIIF provider en
%% RSS-channel. Gebruiker.naamPubliek: bepaalt of de naam als creator bij annotaties
%% getoond wordt.
%% Organisatie.NAAN en Media.ark zijn gereserveerd voor de ARK-fase (uitgesteld,
%% zie keuze-oplossingsrichting).
%% MediaRelatie koppelt twee jottems binnen hetzelfde project ("zelfde object, andere
%% foto", zie het verrijkingenhoofdstuk). Eén rij per koppeling; beide richtingen worden
%% bij het lezen samengevoegd, zodat de relatie bij allebei de jottems zichtbaar is. De
%% linking-annotatie en dcterms:relation in de RDF zijn hiervan afgeleid (V-9).

%% Locatie- en tijdlijngegevens (adres, openings-/sluitingsjaar, geo-WKT, archiefbron)
%% worden vastgelegd als Metadata-rijen op Media; er is geen apart locatiemodel.
%% De coordinaten komen van de speld die de uploader op de kaart prikt (lat/lon als
%% Metadata-rijen); er is geen geocoding-dienst in de MVP.
%% Melding: rapportage van een annotatie of reactie (spam/ongepast) door bezoekers;
%% annotatieIri wijst naar de W3C-annotatie in de annotatieserver, status: nieuw / afgehandeld /
%% afgewezen. Afhandeling door de moderator, gelogd in het Gebeurtenislog.
%% Gebeurtenislog is tevens de transactional outbox (zie systeemarchitectuur):
%% payload beschrijft de mutatie, verwerktOp markeert succesvolle doorwerking naar
%% Elasticsearch / Fuseki / Varnish-purge.

%% Relationships
Organisatie ||--o{ GebruikerRol : "kent"
Organisatie ||--o{ Media : "bevat"
Organisatie ||--o{ Project : "voert_uit"

Project ||--o{ Media : "verzamelt"

Gebruiker ||--o{ GebruikerRol : "heeft"
Gebruiker ||--o{ Media : "uploadt"

Media ||--o{ Metadata : "heeft"

Gebruiker ||--o{ Favoriet : "maakt"
Media ||--o{ Favoriet : "wordt_gefavoriet"

Media ||--o{ MediaRelatie : "verwijst_naar"
Media ||--o{ MediaRelatie : "wordt_verwezen"
Gebruiker ||--o{ MediaRelatie : "legt"

Media ||--o{ Verwijderverzoek : "betreft"
Media ||--o{ Melding : "betreft"
Organisatie ||--o{ Gebeurtenislog : "logt"

Gebruiker ||--o{ Metadata : "maakt"

3. URI-strategie en content negotiation

Elke gepubliceerde jottem krijgt een duurzame platform-URL die als identifier in alle outputs wordt gebruikt:

Resource URI-patroon
Jottem (duurzame link) https://www.iotm.nl/jottem/{mediaId}
Projectpagina (publiek) https://www.iotm.nl/{organisatieSlug}/{projectSlug}
Project (API) https://api.iotm.nl/v1/project/{projectId}
IIIF Image API https://iiif.iotm.nl/3/{mediaId} (/info.json, tiles)
IIIF Manifest https://api.iotm.nl/v1/jottem/{mediaId}/iiif/manifest
IIIF Collection https://api.iotm.nl/v1/organisatie/{slug}/jottems/iiif/collection en https://api.iotm.nl/v1/project/{projectId}/iiif/collection
Annotaties van een jottem (container) https://anno.iotm.nl/w3c/{jottemId}/
Annotatie https://anno.iotm.nl/w3c/{jottemId}/{annotationId}
Annotaties van organisatie / project https://api.iotm.nl/v1/organisatie/{slug}/annotations en https://api.iotm.nl/v1/project/{projectId}/annotations
Projectdataset https://data.iotm.nl/project/{projectId}/dataset (beschrijving), https://data.iotm.nl/project/{projectId}/dump-{projectSlug}.nt.gz (dump; de oude naam zonder slug verwijst permanent door), https://data.iotm.nl/sparql (endpoint)

Deelbaarheid. (Zie ook het overkoepelende hoofdstuk Deelbaarheid, met onder meer de inbedbare widgets.) Elke jottem-detailpagina heeft een Delen-knop (in de primaire kleur van de organisatie, rechtsboven bij de titel en rechtsonder bij de metadata) die het deelmenu van het apparaat opent via de Web Share API, met titel, de oproep "weet jij hier meer van?" en de duurzame jottem-URL. Zo lopen alle kanalen (sociale media, e-mail, link kopiëren) via de keuze die de bezoeker zelf al heeft; browsers zonder die API tonen de knop niet. De HTML-weergave bevat Open Graph-metadata (gerealiseerd; ook op de home-, organisatie- en projectpagina’s, die de tags elk zelf zetten), zodat gedeelde links (ook via de deel-oproep in de goedkeuringsmail) een nette preview tonen:

Open Graph-element Bron
og:title Media.titel met daarachter een willekeurige, binnen het project actieve verrijkings-call-to-action (terugval: de vaste oproep "- weet jij hier meer van?"), zodat een gedeelde link meteen om een concrete bijdrage vraagt; zie het hoofdstuk Deelbaarheid
og:description Media.beschrijving (of, als die ontbreekt, een zin met project- en organisatienaam)
og:image de IIIF Image API-afbeelding (eigen service of beeldbank): full/!1200,1200 en full/max zodra het bronbeeld kleiner is dan 1200 px, want ! mag in IIIF 3 niet vergroten. Bij bron url de foto-URL zelf; presigned URL’s uit de Object Storage verlopen en zijn ongeschikt voor previews
og:image:width/height berekend uit Media.breedte/Media.hoogte en de gevraagde IIIF-maat, dus de werkelijke afmetingen van de preview; onbekend = weglaten
og:url de duurzame jottem-URL
og:image:alt Media.titel
og:type, og:site_name, og:locale vast: article bij jottems en website bij de overige pagina’s, "Jottem", nl_NL

Elke publiekspagina (home, organisatie, project, jottem) draagt daarnaast een zelfverwijzende <link rel="canonical">; op een projectoverzicht houdt een vervolgpagina zijn eigen ?pagina-parameter in de canonical, zodat zoekmachines elke pagina van de reeks als eigen pagina indexeren.

De duurzame jottem-URL ondersteunt content negotiation: Accept: text/html levert de HTML-weergave (IIIF-viewer + metadata + annotaties), application/ld+json, text/turtle of application/rdf+xml levert de RDF-representatie conform schema.org AP NDE (303-redirect naar data.iotm.nl). De mediaId en projectId zijn niet-herbruikbare, betekenisloze identifiers (UUID); duurzame data-URI’s zijn daarop gebaseerd, terwijl publiekspagina’s leesbare slugs gebruiken. Zo kan in de latere ARK-fase (zie het keuzedocument) een ARK ark:/{NAAN}/{shoulder}{mediaId} zonder linkbreuk op dezelfde resource worden gelegd.

4. IIIF

4.1. IIIF Image API v3

Cantaloupe serveert per gepubliceerde jottem info.json, tiles, regio’s en formaten op basis van de pyramidal-TIFF-afgeleide in de Object Storage (derivatives-bucket). De benodigde data:

Element Bron
id URI-strategie (afgeleid van Media.mediaId)
width, height Media.breedte, Media.hoogte (vastgelegd bij verwerking van de upload)
beeldata (tiles) Object Storage derivatives (pyramidal TIFF, gegenereerd door worker)

4.2. IIIF Presentation API v3 (Manifest en Collection)

De backend genereert per jottem een Manifest en per organisatie en per project een Collection (met iiif-prezi3), rechtstreeks uit de database:

Manifest-element Bron
id, type URI-strategie
label Media.titel
summary Media.beschrijving
metadata (paren) Metadata-rijen (vervaardiger, datering, plaats, steekwoorden, ...)
rights Media.licentie (URI), genormaliseerd naar de canonieke http://-vorm van Creative Commons/RightsStatements.org zoals de Presentation-spec eist; in RDF en de datasetbeschrijving blijft de https://-vorm conform de NDE-requirements
requiredStatement (bronvermelding) Organisatie.naam + inzender (indien Gebruiker.naamPubliek)
provider Organisatie.naam, Organisatie.logo, Organisatie.website
navDate Metadata type datering
thumbnail Object Storage thumbs
canvas (width/height) Media.breedte, Media.hoogte
image service IIIF Image API (zie boven)
annotations (AnnotationPage) AnnoRepo, container per jottem
Collection-items Media per organisatieId/projectId (status goedgekeurd)

Audio-jottems (fase 2, zie de uploadscope in de requirements) krijgen een manifest met een audio-canvas (duration; vastgelegd bij verwerking van de upload) en het bestand uit de audio-bucket als painting annotation.

4.3. IIIF Change Discovery v1

Per organisatie én per project publiceert de backend een ActivityStreams-feed (/organisatie/{slug}/activity-stream resp. /project/{projectId}/activity-stream, OrderedCollection met pages) waarmee harvesters nieuwe en gewijzigde jottems kunnen volgen; de projectfeed is tevens een distributie in de projectdatasetbeschrijving:

Element Bron
Create-activiteit Media.publicatieDatum (moment van goedkeuring/publicatie)
Update-activiteit Media.wijzigingsDatum
object (het Manifest) URI-strategie
endTime de betreffende datum

Regel: elke mutatie die de outputs raakt - metadata-wijziging, nieuwe/gewijzigde/verwijderde annotatie, depublicatie - werkt Media.wijzigingsDatum bij, zodat de Change Discovery-feed (en schema:dateModified in de RDF) ook annotatie-wijzigingen omvat, zoals de usecase voor API-gebruikers vraagt. Het verwijderen van een annotatie levert daarbij een Update-activity op de jottem op (géén Delete: de jottem zelf blijft bestaan; Delete/tombstone is voorbehouden aan depublicatie van de jottem). De AnnotationCollections komen live uit AnnoRepo en tonen de verwijderde annotatie direct niet meer; de workers hersynchroniseren daarna de named graph in Fuseki en de zoekindex via het outbox-mechanisme uit de systeemarchitectuur.

5. Annotaties

Annotaties volgen het W3C Web Annotation Data Model en worden opgeslagen in AnnoRepo; lezen kan publiek en standaardconform via het W3C Web Annotation Protocol op anno.iotm.nl (container per jottem: /w3c/{jottemId}/, annotatie: /w3c/{jottemId}/{annotationId}). Schrijven verloopt via de beheer-API, die de geauthenticeerde gebruiker als creator toevoegt (geanonimiseerd als naamPubliek uit staat) en autorisatie bewaakt (eigen annotaties bewerken en verwijderen); elke mutatie wordt met volledige payload vastgelegd in het Gebeurtenislog, dat daarmee de versiegeschiedenis draagt. Platform-extensies (jottem:aard, jottem:verrijking) zijn gedefinieerd in een eigen vocabulaire op https://data.iotm.nl/ns/jottem.jsonld, dat tegelijk als JSON-LD-context dient. Dat document staat op het open-datadomein en wordt door de API uitgeleverd: een vocabulaire-URI staat in gepubliceerde tripels en hoort die te overleven, en de publiekssite is een applicatie die vervangen kan worden.

Element Bron
id, created, modified AnnoRepo / beheer-API
motivation vocabulaire, zie § 9 Vocabulaires en termen
target (gehele jottem) duurzame jottem-URL of Canvas-URI
target (getekend vlak) Canvas-URI + SVG/FragmentSelector (Annotorious)
body (vrije tekst: herinnering, aanvulling, correctie) TextualBody
body (identificatie persoon/gebouw/bedrijf/plaats/gebeurtenis) SpecificResource met label + externe URI (zie § 9 Vocabulaires en termen)
body (geo: standpunt fotograaf, zichtveld) WKT/GeoJSON-body, zie § 10 Zoekindex en geo-data
reactie op annotatie annotatie met de bestaande annotatie als target
creator Gebruiker.naam indien Gebruiker.naamPubliek, anders geanonimiseerd

5.1. Ontsluiting: AnnotationCollection per jottem en per organisatie

De annotatieserver hanteert één container per jottem (containernaam = mediaId). Daarmee zijn annotaties op drie niveaus opvraagbaar, telkens W3C-conform:

Niveau Endpoint Output
Individuele annotatie https://anno.iotm.nl/w3c/{jottemId}/{annotationId} (en GET /annotation/{annotationId} in de publieke API) Annotation
Per jottem GET https://anno.iotm.nl/w3c/{jottemId}/ - rechtstreeks uit de container; de publieke API biedt GET /jottem/{jottemId}/annotations als doorverwijzing en het IIIF Manifest verwijst hiernaar via annotations AnnotationCollection (gepagineerd via AnnotationPages)
Per project GET /project/{projectId}/annotations in de publieke API - de backend aggregeert de containers van alle gepubliceerde jottems van het project (jottem-lijst uit Media.projectId) AnnotationCollection (gepagineerd via AnnotationPages)
Per organisatie GET /organisatie/{slug}/annotations in de publieke API - de backend aggregeert de containers van alle gepubliceerde jottems van de organisatie (jottem-lijst uit Media) AnnotationCollection (gepagineerd via AnnotationPages)

6. RDF en datadump

6.1. Mapping naar schema.org AP NDE

Een worker mapt elke gepubliceerde jottem naar RDF conform het schema.org-profiel van NDE en herbouwt de betreffende named graph in Fuseki (named graph per project, met de projectdataset-IRI als graafnaam; organisatiebrede vragen beslaan de grafen van de projecten van die organisatie). Een nachtelijke reconciliatietaak herbouwt alle grafen uit PostgreSQL. De representatie is opvraagbaar via content negotiation op de duurzame URL, via SPARQL en via de datadump.

schema.org AP NDE Bron
@id duurzame jottem-URL (URI-strategie)
@type schema:ImageObject / schema:AudioObject op basis van Media.mimeType
schema:name Media.titel
schema:description Media.beschrijving
schema:genre / schema:additionalType Media.genre + term-URI (zie § 9 Vocabulaires en termen)
schema:license Media.licentie
schema:datePublished / schema:dateModified Media.publicatieDatum / Media.wijzigingsDatum
schema:dateCreated Metadata type datering
schema:creator Metadata type vervaardiger
schema:contentLocation Metadata type plaats/adres (schema:Place, schema:sameAs naar term-URI, schema:geo bij WKT/GeoJSON)
schema:temporalCoverage Metadata openings-/sluitingsjaar
schema:about Metadata/annotaties: personen, gebouwen, bedrijven, gebeurtenissen (label + URI)
schema:keywords Metadata type steekwoord
schema:isBasedOn / schema:subjectOf Metadata type archiefbron (label + URI)
schema:contentUrl, schema:width, schema:height, schema:encodingFormat IIIF Image-URI, Media.breedte/hoogte/mimeType
schema:thumbnailUrl Object Storage thumbs
schema:publisher Organisatie.naam + Organisatie.website
schema:isPartOf de projectdataset-IRI (Media.projectId)
schema:inLanguage vast nl (NL-only, zie niet-functionele requirements)
dcterms:relation MediaRelatie: een andere jottem die hetzelfde object toont (zie Zelfde object, andere foto); beide richtingen, alleen gepubliceerde jottems
annotaties verwijzing naar het W3C Annotation Protocol-endpoint per jottem

Persoonsgegevens van inzenders komen niet in de RDF: de inzender wordt alleen als schema:contributor opgenomen indien Gebruiker.naamPubliek.

Taalmerk. Alles wat een mens leest krijgt @nl: titel, beschrijving, genre, steekwoorden en de labels bij een term-URI, plus de namen van organisaties en onderwerpen. Dat is meer dan schema:inLanguage, want dat veld zegt iets over het werk als geheel en niet over een afzonderlijke literal; een afnemer die alleen schema:about binnenhaalt, ziet zonder taalmerk een label zonder te weten in welke taal het staat. Identificatoren, datums, formaten, licenties en URL’s blijven ongemerkt: die zijn niet in een taal geschreven. Het merk staat in de JSON-LD-context en niet per waarde, zodat de JSON leesbaar blijft ("keywords": ["menukaarten"]) terwijl elke serialisatie er "menukaarten"@nl van maakt.

Term-URI’s horen in de data, niet op de pagina. Een steekwoord uit het Termennetwerk wordt opgeslagen als label plus URI. In de RDF, de dump en het IIIF-manifest gaan ze samen naar buiten, want daar is de URI het punt: die maakt het woord koppelbaar aan andere collecties. De publiekspagina toont alleen het label; anders zou onder elk steekwoord dezelfde term nog een keer staan, als lange URL.

6.2. Frames naast shapes

Shapes en frames beantwoorden twee verschillende vragen. Een SHACL-shape (zoals dat van het NDE Datasetregister, waartegen het platform de datasetbeschrijving valideert voor aanmelding) zegt wat waar moet zijn: welke eigenschappen verplicht zijn, welke datatypes gelden, welke waarden zijn toegestaan. Een JSON-LD-frame zegt hoe de JSON eruitziet: welke knopen worden genest, welke komen alleen als IRI mee, en welke eigenschappen zijn altijd een array. RDF kent geen boomvorm; JSON wel. Wie een graaf (datadump, SPARQL-resultaat, named graph) naar JSON-LD serialiseert, moet die boom dus zelf kiezen, en zonder frame kiest elke consument een andere.

De JSON-LD die de API teruggeeft wordt documentgericht opgebouwd en niet uit de graaf geserialiseerd; de boomvorm per jottem, per projectdataset en voor de catalogus ligt daarmee feitelijk al vast. Wat ontbrak is dat die vorm ook normatief is vastgelegd, zodat consumenten die met de dump of met SPARQL werken op dezelfde vorm uitkomen. Daarvoor publiceert het platform per kernentiteit een frame:

Entiteit Frame Vorm
jottem (schema:ImageObject / schema:AudioObject) jottem.frame.jsonld publisher, creator/contributor, contentLocation (met geo) en subjectOf genest; isPartOf, isBasedOn, additionalType en dcterms:relation alleen als IRI; keywords, about, inLanguage, contributor, subjectOf en dcterms:relation altijd een array
projectdataset (schema:Dataset) dataset.frame.jsonld publisher en creator genest (met contactPoint), distribution genest en altijd een array, includedInDataCatalog alleen als IRI
datacatalogus (schema:DataCatalog) datacatalog.frame.jsonld dataset altijd een array van dataset-identiteiten; wie de datasets zelf wil, framet die apart met het datasetframe
webannotatie (oa:Annotation) annotatie.frame.jsonld body, target en creator genest; body en target altijd een array
annotatiecollectie (as:OrderedCollection) annotatiecollectie.frame.jsonld first naar AnnotationPage, items als geframede annotaties

De organisatie krijgt bewust geen eigen frame: die heeft in dit platform geen resolvebare URI van onszelf (de @id van schema:publisher is de eigen website van de erfgoedorganisatie), dus een frame zou iets beschrijven wat wij niet publiceren.

De belangrijkste praktische winst zit in de arrays. De klassieke bron van broze clientcode is dat keywords, about, distribution of body de ene keer een object is en de andere keer een lijst, afhankelijk van het aantal waarden. De frame-context pint die eigenschappen op "@container": "@set", zodat een consument nooit meer op Array.isArray() hoeft te controleren.

Namespace. De frames pinnen de schema.org-namespace expliciet op {"@vocab": "https://schema.org/"} in plaats van de externe context https://schema.org/ te dereferencen: dat contextdocument mapt kale termen namelijk naar http://schema.org/, een andere namespace dan de https://schema.org/-IRI’s die de Turtle-, RDF/XML-, dump- en SPARQL-representaties gebruiken. Dezelfde pinning geldt voor de JSON-LD-representaties zelf, zodat alle serialisaties van dezelfde jottem ook werkelijk dezelfde graaf opleveren. Dit kwam aan het licht bij de toets van de frames tegen de live uitvoer en is in de API doorgevoerd; het SHACL-shape van het NDE Datasetregister valideert de gepinde vorm. Voor annotaties geldt hetzelfde uitgangspunt: elke representatie, ook de aggregerende AnnotationCollection, draagt naast http://www.w3.org/ns/anno.jsonld de prefixafspraak {"jottem": "https://data.iotm.nl/ns/jottem.jsonld#"}, anders zijn jottem:verrijking en jottem:aard in dat document geen resolvebare termen. Ook dat is doorgevoerd: de vijf frames zijn met pyld getoetst tegen de live uitvoer, de datadump en een losse annotatie, en leveren in alle gevallen dezelfde tripels op als de bron.

Kanttekening bij dumps. Framen vraagt dat de hele graaf in het geheugen past. Voor grote projecten is het advies per named graph te framen (die is per project) of eerst een CONSTRUCT te doen en het frame op dat resultaat toe te passen.

Uitvoerbaar contract. De frames zijn niet langer alleen documentatie: de API levert ze uit op https://data.iotm.nl/ns/frames/{naam}.frame.jsonld en past ze zelf toe wanneer een client erom vraagt met Accept: application/ld+json;profile="http://www.w3.org/ns/json-ld#framed". Het antwoord noemt in zijn eigen Content-Type welk frame is toegepast, en herhaalt dat in een Link-header met rel="describedby":

Content-Type: application/ld+json;profile="http://www.w3.org/ns/json-ld#framed
              https://data.iotm.nl/ns/frames/jottem.frame.jsonld"
Link: <https://data.iotm.nl/ns/frames/jottem.frame.jsonld>; rel="describedby"; type="application/ld+json"
Vary: Accept

Ondersteund op de jottem (/jottem/{id}), de projectdataset, de datacatalogus en de twee aggregerende annotatiecollecties. Bewust niet ondersteund, en dat is geen omissie: de losse annotatie (die levert de annotatieserver uit, niet de API), de datadump, de IIIF-manifesten en -collections, de Change Discovery-stromen en de RSS-feeds. Vraagt een client daar toch om het profiel, dan krijgt hij de gewone representatie en geen foutmelding: het gevraagde mediatype leveren we immers wel. Boven een aggregatie van vijfduizend annotaties vervalt het framen om dezelfde reden als in de kanttekening hierboven.

Het frame dat de API toepast is byte-identiek aan het frame in dit document; een contracttest haalt beide op en vergelijkt ze. Diezelfde toets eist per ondersteunde URL dat de geframede en de gewone representatie dezelfde graaf opleveren: framen bepaalt de vorm, nooit de inhoud.

6.3. Datasetbeschrijving en datadump

Per project publiceert het platform een datasetbeschrijving als schema:Dataset conform de NDE-requirements voor datasets, aanmeldbaar bij het NDE Datasetregister; de platformbrede datacatalogus (schema:DataCatalog, /datacatalog) bundelt alle projectdatasets. Omdat elke jottem bij precies één project hoort, dekken de projectdatasets samen alle gepubliceerde data.

Verplicht/aanbevolen element Bron
IRI van de dataset https://data.iotm.nl/project/{projectId}/dataset
schema:name Project.naam
schema:description Project.beschrijving
schema:license (van de dataset) Project.datasetLicentie
schema:publisher Organisatie.naam, Organisatie.website
schema:distribution datadump (dump-{projectSlug}.nt.gz, application/n-triples+gzip) én SPARQL-endpoint; aanvullend zie § 6.4 Aanvullende distributies (verkenning)
schema:dateModified afgeleid: max(Media.wijzigingsDatum) per project
schema:temporalCoverage Project.startDatum – Project.eindDatum
schema:inLanguage vast nl

De datadump wordt per project on-the-fly gegenereerd uit PostgreSQL (dezelfde mapping als de Fuseki-graaf, met korte cache; consistent by construction) en is tevens onderdeel van de exit-/exportstrategie uit het keuzedocument: de dumps plus de mediabestanden vormen samen een volledige, herbruikbare export van de gepubliceerde data.

Open-data-landingspagina. De wortel van de data-host draagt een landingspagina voor (her)gebruikers (technisch publiek, geen B1-plicht) die per organisatie en project alle uitgangen bundelt - datasetbeschrijving (met link naar het NDE Datasetregister), datadump, IIIF Collection, activity-stream, W3C AnnotationCollection, RSS, aantallen jottems en annotaties - plus de publieke API als Swagger UI (specificatie uit openapi.yaml) en de broncoderepo’s op GitHub. De pagina leeft in de eigen repo data, wordt in de Jottem-huisstijl geserveerd naast de RDF-endpoints (/project/*, /datacatalog, /sparql) en haalt de projectgegevens client-side uit de publieke API (GET /organisaties). In de huidige fase staat dit geheel op data.dev.iotm.nl; bij livegang verhuist het naar data.iotm.nl.

SPARQL-endpoint met editor. /sparql is het leesendpoint op Fuseki (intern /ds/sparql); een browser die er zonder query aanklopt (GET, lege querystring, Accept: text/html) wordt doorgestuurd naar een YASGUI-editor op /yasgui/ met het endpoint voorgevuld, naar het voorbeeld van sparql.goudatijdmachine.nl. Machineclients (GET met query, POST) merken niets van de redirect.

Beheer en aanmelding. De organisatiebeheerder bewerkt per project de velden waaruit de datasetbeschrijving wordt gegenereerd (Project.naam, beschrijving, datasetLicentie; publisher-gegevens uit de organisatie) en kan de beschrijving - uitsluitend wanneer het project openbare (gepubliceerde) data heeft - valideren en aanmelden bij het NDE Datasetregister via de Datasetregister-API: de backend valideert eerst tegen het SHACL-shape van het register (PUT /datasets/validate), meldt daarna de URL van de datasetbeschrijving aan (POST /datasets) en kan deze ook weer afmelden (DELETE /datasets). Het domein iotm.nl moet daarvoor eenmalig op de allowlist van het register staan (POST /allowed-domains). Zie de bijbehorende endpoints in de beheer-API.

6.4. Aanvullende distributies (verkenning)

Naast de RDF-datadump en het SPARQL-endpoint kunnen ook de andere outputkanalen van een project als schema:distribution in de projectdatasetbeschrijving worden opgenomen; een distributie vereist slechts een schema:contentUrl en schema:encodingFormat (uitsluitend het MIME-type; een eventueel profiel staat apart in schema:usageInfo), en het SHACL-shape van het register staat extra distributies toe. Dat maakt álle toegangswegen tot de projectdata vindbaar via één datasetbeschrijving:

Kandidaat-distributie contentUrl encodingFormat Overwegingen
RDF-datadump (bestaand) https://data.iotm.nl/project/{projectId}/dump-{projectSlug}.nt.gz application/n-triples+gzip de kern-distributie voor NDE-compatibiliteit
SPARQL-endpoint (bestaand) https://data.iotm.nl/sparql application/sparql-results+json bevraagbaar per named graph (project)
IIIF Collection de collection per project application/ld+json (usageInfo: het IIIF Presentation-profiel) direct bruikbaar in IIIF-viewers en -aggregators; bestaat in de publieke API
RSS de jottem-feed per project application/rss+xml laagdrempelig volgen van nieuwe jottems in het project; geen linked data, maar als distributie toegestaan
Webannotaties per project GET /project/{projectId}/annotations (zie § 5.1 Ontsluiting: AnnotationCollection per jottem en per organisatie) application/ld+json (usageInfo: het W3C anno-profiel) ontsluit de verrijkingslaag als zelfstandig herbruikbare (deel)dataset; het aggregerende endpoint bestaat in de publieke API (containerindeling: per jottem)
IIIF Change Discovery de ActivityStreams-feed per project application/ld+json maakt incrementeel harvesten vindbaar; er is ook een organisatiebrede feed

Aanbeveling: alle kandidaten als extra distributies opnemen - de endpoints bestaan in de publieke API, het is louter een uitbreiding van de gegenereerde projectdatasetbeschrijving. Alle kandidaten zijn volledig afleidbaar uit de bestaande bronlagen; er is geen nieuwe brondata nodig.

6.5. E-depot-export: BagIt als verpakking, RO-Crate als beschrijving

Naast de RDF-datadump kan per project een compleet exportpakket worden aangemaakt waarmee al het materiaal van een project kan worden aangeboden aan het e-depot van een archiefinstelling (het SIP in OAIS-termen), en dat tevens de exit-strategie uit het keuzedocument concreet maakt:

{projectSlug}-{datum}.zip  (BagIt)
├── bagit.txt / bag-info.txt          project, organisatie, datum, aantallen
├── manifest-sha512.txt               checksums van alle payload-bestanden
└── data/
    ├── ro-crate-metadata.json        beschrijft alles (schema.org)
    ├── dump.nt.gz                    de RDF-datadump van het project
    ├── media/{mediaId}.(tif|jpg|…)   originelen uit de Object Storage
    ├── metadata/{mediaId}.jsonld     per jottem (schema.org AP NDE)
    └── annotaties/{mediaId}.jsonld   W3C Web Annotations per jottem

Een instellingsspecifieke metadatalaag (MDTO, of een E-ARK SIP/CSIP-structuur) wordt bewust niet standaard gegenereerd: Nederlandse e-depots verschillen daarvoor te sterk (zie de infrastructuuranalyse); zo’n laag is maatwerk per ontvangende instelling en kan bovenop de bag worden toegevoegd. METS/PREMIS genereert het e-depot zelf bij ingest.

Proces. De organisatiebeheerder start de export per project; een worker stelt het pakket asynchroon samen (originelen uit de Object Storage, metadata en annotaties uit de bronlagen) en plaatst het in een aparte exports-bucket met beperkte bewaartermijn. Zodra het pakket klaar is, ontvangt de organisatiebeheerder een e-mail met een directe downloadlink.

7. API-beschrijvingen

De API is gesplitst in een publieke lees-API (zonder authenticatie, voor bezoekers en API-gebruikers/harvesters) en een beheer-API (OIDC Bearer-authenticatie, voor uploaden, modereren en beheren).

7.1. Publieke API

7.2. Beheer-API

8. RSS

Twee feedtypen conform de RSS 2.0-specificatie: een platformbrede feed met nieuwe organisaties en per organisatie een feed met nieuwe jottems.

Element Bron
channel title / link / description Organisatie.naam / organisatiepagina-URL / Organisatie.beschrijving
item title Media.titel
item link / guid duurzame jottem-URL
item description Media.beschrijving + thumbnail
item pubDate Media.publicatieDatum
item enclosure Object Storage thumbs (met Media.mimeType)

9. Vocabulaires en termen

Vaste waardelijsten voorkomen vrije-tekstwildgroei en maken facetteren en linken mogelijk:

9.1. NDE Termennetwerk (GraphQL)

Alle term-lookups (genres én term-URI’s) verlopen via de GraphQL API van het NDE Termennetwerk (https://termennetwerk-api.netwerkdigitaalerfgoed.nl/graphql), één externe API die zoeken in vele terminologiebronnen bundelt (CHT, Wikidata, GTAA, thesauri van RKD, RCE, e.a.):

Terminologiebronnen per project. De organisatiebeheerder stelt per project in welke terminologiebronnen beschikbaar zijn voor term-URI’s (Project.terminologiebronnen, een lijst van bron-URI’s uit de sources-query; standaard staan alle bronnen open). De upload- en annotatie-interfaces beperken hun terms-queries tot die bronnen, zodat bijdragers per project passende thesauri aangeboden krijgen (bijv. CHT + Wikidata voor Smaak van Gouda). De backend biedt hiervoor een sources-proxy in de beheer-API.

Huisstijl in de publiekspagina’s. Het kleurenpalet van de organisatie (kleurPrimair, kleurSecundair) komt mee in de publieke API (GET /organisaties, de organisatie- en projectweergave en het jottem-detail) en kleurt de publiekspagina’s van die organisatie: de primaire kleur de koppen, de bovenrand van projectkaarten, de header en alle annotaties (kaart in de lijst, kader op de foto, popup bij dat kader); de secundaire kleur de knoppen, de linkerrand van CTA-knoppen en de footer. De frontend zet daarvoor CSS-variabelen met de Jottem-kleuren als standaard, zodat beheer-, moderatie- en profielpagina’s de basishuisstijl houden. Tekst op een gekleurd vlak wordt automatisch wit of inkt en tekst in de secundaire kleur wordt zo nodig verdonkerd, zodat de contrastverhouding altijd minimaal 4,5:1 is.

Wie heeft dit geplaatst. Bij een jottem, een annotatie en een reactie toont de publiekspagina de inzender in één regel: profielfoto (of een neutraal poppetje), naam en hoe lang geleden ("vandaag", "gisteren", "3 dagen geleden", daarna de datum). De naam zit al in de annotatie (creator); de bijbehorende profielfoto komt uit GET /deelnemers?ids=..., dat alleen gebruikers teruggeeft die hun naam publiek delen (Gebruiker.naamPubliek). Staat die uit, dan toont de pagina de anonieme weergave zonder foto.

Plaats van de organisatie. De platformbeheerder kiest per organisatie een plaats uit GeoNames (Organisatie.spatialUri + spatialNaam); die vult schema:spatialCoverage in de datasetbeschrijvingen van de organisatie. Bij het opslaan haalt de backend eenmalig de coördinaten bij die URI op (content negotiation naar RDF, wgs84_pos:lat/long) en bewaart ze in spatialLat/spatialLon. Dat is afgeleide data: niet zichtbaar of bewerkbaar in de beheerinterface, en niet opgenomen in de datasetbeschrijving. De coördinaten dienen als startpunt van de kaarten, zodat elke organisatie in de eigen omgeving begint: de speld bij het uploaden en het standpunt van de fotograaf bij de zichtveld-annotatie. Een jottem met een eigen speld wint altijd van de organisatieplaats.

10. Zoekindex en geo-data

Elasticsearch is een afgeleide store, gevuld door de indexeer-worker bij publicatie of wijziging (incl. annotatie-mutaties):

Indexveld Bron Gebruik
titel, beschrijving Media fulltext
metadata_labels, steekwoorden Metadata fulltext + facet
annotatie_teksten AnnoRepo (gesynct bij annotatie-mutatie) fulltext (usecase: annotaties zoeken)
genre, organisatie, project Media, Organisatie, Project facet
periode Metadata datering/jaren facet/range
locatie (geo_point/geo_shape) Metadata WKT/GeoJSON kaartzoeken
publicatieDatum Media sorteren

Geo-data wordt vastgelegd als WKT/GeoJSON in Metadata-rijen (adres/locatie van de jottem, standpunt fotograaf, zichtveld) en in annotatie-bodies, en gebruikt in: de kaart-frontend (MapLibre), de pand-tijdlijn (adres + openings-/sluitingsjaar), de koppeling met externe tijdmachines (zoals de Gouda Tijdmachine), Elasticsearch (geo-queries) en de RDF (schema:geo). Voor IIIF is de navPlace-extensie een optie om geo-informatie ook in manifests op te nemen.

11. Outputcontrole (traceability)

De veld→bron-tabellen in de voorgaande secties vormen de controle of alle benodigde data aan de outputkant beschikbaar is in de bronlagen. Daarbij zijn de volgende hiaten gevonden; deze zijn direct in het ERD verwerkt:

Gevonden hiaat Benodigd voor Oplossing in datamodel
Geen titel/beschrijving als eigen velden IIIF label/summary, RDF schema:name/description, RSS Media.titel, Media.beschrijving
Geen licentie/rechten per jottem IIIF rights, RDF schema:license, herbruikvoorwaarden Media.licentie (de uploader bevestigt bij het indienen de projectlicentie; die waarde wordt op de jottem vastgelegd)
Geen afbeeldingsdimensies/formaat IIIF info.json en canvas, RDF schema:width/height/encodingFormat Media.breedte, Media.hoogte, Media.mimeType (vastgelegd bij verwerking upload)
Geen wijzigingsdatum IIIF Change Discovery Update, RDF schema:dateModified Media.wijzigingsDatum + regel: elke relevante mutatie (ook annotaties) werkt deze bij
Geen organisatiebeschrijving/website publisher-informatie, IIIF provider, RSS-channel Organisatie.beschrijving, Organisatie.website
Albumconcept onderontwikkeld (persoonlijk fotoboek vs. campagne) projectpagina’s, datasetbeschrijving per project, IIIF Collection/RSS/annotaties per project Project-entiteit op organisatieniveau (uuid, naam, slug, beschrijving, oproep, periode, afbeelding, datasetLicentie, status); Media.projectId verplicht - elke jottem hoort bij precies één project en elke organisatie heeft er minstens één
Geen privacykeuze voor naamsvermelding annotatie-creator, IIIF requiredStatement, RDF schema:contributor Gebruiker.naamPubliek
Verwijderverzoeken wel in de API, niet in het datamodel afhandelworkflow, depublicatie (status "gedepubliceerd", tombstone) Verwijderverzoek-entiteit + status gedepubliceerd op Media
Geen bron voor statistieken statistieken per organisatie/project (logins, uploads, moderatie, annotaties) Gebeurtenislog-entiteit; persoonsgebonden regels na bewaartermijn geaggregeerd/geanonimiseerd (AVG)

Aandachtspunten die geen datamodel-wijziging vragen maar wel een implementatieregel zijn: