Bouw een Nextcloud-app op de Conduction-stack — Deel 4: Kennis + uitleveren
Zet xWiki lokaal op, schrijf zone-specifieke kennisartikelen, toon ze per desk via een knowledge_article-schema in OpenRegister, package de app en publiceer in de Conduction app store.
Deel 4 van de zesdelige DeskDesk-tutorial. Deel 3 liet boekingen in de NC Agenda verschijnen met één schema-annotatie. Deel 4 haalt externe kennis — zone-specifieke etiquette, equipment-notities, troubleshooting — uit xWiki naar de detail-sidebar van een desk via een knowledge_article-schema in OpenRegister. Daarna packagen we de app en leveren we hem uit.
Externe kennis integreren is dé canonieke case voor OpenConnector: data ophalen uit een systeem dat je niet bezit (xWiki hier, maar hetzelfde geldt voor Confluence, Notion, SharePoint, Decos, Mendix), en die binnen de Nextcloud-native UI tonen zonder dat de gebruiker hoeft te schakelen.
Stap 0: Start xWiki op
De Conduction-dev-stack declareert al een xWiki-service onder het xwiki-profiel van apps-extra/openregister/docker-compose.yml:
xwiki:
profiles: [xwiki, integrations]
image: xwiki:lts-postgres-tomcat
container_name: openregister-xwiki
ports: ["8086:8080"]
volumes: ["xwiki-data:/usr/local/xwiki"]
environment:
DB_USER: nextcloud
DB_PASSWORD: "!ChangeMe!"
DB_HOST: db
DB_DATABASE: xwiki
depends_on: [db]
xWiki deelt de Postgres-container met Nextcloud + OpenRegister, maar gebruikt een aparte database genaamd xwiki. Maak die eenmalig aan:
docker exec openregister-postgres psql -U nextcloud -d postgres -c "CREATE DATABASE xwiki;"
Start xWiki nu met het profiel:
cd apps-extra/openregister
docker compose --profile xwiki up -d xwiki
De eerste boot duurt ongeveer een minuut. xWiki initialiseert zijn schema in de lege Postgres-database en pakt gebundelde webapps uit. Wacht tot de health check op healthy springt:
docker ps --filter name=openregister-xwiki --format '{{.Status}}'
# Up 22 seconds (health: starting) <- still booting
# Up 1 minute (healthy) <- ready
Open http://localhost:8086. xWiki stuurt je door naar de Distribution Wizard, omdat de lege database geen admin-user en geen flavor heeft.
Stap 0a: Loop door de Distribution Wizard
- Continue voorbij het welkomstscherm.
- Step 1 - Admin user. Vul in:
Klik Register and login. Je bent nu ingelogd alsFirst Name: Desk Last Name: Admin Username: admin Password: admin1234 (any 8+ chars works) Confirm: admin1234 Email: admin@deskdesk.localadmin. - Step 2 - Flavor. Klik Let the wiki be empty (de gebundelde Standard Flavor is een download van zo'n 150 MB die we voor deze tutorial niet nodig hebben). Bevestig.
- Step 5 - Report toont de aangemaakte pagina's (alleen
HomeenXWiki). Klik Continue om op de hoofdpagina te landen.
De wiki is nu gebootstrapt. Doe een sanity check op het REST-endpoint:
curl -u admin:admin1234 -H 'Accept: application/json' \
http://localhost:8086/rest/wikis/xwiki/spaces | head -c 200
# {"links":[],"spaces":[{"links":[...,"id":"xwiki:XWiki", ...}]}
Twee dingen om over de URL te weten: de REST API leeft op /rest, niet /xwiki/rest (dat laatste is het pad voor de menselijke weergave), en basic auth is de standaard.
Stap 1: Schrijf de kennisartikelen
Voor deze tutorial gebruiken we een eigen parent space DeskDeskKnowledge met één subspace per zone. De mappenstructuur matcht de zone-enum van de desk (east, central, west, north, south), zodat elke desk netjes op de artikelen van zijn zone uitkomt.
Je kunt de artikelen via xWiki's UI schrijven (Create Page → set parent space → write content). Sneller: spreek de REST API van xWiki direct aan. Dezelfde payload-vorm werkt vanuit cURL, OpenConnector of een willekeurige HTTP-client.
put() {
curl -s -u admin:admin1234 -X PUT \
-H "Content-Type: application/xml" \
-d "<page xmlns=\"http://www.xwiki.org\"><title>$3</title><syntax>xwiki/2.1</syntax><content>$4</content></page>" \
"http://localhost:8086/rest/wikis/xwiki/spaces/DeskDeskKnowledge/spaces/$1/pages/$2" \
-o /dev/null -w "%{http_code} $1/$2\n"
}
put East Etiquette "East zone etiquette" \
"The east windows get direct sun 14:00 to 17:00 in summer. Bring a curtain clip from the supplies cabinet on floor 3. Phone calls are fine in this zone, phonebooths are a 30-second walk away."
put Central MeetingDesks "Central zone meeting desks" \
"Central desks 11 to 14 seat four. The cabling channel under each desk has two HDMI ports and USB-C 100W power. Camera and mic in the ceiling are wired into the in-room Jitsi instance. See the QR code on the desk."
put West QuietRules "West zone quiet rules" \
"The west zone is the designated quiet zone. No calls, no meetings. Keep notifications muted. The kitchen is on the opposite side of the floor for a reason."
Elke call geeft 201 terug. Surf naar http://localhost:8086/xwiki/bin/view/DeskDeskKnowledge/East/Etiquette om het East-artikel in de standaardweergave van xWiki te zien. Dezelfde inhoud is ook via REST op te halen:
curl -u admin:admin1234 \
http://localhost:8086/rest/wikis/xwiki/spaces/DeskDeskKnowledge/spaces/East/pages
Drie korte artikelen. Realistisch genoeg dat je ze in productie ook zo zou schrijven. Het belangrijkste stuk is de subspace-naam: dat is waar we vanuit DeskDesk op pivoten.
Waarom de tutorial je het schrijven van artikelen laat doen in plaats van ze programmatisch te seeden: in echte teams zijn wiki-artikelen organisch — geschreven door wie het etiquette-gat opmerkt, bewerkt door de volgende persoon, gearchiveerd als de plattegrond verandert. De integratie die we bouwen maakt ze vindbaar vanaf waar de gebruiker al staat, niet andersom.
Stap 2: Voeg een knowledge_article-schema toe aan DeskDesk
Voordat we vanuit xWiki gaan synchroniseren, geef je OpenRegister een plek om de artikelen op te slaan. Voeg een vierde schema toe aan lib/Settings/deskdesk_register.json:
"knowledge_article": {
"slug": "knowledge_article",
"icon": "BookOpenVariantOutline",
"version": "0.1.0",
"title": "Knowledge article",
"description": "An external knowledge article surfaced in DeskDesk via OpenConnector. Read-only — canonical source lives in xWiki.",
"type": "object",
"required": ["name", "zone"],
"properties": {
"name": { "type": "string" },
"zone": { "type": "string", "enum": ["north", "south", "east", "west", "central"] },
"body": { "type": "string" },
"url": { "type": "string", "format": "uri" },
"externalId": { "type": "string" }
}
}
Belangrijk: de JSON-key en het slug-veld moeten matchen (knowledge_article aan beide kanten). OpenRegister bouwt het REST-endpoint vanuit de slug, dus een mismatch levert Schema not found: 'knowledge_article' op bij ophalen.
Bump terwijl je daar bent ook de info.version van het register naar 0.3.0, zodat de importhandler het nieuwe schema bij de volgende run oppikt:
"info": {
"title": "DeskDesk Register",
"description": "Floors, desks, bookings, and knowledge articles.",
"version": "0.3.0"
}
Seed ook drie artikelen die matchen met de xWiki-inhoud. De seeds maken de app demo-baar voordat je de sync hebt aangesloten, en dienen als referentie-vorm voor de OpenConnector-mapping die je in Stap 3 bouwt.
"knowledge-east-etiquette": {
"@self": { "register": "deskdesk", "schema": "knowledge_article", "slug": "knowledge-east-etiquette" },
"name": "East zone etiquette",
"zone": "east",
"body": "The east windows get direct sun 14:00 to 17:00 in summer...",
"url": "http://localhost:8086/xwiki/bin/view/DeskDeskKnowledge/East/Etiquette",
"externalId": "xwiki:DeskDeskKnowledge.East.Etiquette"
}
Voeg de knowledge_article-slug toe aan de kleine lijst SettingsService::SCHEMA_SLUGS in lib/Service/SettingsService.php, zodat /api/settings het numerieke id naast de andere schema's blootlegt. Hetzelfde in src/store/store.js:
const SCHEMAS = ['floor', 'desk', 'booking', 'knowledge_article']
Herlaad de configuratie zodat OpenRegister het nieuwe schema + de seeds oppikt:
docker exec nextcloud apache2ctl graceful
# then from your Nextcloud session, POST /apps/deskdesk/api/settings/load
# (the deskdesk admin page has a Reload button that does this for you)
Stap 3: Maak een OpenConnector-source voor xWiki aan
De seeds krijgen de UI meteen aan de praat. Voor live data — als een artikel in xWiki verandert wil je die wijziging bij de volgende sync in DeskDesk zien — sluit je OpenConnector aan.
Ga naar /apps/openconnector/sources en maak een nieuwe source.
Name: xWiki Knowledge
Type: API
Location: http://openregister-xwiki:8080/rest
(container-to-container hostname; from the host you use http://localhost:8086/rest)
Auth: Basic
Username: admin
Password: admin1234
Headers: Accept: application/json
Test: GET /wikis/xwiki/spaces/DeskDeskKnowledge/spaces/East/pages
De Test zou het East-artikel moeten teruggeven. OpenConnector slaat de source op met een numeriek id; we refereren ernaar vanuit de synchronisation in de volgende stap.
Stap 4: Definieer de synchronisation
Sources tonen ruwe API-responses; synchronisations mappen ze naar OpenRegister-objecten. Maak een synchronisation aan:
Name: xWiki articles → OpenRegister
Source: xWiki Knowledge (the one you just made)
Source endpoint: /wikis/xwiki/spaces/DeskDeskKnowledge/spaces/{zone}/pages
with zone iterated over ['East', 'Central', 'West']
Target register: deskdesk
Target schema: knowledge_article
Mapping:
title → name
content → body
spaces.last → zone (lowercased)
id → externalId
xwikiAbsoluteUrl → url
Schedule: every 30 minutes
Sla op en draai één keer. Je ziet drie knowledge_article-objecten in OpenRegister waarvan het externalId-veld begint met xwiki:. Verwijder de seed-objecten die je in Stap 2 toevoegde: ze worden vervangen door de gesyncde versies.
Stap 5: Toon artikelen in CnObjectSidebar
De desk-detail-pagina mount al een CnObjectSidebar. Declareer een custom tab in het manifest om er een Kennis-tab aan toe te voegen:
{
"id": "desks-detail",
"route": "/desks/:id",
"type": "detail",
"title": "deskdesk.desks.detailTitle",
"config": {
"register": "deskdesk",
"schema": "desk",
"sidebar": {
"tabs": [
{ "id": "data", "label": "deskdesk.sidebar.data", "widgets": [{ "type": "data" }] },
{ "id": "knowledge", "label": "deskdesk.sidebar.knowledge", "component": "knowledge-tab" },
{ "id": "metadata", "label": "deskdesk.sidebar.metadata", "widgets": [{ "type": "metadata" }] }
]
}
}
}
De data- en metadata-tabs gebruiken ingebouwde widget-types. De knowledge-tab wijst naar een custom component knowledge-tab, dat de App registreert via de customComponents-map.
Maak het tab-component zelf aan op src/views/KnowledgeTab.vue:
<template>
<div class="knowledge-tab">
<NcLoadingIcon v-if="loading" :size="32" />
<NcEmptyContent
v-else-if="!articles.length"
:name="t('deskdesk', 'No articles for this zone yet')" />
<article v-for="article in articles" :key="article.id" class="knowledge-tab__article">
<header class="knowledge-tab__head">
<h3>{{ article.name }}</h3>
<a v-if="article.url" :href="article.url" target="_blank" rel="noopener noreferrer">
{{ t('deskdesk', 'Open in wiki') }} ↗
</a>
</header>
<p class="knowledge-tab__body">{{ article.body }}</p>
</article>
</div>
</template>
<script>
import { NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
import { useObjectStore } from '../store/store.js'
export default {
name: 'KnowledgeTab',
components: { NcEmptyContent, NcLoadingIcon },
props: {
objectId: { type: String, required: true },
objectType: { type: String, default: 'desk' },
},
setup() { return { objectStore: useObjectStore() } },
data() { return { articles: [], loading: true } },
watch: {
objectId: {
immediate: true,
async handler() {
this.loading = true
try {
const desk = await this.objectStore.fetchObject(this.objectType, this.objectId)
if (!desk?.zone) { this.articles = []; return }
await this.objectStore.fetchCollection('knowledge_article', { zone: desk.zone, _limit: 25 })
this.articles = this.objectStore.collections.knowledge_article || []
} finally { this.loading = false }
},
},
},
}
</script>
Registreer het component in src/App.vue en geef het door aan CnAppRoot via de :custom-components-prop:
<script>
import KnowledgeTab from './views/KnowledgeTab.vue'
const customComponents = {
'knowledge-tab': KnowledgeTab,
}
export default {
/* ... */
data() {
return { customComponents, /* ... */ }
},
}
</script>
<template>
<CnAppRoot
:app-id="appId"
:manifest="manifest"
:page-types="pageTypes"
:custom-components="customComponents">
<template #sidebar>
<CnObjectSidebar v-if="objectSidebarState.active" ... />
</template>
</CnAppRoot>
</template>
(Zie de reference repo voor het volledige bestand. App.vue voorziet ook in objectSidebarState, zodat het externe-sidebar-kanaal van CnDetailPage werkt.)
Bouw, herlaad en navigeer naar de detail-URL van een 3-East-desk. De rechter sidebar toont drie tabs: Data, Kennis, Metadata. Klik op Kennis. Het artikel "East zone etiquette" verschijnt, met de body gerenderd en een link terug naar de wiki om te bewerken.
Navigeer naar een 2-West-desk: zelfde tab, ander artikel ("West zone quiet rules"). De zonefilter doet de match gratis voor je; je hoefde geen per-desk bedrading te schrijven.
Stap 6: Package de app
Het release-proces is mechanisch:
- Bump de versie in
appinfo/info.xml(<version>0.1.0</version>→<version>0.4.0</version>). - Draai de production build:
composer install --no-dev --optimize-autoloader npm install --legacy-peer-deps npm run build - Strip ongewenste bestanden — vendor-dev-dependencies, node_modules, tests, openspec-changes, .git, .github (je CI staat al op source-niveau). Het template levert een
Makefile-target hiervoor; anders is het patroon voor de productie-tarball:tar --exclude=node_modules --exclude=vendor/bin \ --exclude=tests --exclude=openspec/changes \ --exclude=.git --exclude=.github \ -czf deskdesk-0.4.0.tar.gz deskdesk/ - Onderteken de tarball met de EC-key die de Nextcloud app store verwacht (zie de Nextcloud-docs).
- Push naar GitHub met een tag die overeenkomt met de versie (
git tag v0.4.0 && git push origin v0.4.0). Het template levert een.github/workflows/release.ymldie de tag oppikt, de tarball bouwt, ondertekent en de release publiceert.
Voor de Conduction app store (apps.conduction.nl) duwt dezelfde release-pipeline naar dat endpoint als de secret is geconfigureerd. Zie de .github/workflows/release.yml als referentie.
Stap 7: Indienen bij apps.nextcloud.com (optioneel)
Als je DeskDesk in de publieke Nextcloud app store wilt, registreer je je op apps.nextcloud.com, upload je je ondertekende tarball, vul je het formulier in (beschrijving, screenshots, licentie — EUPL-1.2 is prima — categorieën organization) en wacht je op review. De cyclus is meestal 1 tot 3 dagen voor een nieuwe app.
Wat je hebt gebouwd
Je begon Deel 1 met een leeg template. Achtenveertig stappen verder heb je:
- Een echte Nextcloud-app met het canonieke chassis
- Vier OpenRegister-schema's met relaties en seed-data
- Een manifest-gedreven shell met twee kleine wrappers (Index, Detail) die verdwijnen zodra de library auto-fetch in de default page-types verdraagt
- Boekingen die via een schema-annotatie in de NC Agenda verschijnen
- Kennisartikelen uit xWiki die via OpenConnector in de desk-detail-sidebar verschijnen
- Een geversioneerde, gepackagede, publiceerbare release
Totaal aan geschreven Vue: drie kleine bestanden (IndexPageWrapper, DetailPageWrapper, KnowledgeTab), samen ongeveer 100 regels. Totaal PHP: het rename-ritueel, een pad-resolutie-fix in SettingsService en de schema-slug-lijst. Al het andere is JSON.
Die verhouding — JSON beschrijft wat je wil, het framework rendert het — is het hele punt van de Conduction-stack. Schema's sturen zowel backend als frontend. Manifesten sturen de shell. Schema-annotaties sturen cross-app-integraties. Jij declareert; het framework rendert.
Troubleshooting
Waar nu naartoe
Volgende stappen
Deel 5 en Deel 6 staan naast elkaar — ze breiden de uitgeleverde app uit in twee onafhankelijke richtingen. Pak ze in willekeurige volgorde, of kies degene die je volgende app het eerst nodig heeft. Deel 5 verdiept het manifest-vlak; Deel 6 verbreedt naar andere systemen. Geen van beide verwijst naar de ander voor inhoud.
