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:
-
BagIt (RFC 8493) als omhulsel - een zip met
data/-payload en checksummanifesten (manifest-sha512.txt,bag-info.txt), zodat de ontvangende instelling bit-integriteit bij ingest kan verifiëren. BagIt wordt native ondersteund door Archivematica en wordt probleemloos geaccepteerd door Preservica - de systemen achter de meeste Nederlandse e-depots. -
RO-Crate als inhoudelijke beschrijving - een
ro-crate-metadata.json(JSON-LD op basis van schema.org) die alle bestanden in het pakket beschrijft en relateert. Omdat de Jottem-metadata al schema.org AP NDE is, volgt de crate vrijwel direct uit de bestaande RDF-mapping; het pakket blijft daardoor ook búiten een e-depot zelfbeschrijvend en herbruikbaar.
{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:
-
Genre/materiaaltype (
Media.genre): foto, menukaart, advertentie, folder, krantenartikel, vergunning, audio, overig - elk gekoppeld aan een term uit de Cultuurhistorische Thesaurus (CHT), opgezocht via het NDE Termennetwerk (zie hieronder). De waardelijst met bijbehorende CHT-URI’s is platformconfiguratie. -
Metadata-types (
Metadata.type): vervaardiger, datering, plaats, adres, openingsjaar, sluitingsjaar, persoon, gebouw, bedrijf, gebeurtenis, archiefbron, steekwoord, standpunt (WKT), zichtveld (WKT), herinnering. -
Annotatiemotivaties (
motivation, W3C-vocabulaire):describing(herinnering),commenting(reactie),identifying(persoon/gebouw/bedrijf op vlak),tagging(steekwoord),linking(archiefbron, gebeurtenis),editing(correctie). -
Term-URI’s: bij plaatsnamen, personen, gebeurtenissen en gebouwen wordt naast het label een URI vastgelegd (
Metadata.uriresp. annotation body), zodat de RDF-output linkbaar is.
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.):
-
de
sources-query levert de lijst van beschikbare terminologiebronnen; -
de
terms-query zoekt een term in één of meer gekozen bronnen en levert label + URI.
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:
-
Reproduceerbaarheid: Elasticsearch en Fuseki zijn volledig herbouwbaar uit PostgreSQL + AnnoRepo + Object Storage; een herindexeer-/hersync-taak hoort bij de beheervoorzieningen. Ook de e-depot-export is volledig afleidbaar uit deze bronlagen.
-
Depublicatie: bij een gehonoreerd verwijderverzoek worden alle outputs geschoond (index, RDF/named graph, feeds, IIIF-cache-purge) en resteert een tombstone op de duurzame URL.
-
Audio (fase 2):
durationwordt bij verwerking van audio-uploads vastgelegd (technische metadata bij het mediabestand) ten behoeve van het IIIF-canvas. -
Vocabulaire-URI’s (genre e.d.) zijn configuratie, geen databasemodel; ze moeten wel versioneerbaar beheerd worden. De per project beschikbare terminologiebronnen komen uit
Project.terminologiebronnen(zie § 9.1 NDE Termennetwerk (GraphQL)).